├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ ├── rust.yml │ └── release.yml ├── test.png ├── example.png ├── test.xylo ├── src ├── lib.rs ├── functions │ ├── system.rs │ ├── func.rs │ ├── character.rs │ ├── path.rs │ ├── rand.rs │ ├── mod.rs │ ├── shape.rs │ ├── compare.rs │ ├── color.rs │ └── transform.rs ├── error.rs ├── main.rs ├── minify.rs ├── format.rs └── colors.rs ├── CODE_OF_CONDUCT.md ├── example.xylo ├── LICENSE ├── Cargo.toml ├── CONTRIBUTING.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: giraffekey 2 | liberapay: xylo 3 | -------------------------------------------------------------------------------- /test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffekey/xylo/HEAD/test.png -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffekey/xylo/HEAD/example.png -------------------------------------------------------------------------------- /test.xylo: -------------------------------------------------------------------------------- 1 | root = hsl (rand * 360) 0.4 0.2 FILL : grid 2 | 3 | grid_size = 10 4 | 5 | grid = t (-width / 2.0) (-height / 2.0) (ss (float width / grid_size) (collect rows)) 6 | 7 | rows = 8 | for i in 0..grid_size 9 | collect (cols i) 10 | 11 | cols i = 12 | for j in 0..grid_size 13 | hsl (rand * 360) 0.5 0.6 ( 14 | t (i + 0.5) (j + 0.5) (r (rand * 360) (ss 0.375 SQUARE))) 15 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "no-std", no_std)] 2 | 3 | #[cfg(feature = "no-std")] 4 | extern crate alloc; 5 | 6 | #[cfg(all(feature = "std", feature = "no-std"))] 7 | compile_error!("Enable either `std` or `no-std`, but not both!"); 8 | 9 | #[cfg(not(any(feature = "std", feature = "no-std")))] 10 | compile_error!("Either `std` or `no-std` must be enabled!"); 11 | 12 | mod colors; 13 | mod error; 14 | mod format; 15 | mod functions; 16 | mod interpreter; 17 | mod minify; 18 | mod out; 19 | mod parser; 20 | mod renderer; 21 | mod shape; 22 | 23 | pub use error::{Error, Result}; 24 | pub use format::format; 25 | pub use minify::minify; 26 | pub use out::*; 27 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Build 19 | run: cargo build --release --verbose 20 | 21 | - name: Run std tests 22 | run: cargo test --release --verbose 23 | 24 | - name: Run no_std tests 25 | run: cargo test --release --verbose --no-default-features --features no-std 26 | 27 | - name: Run no IO tests 28 | run: cargo test --release --verbose --no-default-features --features std simd 29 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ### Rules 4 | 5 | 1. **Be respectful.** No harassment, insults, or discrimination. 6 | 2. **Stay on topic.** Keep discussions relevant to Xylo. 7 | 3. **No spam.** This includes excessive self-promotion. 8 | 4. **Keep it legal.** Don't share harmful content. 9 | 10 | ### Reporting 11 | 12 | - Violations can be reported to maintainers via [e-mail](mailto:contact@masterofgiraffe.com) or [Discord](https://discord.gg/xUQXFQ5DyZ). 13 | - Include evidence (screenshots, logs) if possible. 14 | 15 | ### Consequences 16 | 17 | - Minor violations: Warning 18 | - Repeat or severe violations: Ban 19 | 20 | This applies to all Xylo spaces (GitHub, Discord, Reddit, Telegram). 21 | -------------------------------------------------------------------------------- /example.xylo: -------------------------------------------------------------------------------- 1 | root = 2 | l 0 FILL : ss 400 bouquet 3 | 4 | quality = 750 5 | 6 | bouquet = 7 | r 36 (ss 1.3 flower5) 8 | : r 15 flower6 9 | 10 | flower6 = 11 | demiflower6 12 | : flip 90 demiflower6 13 | 14 | flower5 = 15 | demiflower5 16 | : flip 90 demiflower5 17 | 18 | demiflower6 = 19 | collect ( 20 | for i in 0..12 21 | r (i * 30) (petal6 quality)) 22 | 23 | demiflower5 = 24 | collect ( 25 | for i in 0..5 26 | r (i * 72) (petal5 quality)) 27 | 28 | petal5 i = 29 | if i == 0 30 | EMPTY 31 | else 32 | s 1 0.0001 SQUARE 33 | : ss 0.005 (tx -0.5 (l 0 CIRCLE)) 34 | : tx 0.5 (r 144.04 (ss 0.998 (tx 0.5 (lshift (1 / quality * 0.6) (petal5 (i - 1)))))) 35 | 36 | petal6 i = 37 | if i == 0 38 | EMPTY 39 | else 40 | s 1 0.0001 SQUARE 41 | : tx -0.5 (ss 0.01 (l 0 CIRCLE)) 42 | : tx 0.5 (r 120.21 (ss 0.996 (tx 0.5 (lshift (1 / quality) (petal6 (i - 1)))))) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2025 giraffekey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Binaries 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | matrix: 17 | include: 18 | - os: ubuntu-latest 19 | target: target/release/xylo-lang 20 | file_name: xylo_ubuntu 21 | - os: macos-latest 22 | target: target/release/xylo-lang 23 | file_name: xylo_macos 24 | - os: windows-latest 25 | target: target/release/xylo-lang.exe 26 | file_name: xylo_windows.exe 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Build 32 | run: | 33 | cargo build --release 34 | mv ${{ matrix.target }} ${{ matrix.file_name }} 35 | 36 | - name: Create Release 37 | id: create_release 38 | uses: softprops/action-gh-release@v2 39 | if: github.ref_type == 'tag' 40 | with: 41 | files: | 42 | ${{ matrix.file_name }} 43 | -------------------------------------------------------------------------------- /src/functions/system.rs: -------------------------------------------------------------------------------- 1 | use crate::builtin_function; 2 | use crate::error::{Error, Result}; 3 | use crate::interpreter::{Data, Value}; 4 | 5 | use rand_chacha::ChaCha8Rng; 6 | 7 | builtin_function!(width data => { 8 | [] => |data: &Data| Ok(Value::Integer(data.dimensions.0 as i32)), 9 | }); 10 | 11 | builtin_function!(height data => { 12 | [] => |data: &Data| Ok(Value::Integer(data.dimensions.1 as i32)), 13 | }); 14 | 15 | #[cfg(test)] 16 | mod tests { 17 | use super::*; 18 | use rand::SeedableRng; 19 | 20 | #[test] 21 | fn test_width_height_functions() { 22 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 23 | 24 | // Test with standard dimensions 25 | let data = Data { 26 | dimensions: (800, 600), 27 | ..Default::default() 28 | }; 29 | 30 | // Test width 31 | assert_eq!(width(&mut rng, &data, &[]).ok(), Some(Value::Integer(800))); 32 | 33 | // Test height 34 | assert_eq!(height(&mut rng, &data, &[]).ok(), Some(Value::Integer(600))); 35 | 36 | // Test with minimum dimensions 37 | let min_data = Data { 38 | dimensions: (1, 1), 39 | ..Default::default() 40 | }; 41 | 42 | assert_eq!( 43 | width(&mut rng, &min_data, &[]).ok(), 44 | Some(Value::Integer(1)) 45 | ); 46 | assert_eq!( 47 | height(&mut rng, &min_data, &[]).ok(), 48 | Some(Value::Integer(1)) 49 | ); 50 | 51 | // Test with maximum i32 dimensions 52 | let max_data = Data { 53 | dimensions: (i32::MAX as u32, i32::MAX as u32), 54 | ..Default::default() 55 | }; 56 | 57 | assert_eq!( 58 | width(&mut rng, &max_data, &[]).ok(), 59 | Some(Value::Integer(i32::MAX)) 60 | ); 61 | assert_eq!( 62 | height(&mut rng, &max_data, &[]).ok(), 63 | Some(Value::Integer(i32::MAX)) 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xylo-lang" 3 | version = "0.1.3" 4 | edition = "2021" 5 | license = "MIT" 6 | authors = ["giraffekey "] 7 | description = "A functional programming language for generative art." 8 | repository = "https://github.com/giraffekey/xylo" 9 | readme = "README.md" 10 | categories = [ 11 | "command-line-utilities", 12 | "graphics", 13 | "no-std", 14 | "rendering", 15 | "visualization" 16 | ] 17 | keywords = [ 18 | "language", 19 | "procedural", 20 | "art" 21 | ] 22 | 23 | [profile.release] 24 | codegen-units = 1 25 | opt-level = "z" 26 | lto = true 27 | debug = false 28 | panic = "abort" 29 | strip = true 30 | 31 | [dependencies] 32 | ahash = { version = "0.8", default-features = false } 33 | asdf-pixel-sort = { version = "0.2.1", git = "https://github.com/giraffekey/asdf-pixel-sort", optional = true } 34 | base64 = { version = "0.22", default-features = false } 35 | clap = { version = "4.5", features = ["derive"] } 36 | factorial = "0.4" 37 | fontdue = { version = "0.9", optional = true } 38 | hashbrown = "0.15" 39 | image = { version = "0.25", optional = true } 40 | imageproc = { version = "0.25", optional = true } 41 | itertools = { version = "0.14", default-features = false } 42 | noise = "0.9" 43 | nom = { version = "8.0", default-features = false } 44 | num = "0.4" 45 | num-bigint = "0.4.6" 46 | palette = { version = "0.7", default-features = false } 47 | png = "0.17" 48 | rand = { version = "0.9", default-features = false } 49 | rand_chacha = { version = "0.9", default-features = false } 50 | sha2 = "0.10" 51 | tiny-skia = { version = "0.11", default-features = false, optional = true } 52 | voronoi = "0.1" 53 | 54 | [features] 55 | default = ["std", "io", "simd"] 56 | std = [ 57 | "ahash/std", 58 | "base64/std", 59 | "itertools/use_std", 60 | "nom/std", 61 | "palette/std", 62 | "rand/std", 63 | "rand/thread_rng", 64 | "rand_chacha/std", 65 | "tiny-skia/std", 66 | "tiny-skia/png-format", 67 | ] 68 | io = [ 69 | "asdf-pixel-sort", 70 | "fontdue", 71 | "image", 72 | "imageproc", 73 | ] 74 | no-std = [ 75 | "base64/alloc", 76 | "hashbrown/alloc", 77 | "itertools/use_alloc", 78 | "nom/alloc", 79 | "palette/alloc", 80 | "palette/libm", 81 | "rand/alloc", 82 | "tiny-skia/no-std-float" 83 | ] 84 | simd = ["tiny-skia/simd"] 85 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "no-std")] 2 | use alloc::{ 3 | format, 4 | string::{String, ToString}, 5 | }; 6 | 7 | #[derive(Debug)] 8 | pub enum Error { 9 | ParseError, 10 | NotDigit(String), 11 | InvalidList, 12 | InvalidRange, 13 | InvalidRoot, 14 | MissingSeed, 15 | UnknownFunction(String), 16 | InvalidArgument(String), 17 | InvalidDefinition(String), 18 | InvalidCondition, 19 | InvalidMatch, 20 | MatchNotFound, 21 | NotIterable, 22 | NegativeNumber, 23 | OutOfBounds, 24 | NotFound, 25 | MaxDepthReached, 26 | NoIO, 27 | PngError(png::EncodingError), 28 | #[cfg(feature = "std")] 29 | FileError(std::io::Error), 30 | } 31 | 32 | impl ToString for Error { 33 | fn to_string(&self) -> String { 34 | match self { 35 | Error::ParseError => "Could not parse file.".into(), 36 | Error::NotDigit(name) => format!("Value passed to `{}` was not a digit.", name), 37 | Error::InvalidList => "Type mismatch in list.".into(), 38 | Error::InvalidRange => "Invalid range.".into(), 39 | Error::InvalidRoot => "The `root` function must return a shape.".into(), 40 | Error::MissingSeed => "Seed required for rng.".into(), 41 | Error::UnknownFunction(name) => format!("Unknown function `{}`.", name), 42 | Error::InvalidArgument(name) => { 43 | format!("Invalid argument passed to `{}` function.", name) 44 | } 45 | Error::InvalidDefinition(name) => { 46 | format!("Incorrect parameters in `{}` function.", name) 47 | } 48 | Error::InvalidCondition => "If condition must reduce to a boolean.".into(), 49 | Error::InvalidMatch => "Incorrect type comparison in match statement.".into(), 50 | Error::MatchNotFound => "Not all possibilities covered in match statement".into(), 51 | Error::NotIterable => "Value is not iterable.".into(), 52 | Error::NegativeNumber => "Number cannot be negative.".into(), 53 | Error::OutOfBounds => "Index out of bounds.".into(), 54 | Error::NotFound => "Value not found.".into(), 55 | Error::MaxDepthReached => "Max call stack depth reached.".into(), 56 | Error::NoIO => "Cannot use IO functions without the io feature enabled.".into(), 57 | Error::PngError(e) => e.to_string(), 58 | #[cfg(feature = "std")] 59 | Error::FileError(e) => e.to_string(), 60 | } 61 | } 62 | } 63 | 64 | pub type Result = core::result::Result; 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Xylo 2 | 3 | Thank you for your interest in contributing to Xylo! Whether you're improving the core language, adding examples, or fixing bugs, your work helps grow a creative coding tool for artists and programmers. 4 | 5 | ## Ways to Contribute 6 | 7 | ### 1. Reporting Issues 8 | 9 | Found a bug or have a feature request? [Open an issue](https://github.com/giraffekey/xylo/issues/new) with: 10 | 11 | - A clear title. 12 | - Steps to reproduce the problem. 13 | - Expected vs. actual behavior (include screenshots if it's a visual bug). 14 | - Your environment (OS, Xylo version/build, Rust version, etc.). 15 | 16 | ### 2. Improving Documentation 17 | 18 | Help make Xylo more accessible by: 19 | 20 | - Fixing typos/clarifying the [GitBook docs](https://github.com/giraffekey/xylo-docs/). 21 | - Adding tutorials (e.g., "How to create L-systems in Xylo"). 22 | - Documenting edge cases or compiler behaviors. 23 | 24 | ### 3. Setting Up for Development 25 | 26 | #### Prerequisites 27 | 28 | - Git 29 | - Rust and Cargo (latest stable version). 30 | 31 | #### Clone and Build 32 | 33 | ```sh 34 | git clone https://github.com/giraffekey/xylo.git 35 | cd xylo 36 | cargo build # Debug build 37 | cargo build --release # Release build 38 | ``` 39 | 40 | #### Run Tests 41 | 42 | ```sh 43 | cargo test # Unit tests (std) 44 | cargo test --no-default-features --features no-std # Unit tests (no_std) 45 | cargo run --release -- generate example.xylo --width 800 --height 800 # Run the example 46 | ``` 47 | 48 | ### 4. Making Changes 49 | 50 | #### Code Structure 51 | 52 | - **`src/`**: Interpreter core (parser, interpreter, renderer). 53 | - **`src/functions/`**: Standard library functions 54 | 55 | #### Workflow 56 | 57 | 1. **Branch off `main`**: 58 | ```sh 59 | git checkout -b your-branch 60 | ``` 61 | 2. **Follow Rust conventions**: 62 | - Use `snake_case` for variables/functions. 63 | - Document public APIs with `///` comments. 64 | - Format with `cargo fmt` and lint with `cargo clippy`. 65 | 3. **Test rigorously**: 66 | - Add unit tests in `src/`. 67 | - Verify generative art outputs. 68 | 69 | #### Commit Messages 70 | 71 | Commit messages should be descriptive and finish the sentence "This commit will...". 72 | 73 | Examples: 74 | ``` 75 | Add HSL color utilities 76 | Handle division by zero errors 77 | Add a function for creating fractals 78 | ``` 79 | 80 | ### 5. Submitting a Pull Request 81 | 82 | 1. **Push your branch**: 83 | ```sh 84 | git push origin your-branch 85 | ``` 86 | 2. **Open a PR**: 87 | - Target the `main` branch. 88 | - Describe changes and reference related issues (e.g., `Fixes #42`). 89 | - Include screenshots for changes that involve art generation. 90 | 3. **Address feedback**: Maintainers will review within 48 hours. 91 | 92 | ## Contribution Areas 93 | 94 | ### A. Core Interpreter 95 | 96 | - Optimize performance and resource usage. 97 | - Extend the type system or syntax. 98 | - Write extensive tests for edge cases. 99 | 100 | ### B. Standard Library 101 | 102 | - Add built-in utility functions. 103 | - Improve error messages and handling. 104 | 105 | ### C. Tooling 106 | 107 | - WASM/web playground. 108 | - IDE plugins (VS Code, IntelliJ). 109 | 110 | ## Code of Conduct 111 | 112 | Please review our [Code of Conduct](CODE_OF_CONDUCT.md) before participating. 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xylo   [![License]][license] [![Build Status]][actions] [![Latest Version]][crates.io] [![Docs]][docs.rs] 2 | 3 | [License]: https://img.shields.io/badge/license-MIT-blue.svg 4 | [license]: https://github.com/giraffekey/xylo/blob/main/LICENSE 5 | [Build Status]: https://github.com/giraffekey/xylo/actions/workflows/rust.yml/badge.svg 6 | [actions]: https://github.com/giraffekey/xylo/actions 7 | [Latest Version]: https://img.shields.io/crates/v/xylo-lang.svg 8 | [crates.io]: https://crates.io/crates/xylo-lang 9 | [Docs]: https://docs.rs/xylo-lang/badge.svg 10 | [docs.rs]: https://docs.rs/xylo-lang/latest/xylo_lang/index.html 11 | 12 | Xylo is a domain specific language of the functional paradigm that is designed for use in creative coding and procedural art. 13 | 14 | Documentation: https://xylo-1.gitbook.io/docs 15 | 16 | ![example](example.png) 17 | 18 | ## Example 19 | 20 | ```ocaml 21 | root = 22 | l 0 FILL : ss 400 bouquet 23 | 24 | quality = 750 25 | 26 | bouquet = 27 | r 36 (ss 1.3 flower5) 28 | : r 15 flower6 29 | 30 | flower6 = 31 | demiflower6 32 | : flip 90 demiflower6 33 | 34 | flower5 = 35 | demiflower5 36 | : flip 90 demiflower5 37 | 38 | demiflower6 = 39 | collect ( 40 | for i in 0..12 41 | r (i * 30) (petal6 quality)) 42 | 43 | demiflower5 = 44 | collect ( 45 | for i in 0..5 46 | r (i * 72) (petal5 quality)) 47 | 48 | petal5 i = 49 | if i == 0 50 | EMPTY 51 | else 52 | s 1 0.0001 SQUARE 53 | : ss 0.005 (tx -0.5 (l 0 CIRCLE)) 54 | : tx 0.5 (r 144.04 (ss 0.998 (tx 0.5 (lshift (1 / quality * 0.6) (petal5 (i - 1)))))) 55 | 56 | petal6 i = 57 | if i == 0 58 | EMPTY 59 | else 60 | s 1 0.0001 SQUARE 61 | : tx -0.5 (ss 0.01 (l 0 CIRCLE)) 62 | : tx 0.5 (r 120.21 (ss 0.996 (tx 0.5 (lshift (1 / quality) (petal6 (i - 1)))))) 63 | ``` 64 | 65 | ## Installation 66 | 67 | ### Released Builds 68 | 69 | You can find the latest release of the Xylo binaries [here](https://github.com/giraffekey/xylo/releases/latest). 70 | 71 | ### Cargo Install 72 | 73 | You can install Xylo using Cargo: 74 | 75 | ```sh 76 | cargo install xylo-lang 77 | ``` 78 | 79 | ### Manual Build (Linux) 80 | 81 | Clone the repo: 82 | 83 | ```sh 84 | git clone https://github.com/giraffekey/xylo 85 | ``` 86 | 87 | Build the repo: 88 | 89 | ```sh 90 | cargo build --release 91 | ``` 92 | 93 | Copy the CLI: 94 | 95 | ```sh 96 | sudo cp target/release/xylo-lang /usr/bin/xylo 97 | ``` 98 | 99 | ## Usage 100 | 101 | Write some Xylo code in a `.xylo` file e.g. `art.xylo`. 102 | 103 | Then, generate an image from that code: 104 | 105 | ```sh 106 | xylo generate art.xylo --width 800 --height 800 107 | ``` 108 | 109 | If your code is valid, you should see an image output to `art.png`. 110 | 111 | 112 | ## Community and Support 113 | 114 | Join our community and stay connected! You can reach out to us through the following platforms: 115 | 116 | - **Discord**: [Join our Discord server](https://discord.gg/xUQXFQ5DyZ) to share your art, discuss ideas, and collaborate on the the project. 117 | - **Telegram**: [Join our Telegram group](https://t.me/xylolang) for real-time updates and discussions. 118 | - **Reddit**: [Visit our subreddit](https://www.reddit.com/r/xylolang) to participate in discussions, share your artwork, and connect with other community members. 119 | - **Donations**: If you appreciate our work and would like to support us, consider donating through [Liberapay](https://liberapay.com/xylo) or [Giveth](https://giveth.io/project/xylo-programming-language). Your contributions help us continue improving the project! 120 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "std")] 2 | use { 3 | clap::{Parser, Subcommand}, 4 | sha2::{Digest, Sha256}, 5 | std::path::PathBuf, 6 | std::time::SystemTime, 7 | xylo_lang::{format_file, generate_file, minify_file, Config, Result}, 8 | }; 9 | 10 | #[cfg(feature = "std")] 11 | #[derive(Parser)] 12 | #[command(version, about, long_about = None)] 13 | struct Cli { 14 | #[command(subcommand)] 15 | command: Option, 16 | } 17 | 18 | #[cfg(feature = "std")] 19 | #[derive(Subcommand)] 20 | enum Commands { 21 | Generate { 22 | source: PathBuf, 23 | dest: Option, 24 | #[arg(long)] 25 | width: Option, 26 | #[arg(long)] 27 | height: Option, 28 | #[arg(long)] 29 | max_depth: Option, 30 | #[arg(short, long)] 31 | count: Option, 32 | #[arg(short, long)] 33 | seed: Option, 34 | }, 35 | Minify { 36 | source: PathBuf, 37 | dest: Option, 38 | }, 39 | Format { 40 | source: PathBuf, 41 | dest: Option, 42 | }, 43 | } 44 | 45 | #[cfg(feature = "std")] 46 | fn main() { 47 | match run_cli() { 48 | Ok(()) => (), 49 | Err(e) => eprintln!("{}", e.to_string()), 50 | } 51 | } 52 | 53 | #[cfg(feature = "std")] 54 | fn run_cli() -> Result<()> { 55 | let cli = Cli::parse(); 56 | 57 | match cli.command { 58 | Some(Commands::Generate { 59 | source, 60 | dest, 61 | width, 62 | height, 63 | max_depth, 64 | count, 65 | seed, 66 | }) => { 67 | let dest = match dest { 68 | Some(dest) => dest, 69 | None => format!( 70 | "{}.png", 71 | source 72 | .file_name() 73 | .unwrap() 74 | .to_str() 75 | .unwrap() 76 | .split(".") 77 | .next() 78 | .unwrap() 79 | ) 80 | .into(), 81 | }; 82 | 83 | let width = width.unwrap_or(400); 84 | let height = height.unwrap_or(400); 85 | let max_depth = max_depth.unwrap_or(1500); 86 | let count = count.unwrap_or(1); 87 | 88 | let seed = match seed { 89 | Some(seed) => { 90 | let mut hasher = Sha256::default(); 91 | hasher.update(seed.as_bytes()); 92 | let mut seed = [0; 32]; 93 | hasher.finalize_into((&mut seed).into()); 94 | Some(seed) 95 | } 96 | None => None, 97 | }; 98 | 99 | let config = Config { 100 | dimensions: (width, height), 101 | max_depth, 102 | seed, 103 | }; 104 | 105 | for i in 0..count { 106 | let dest = if count == 1 { 107 | dest.clone() 108 | } else { 109 | dest.parent().unwrap().join(format!( 110 | "{}_{}.{}", 111 | dest.file_stem().unwrap().to_string_lossy(), 112 | i, 113 | dest.extension().unwrap().to_string_lossy() 114 | )) 115 | }; 116 | 117 | let now = SystemTime::now(); 118 | generate_file(&source, &dest, config)?; 119 | 120 | println!( 121 | "Output to {:?} in {:?}", 122 | dest, 123 | SystemTime::now().duration_since(now).unwrap() 124 | ); 125 | } 126 | } 127 | Some(Commands::Minify { source, dest }) => { 128 | let dest = dest.unwrap_or(source.clone()); 129 | let now = SystemTime::now(); 130 | minify_file(source, &dest)?; 131 | println!( 132 | "Output to {:?} in {:?}", 133 | dest, 134 | SystemTime::now().duration_since(now).unwrap() 135 | ); 136 | } 137 | Some(Commands::Format { source, dest }) => { 138 | let dest = dest.unwrap_or(source.clone()); 139 | let now = SystemTime::now(); 140 | format_file(source, &dest)?; 141 | println!( 142 | "Output to {:?} in {:?}", 143 | dest, 144 | SystemTime::now().duration_since(now).unwrap() 145 | ); 146 | } 147 | None => (), 148 | } 149 | 150 | Ok(()) 151 | } 152 | 153 | #[cfg(feature = "no-std")] 154 | fn main() {} 155 | -------------------------------------------------------------------------------- /src/functions/func.rs: -------------------------------------------------------------------------------- 1 | use crate::builtin_function; 2 | use crate::error::{Error, Result}; 3 | use crate::interpreter::{Data, Value}; 4 | 5 | use rand_chacha::ChaCha8Rng; 6 | 7 | builtin_function!(pipe => { 8 | [arg, Value::Function(name, argc, pre_args)] => { 9 | let mut pre_args = pre_args.clone(); 10 | pre_args.push(arg.clone()); 11 | pre_args.reverse(); 12 | Value::Function(name.clone(), argc - 1, pre_args) 13 | }, 14 | }); 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | use super::*; 19 | 20 | #[cfg(feature = "no-std")] 21 | use alloc::vec; 22 | 23 | use rand::SeedableRng; 24 | 25 | #[test] 26 | fn test_pipe_basic() { 27 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 28 | let data = Data::default(); 29 | 30 | // Create a simple function that adds 1 31 | let add_one = Value::Function( 32 | "add_one".into(), 33 | 1, 34 | vec![Value::Integer(1)], // Pre-loaded argument 35 | ); 36 | 37 | // Pipe 5 through add_one 38 | let result = pipe(&mut rng, &data, &[Value::Integer(5), add_one.clone()]).unwrap(); 39 | 40 | // Should return a new function with arity reduced by 1 41 | if let Value::Function(name, argc, pre_args) = result { 42 | assert_eq!(name, "add_one"); 43 | assert_eq!(argc, 0); // Original arity was 1, now 0 after piping 44 | assert_eq!(pre_args, vec![Value::Integer(5), Value::Integer(1)]); 45 | } else { 46 | panic!("pipe did not return a function"); 47 | } 48 | } 49 | 50 | #[test] 51 | fn test_pipe_multiple_args() { 52 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 53 | let data = Data::default(); 54 | 55 | // Create a function that adds two numbers (arity 2) 56 | let add = Value::Function( 57 | "add".into(), 58 | 2, 59 | vec![], // No pre-loaded arguments 60 | ); 61 | 62 | // Pipe 5 through add (partially applied) 63 | let result = pipe(&mut rng, &data, &[Value::Integer(5), add.clone()]).unwrap(); 64 | 65 | // Should return a new function with arity reduced by 1 66 | if let Value::Function(name, argc, pre_args) = result { 67 | assert_eq!(name, "add"); 68 | assert_eq!(argc, 1); // Original arity was 2, now 1 after piping 69 | assert_eq!(pre_args, vec![Value::Integer(5)]); 70 | } else { 71 | panic!("pipe did not return a function"); 72 | } 73 | } 74 | 75 | #[test] 76 | fn test_pipe_with_pre_args() { 77 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 78 | let data = Data::default(); 79 | 80 | // Create a function with pre-loaded arguments 81 | let func = Value::Function( 82 | "multiply".into(), 83 | 3, // Total arity 84 | vec![Value::Integer(2)], // One pre-loaded argument 85 | ); 86 | 87 | // Pipe 3 through the function 88 | let result = pipe(&mut rng, &data, &[Value::Integer(3), func.clone()]).unwrap(); 89 | 90 | // Should return a new function with arity reduced by 1 91 | if let Value::Function(name, argc, pre_args) = result { 92 | assert_eq!(name, "multiply"); 93 | assert_eq!(argc, 2); // Original arity was 3, now 2 after piping 94 | assert_eq!(pre_args, vec![Value::Integer(3), Value::Integer(2)]); 95 | } else { 96 | panic!("pipe did not return a function"); 97 | } 98 | } 99 | 100 | #[test] 101 | fn test_pipe_invalid_args() { 102 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 103 | let data = Data::default(); 104 | 105 | // Test with non-function as second argument 106 | assert!(pipe(&mut rng, &data, &[Value::Integer(1), Value::Integer(2)]).is_err()); 107 | 108 | // Test with wrong number of arguments 109 | assert!(pipe(&mut rng, &data, &[Value::Integer(1)]).is_err()); 110 | 111 | assert!(pipe( 112 | &mut rng, 113 | &data, 114 | &[ 115 | Value::Integer(1), 116 | Value::Function("test".into(), 1, vec![]), 117 | Value::Integer(3) 118 | ] 119 | ) 120 | .is_err()); 121 | } 122 | 123 | #[test] 124 | fn test_pipe_chaining() { 125 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 126 | let data = Data::default(); 127 | 128 | // Create a simple function that adds two numbers 129 | let add = Value::Function( 130 | "add".into(), 131 | 2, 132 | vec![], // No pre-loaded arguments 133 | ); 134 | 135 | // Pipe 5 through add (partial application) 136 | let partial = pipe(&mut rng, &data, &[Value::Integer(5), add.clone()]).unwrap(); 137 | 138 | // Pipe 10 through the partially applied function 139 | let result = pipe(&mut rng, &data, &[Value::Integer(10), partial]).unwrap(); 140 | 141 | // Should now have all arguments needed 142 | if let Value::Function(name, argc, pre_args) = result { 143 | assert_eq!(name, "add"); 144 | assert_eq!(argc, 0); // All arguments provided 145 | assert_eq!(pre_args, vec![Value::Integer(10), Value::Integer(5)]); 146 | } else { 147 | panic!("pipe did not return a function"); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/functions/character.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "no-std")] 2 | use alloc::string::ToString; 3 | 4 | use crate::builtin_function; 5 | use crate::error::{Error, Result}; 6 | use crate::interpreter::{Data, Value}; 7 | 8 | use rand_chacha::ChaCha8Rng; 9 | 10 | builtin_function!(is_control => { 11 | [Value::Char(c)] => { 12 | Value::Boolean(c.is_ascii_control()) 13 | } 14 | }); 15 | 16 | builtin_function!(is_uppercase => { 17 | [Value::Char(c)] => { 18 | Value::Boolean(c.is_ascii_uppercase()) 19 | } 20 | }); 21 | 22 | builtin_function!(is_lowercase => { 23 | [Value::Char(c)] => { 24 | Value::Boolean(c.is_ascii_lowercase()) 25 | } 26 | }); 27 | 28 | builtin_function!(is_alpha => { 29 | [Value::Char(c)] => { 30 | Value::Boolean(c.is_ascii_alphabetic()) 31 | } 32 | }); 33 | 34 | builtin_function!(is_alphanumeric => { 35 | [Value::Char(c)] => { 36 | Value::Boolean(c.is_ascii_alphanumeric()) 37 | } 38 | }); 39 | 40 | builtin_function!(is_digit => { 41 | [Value::Char(c)] => { 42 | Value::Boolean(c.is_ascii_digit()) 43 | } 44 | }); 45 | 46 | // builtin_function!(is_oct_digit => { 47 | // [Value::Char(c)] => { 48 | // Value::Boolean(c.is_ascii_octdigit()) 49 | // } 50 | // }); 51 | 52 | builtin_function!(is_hex_digit => { 53 | [Value::Char(c)] => { 54 | Value::Boolean(c.is_ascii_hexdigit()) 55 | } 56 | }); 57 | 58 | builtin_function!(is_numeric => { 59 | [Value::Char(c)] => { 60 | Value::Boolean(c.is_numeric()) 61 | } 62 | }); 63 | 64 | builtin_function!(is_punctuation => { 65 | [Value::Char(c)] => { 66 | Value::Boolean(c.is_ascii_punctuation()) 67 | } 68 | }); 69 | 70 | builtin_function!(is_whitespace => { 71 | [Value::Char(c)] => { 72 | Value::Boolean(c.is_ascii_whitespace()) 73 | } 74 | }); 75 | 76 | builtin_function!(is_ascii => { 77 | [Value::Char(c)] => { 78 | Value::Boolean(c.is_ascii()) 79 | } 80 | }); 81 | 82 | builtin_function!(to_uppercase => { 83 | [Value::Char(c)] => { 84 | Value::Char(c.to_ascii_uppercase()) 85 | } 86 | }); 87 | 88 | builtin_function!(to_lowercase => { 89 | [Value::Char(c)] => { 90 | Value::Char(c.to_ascii_lowercase()) 91 | } 92 | }); 93 | 94 | builtin_function!(digit_to_int => { 95 | [Value::Char(c)] => { 96 | match c.to_digit(10) { 97 | Some(n) => Value::Integer(n as i32), 98 | None => return Err(Error::NotDigit("digit_to_int".into())) 99 | } 100 | } 101 | }); 102 | 103 | builtin_function!(int_to_digit => { 104 | [Value::Integer(n)] => { 105 | match char::from_digit(*n as u32, 10) { 106 | Some(c) => Value::Char(c), 107 | None => return Err(Error::NotDigit("int_to_digit".into())) 108 | } 109 | } 110 | }); 111 | 112 | builtin_function!(words => { 113 | [Value::String(s)] => { 114 | Value::List(s.split_whitespace().map(|word| Value::String(word.into())).collect()) 115 | } 116 | }); 117 | 118 | builtin_function!(lines => { 119 | [Value::String(s)] => { 120 | Value::List(s.lines().map(|line| Value::String(line.into())).collect()) 121 | } 122 | }); 123 | 124 | builtin_function!(string => { 125 | [Value::Integer(n)] => Value::String(n.to_string()), 126 | [Value::Float(n)] => Value::String(n.to_string()), 127 | [Value::Complex(n)] => Value::String(n.to_string()), 128 | [Value::Char(c)] => Value::String(c.to_string()), 129 | }); 130 | 131 | #[cfg(test)] 132 | mod tests { 133 | use super::*; 134 | 135 | #[cfg(feature = "no-std")] 136 | use alloc::vec; 137 | 138 | use num::Complex; 139 | use rand::SeedableRng; 140 | 141 | #[test] 142 | fn test_char_predicates() { 143 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 144 | let data = Data::default(); 145 | 146 | // is_control 147 | assert_eq!( 148 | is_control(&mut rng, &data, &[Value::Char('\n')]).ok(), 149 | Some(Value::Boolean(true)) 150 | ); 151 | assert_eq!( 152 | is_control(&mut rng, &data, &[Value::Char('a')]).ok(), 153 | Some(Value::Boolean(false)) 154 | ); 155 | 156 | // is_uppercase 157 | assert_eq!( 158 | is_uppercase(&mut rng, &data, &[Value::Char('A')]).ok(), 159 | Some(Value::Boolean(true)) 160 | ); 161 | assert_eq!( 162 | is_uppercase(&mut rng, &data, &[Value::Char('a')]).ok(), 163 | Some(Value::Boolean(false)) 164 | ); 165 | 166 | // is_lowercase 167 | assert_eq!( 168 | is_lowercase(&mut rng, &data, &[Value::Char('a')]).ok(), 169 | Some(Value::Boolean(true)) 170 | ); 171 | assert_eq!( 172 | is_lowercase(&mut rng, &data, &[Value::Char('A')]).ok(), 173 | Some(Value::Boolean(false)) 174 | ); 175 | 176 | // is_alpha 177 | assert_eq!( 178 | is_alpha(&mut rng, &data, &[Value::Char('a')]).ok(), 179 | Some(Value::Boolean(true)) 180 | ); 181 | assert_eq!( 182 | is_alpha(&mut rng, &data, &[Value::Char('1')]).ok(), 183 | Some(Value::Boolean(false)) 184 | ); 185 | 186 | // is_alphanumeric 187 | assert_eq!( 188 | is_alphanumeric(&mut rng, &data, &[Value::Char('a')]).ok(), 189 | Some(Value::Boolean(true)) 190 | ); 191 | assert_eq!( 192 | is_alphanumeric(&mut rng, &data, &[Value::Char('1')]).ok(), 193 | Some(Value::Boolean(true)) 194 | ); 195 | assert_eq!( 196 | is_alphanumeric(&mut rng, &data, &[Value::Char('!')]).ok(), 197 | Some(Value::Boolean(false)) 198 | ); 199 | 200 | // is_digit 201 | assert_eq!( 202 | is_digit(&mut rng, &data, &[Value::Char('9')]).ok(), 203 | Some(Value::Boolean(true)) 204 | ); 205 | assert_eq!( 206 | is_digit(&mut rng, &data, &[Value::Char('a')]).ok(), 207 | Some(Value::Boolean(false)) 208 | ); 209 | 210 | // is_hex_digit 211 | assert_eq!( 212 | is_hex_digit(&mut rng, &data, &[Value::Char('a')]).ok(), 213 | Some(Value::Boolean(true)) 214 | ); 215 | assert_eq!( 216 | is_hex_digit(&mut rng, &data, &[Value::Char('F')]).ok(), 217 | Some(Value::Boolean(true)) 218 | ); 219 | assert_eq!( 220 | is_hex_digit(&mut rng, &data, &[Value::Char('g')]).ok(), 221 | Some(Value::Boolean(false)) 222 | ); 223 | 224 | // is_numeric 225 | assert_eq!( 226 | is_numeric(&mut rng, &data, &[Value::Char('Ⅷ')]).ok(), 227 | Some(Value::Boolean(true)) 228 | ); 229 | assert_eq!( 230 | is_numeric(&mut rng, &data, &[Value::Char('a')]).ok(), 231 | Some(Value::Boolean(false)) 232 | ); 233 | 234 | // is_punctuation 235 | assert_eq!( 236 | is_punctuation(&mut rng, &data, &[Value::Char('!')]).ok(), 237 | Some(Value::Boolean(true)) 238 | ); 239 | assert_eq!( 240 | is_punctuation(&mut rng, &data, &[Value::Char('a')]).ok(), 241 | Some(Value::Boolean(false)) 242 | ); 243 | 244 | // is_whitespace 245 | assert_eq!( 246 | is_whitespace(&mut rng, &data, &[Value::Char(' ')]).ok(), 247 | Some(Value::Boolean(true)) 248 | ); 249 | assert_eq!( 250 | is_whitespace(&mut rng, &data, &[Value::Char('a')]).ok(), 251 | Some(Value::Boolean(false)) 252 | ); 253 | 254 | // is_ascii 255 | assert_eq!( 256 | is_ascii(&mut rng, &data, &[Value::Char('a')]).ok(), 257 | Some(Value::Boolean(true)) 258 | ); 259 | assert_eq!( 260 | is_ascii(&mut rng, &data, &[Value::Char('ñ')]).ok(), 261 | Some(Value::Boolean(false)) 262 | ); 263 | } 264 | 265 | #[test] 266 | fn test_char_transforms() { 267 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 268 | let data = Data::default(); 269 | 270 | // to_uppercase 271 | assert_eq!( 272 | to_uppercase(&mut rng, &data, &[Value::Char('a')]).ok(), 273 | Some(Value::Char('A')) 274 | ); 275 | assert_eq!( 276 | to_uppercase(&mut rng, &data, &[Value::Char('A')]).ok(), 277 | Some(Value::Char('A')) 278 | ); 279 | 280 | // to_lowercase 281 | assert_eq!( 282 | to_lowercase(&mut rng, &data, &[Value::Char('A')]).ok(), 283 | Some(Value::Char('a')) 284 | ); 285 | assert_eq!( 286 | to_lowercase(&mut rng, &data, &[Value::Char('a')]).ok(), 287 | Some(Value::Char('a')) 288 | ); 289 | } 290 | 291 | #[test] 292 | fn test_digit_conversions() { 293 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 294 | let data = Data::default(); 295 | 296 | // digit_to_int 297 | assert_eq!( 298 | digit_to_int(&mut rng, &data, &[Value::Char('5')]).ok(), 299 | Some(Value::Integer(5)) 300 | ); 301 | assert_eq!( 302 | digit_to_int(&mut rng, &data, &[Value::Char('a')]).ok(), 303 | None 304 | ); 305 | 306 | // int_to_digit 307 | assert_eq!( 308 | int_to_digit(&mut rng, &data, &[Value::Integer(7)]).ok(), 309 | Some(Value::Char('7')) 310 | ); 311 | assert_eq!( 312 | int_to_digit(&mut rng, &data, &[Value::Integer(10)]).ok(), 313 | None 314 | ); 315 | } 316 | 317 | #[test] 318 | fn test_string_operations() { 319 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 320 | let data = Data::default(); 321 | 322 | // words 323 | assert_eq!( 324 | words(&mut rng, &data, &[Value::String("hello world".into())]).ok(), 325 | Some(Value::List(vec![ 326 | Value::String("hello".into()), 327 | Value::String("world".into()) 328 | ])) 329 | ); 330 | assert_eq!( 331 | words(&mut rng, &data, &[Value::String(" ".into())]).ok(), 332 | Some(Value::List(vec![])) 333 | ); 334 | 335 | // lines 336 | assert_eq!( 337 | lines(&mut rng, &data, &[Value::String("a\nb\nc".into())]).ok(), 338 | Some(Value::List(vec![ 339 | Value::String("a".into()), 340 | Value::String("b".into()), 341 | Value::String("c".into()) 342 | ])) 343 | ); 344 | assert_eq!( 345 | lines(&mut rng, &data, &[Value::String("".into())]).ok(), 346 | Some(Value::List(vec![])) 347 | ); 348 | 349 | // string conversion 350 | assert_eq!( 351 | string(&mut rng, &data, &[Value::Integer(42)]).ok(), 352 | Some(Value::String("42".into())) 353 | ); 354 | assert_eq!( 355 | string(&mut rng, &data, &[Value::Float(3.14)]).ok(), 356 | Some(Value::String("3.14".into())) 357 | ); 358 | assert_eq!( 359 | string(&mut rng, &data, &[Value::Char('x')]).ok(), 360 | Some(Value::String("x".into())) 361 | ); 362 | assert_eq!( 363 | string(&mut rng, &data, &[Value::Complex(Complex::new(1.0, 2.0))]).ok(), 364 | Some(Value::String("1+2i".into())) 365 | ); 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/functions/path.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "std")] 2 | use std::rc::Rc; 3 | 4 | #[cfg(feature = "no-std")] 5 | use alloc::{rc::Rc, vec}; 6 | 7 | use crate::builtin_function; 8 | use crate::error::{Error, Result}; 9 | use crate::interpreter::{Data, Value}; 10 | use crate::shape::{PathSegment, Shape}; 11 | use core::cell::RefCell; 12 | 13 | use rand_chacha::ChaCha8Rng; 14 | 15 | builtin_function!(move_to => { 16 | [x, y] => { 17 | let x = match x { 18 | Value::Integer(x) => *x as f32, 19 | Value::Float(x) => *x, 20 | _ => return Err(Error::InvalidArgument("move_to".into())), 21 | }; 22 | 23 | let y = match y { 24 | Value::Integer(y) => *y as f32, 25 | Value::Float(y) => *y, 26 | _ => return Err(Error::InvalidArgument("move_to".into())), 27 | }; 28 | 29 | let segments = vec![PathSegment::MoveTo(x, y)]; 30 | let shape = Shape::path(segments); 31 | Value::Shape(Rc::new(RefCell::new(shape))) 32 | } 33 | }); 34 | 35 | builtin_function!(line_to => { 36 | [x, y] => { 37 | let x = match x { 38 | Value::Integer(x) => *x as f32, 39 | Value::Float(x) => *x, 40 | _ => return Err(Error::InvalidArgument("line_to".into())), 41 | }; 42 | 43 | let y = match y { 44 | Value::Integer(y) => *y as f32, 45 | Value::Float(y) => *y, 46 | _ => return Err(Error::InvalidArgument("line_to".into())), 47 | }; 48 | 49 | let segments = vec![PathSegment::LineTo(x, y)]; 50 | let shape = Shape::path(segments); 51 | Value::Shape(Rc::new(RefCell::new(shape))) 52 | } 53 | }); 54 | 55 | builtin_function!(quad_to => { 56 | [x1, y1, x, y] => { 57 | let x1 = match x1 { 58 | Value::Integer(x1) => *x1 as f32, 59 | Value::Float(x1) => *x1, 60 | _ => return Err(Error::InvalidArgument("quad_to".into())), 61 | }; 62 | 63 | let y1 = match y1 { 64 | Value::Integer(y1) => *y1 as f32, 65 | Value::Float(y1) => *y1, 66 | _ => return Err(Error::InvalidArgument("quad_to".into())), 67 | }; 68 | 69 | let x = match x { 70 | Value::Integer(x) => *x as f32, 71 | Value::Float(x) => *x, 72 | _ => return Err(Error::InvalidArgument("quad_to".into())), 73 | }; 74 | 75 | let y = match y { 76 | Value::Integer(y) => *y as f32, 77 | Value::Float(y) => *y, 78 | _ => return Err(Error::InvalidArgument("quad_to".into())), 79 | }; 80 | 81 | let segments = vec![PathSegment::QuadTo(x1, y1, x, y)]; 82 | let shape = Shape::path(segments); 83 | Value::Shape(Rc::new(RefCell::new(shape))) 84 | } 85 | }); 86 | 87 | builtin_function!(cubic_to => { 88 | [x1, y1, x2, y2, x, y] => { 89 | let x1 = match x1 { 90 | Value::Integer(x1) => *x1 as f32, 91 | Value::Float(x1) => *x1, 92 | _ => return Err(Error::InvalidArgument("cubic_to".into())), 93 | }; 94 | 95 | let y1 = match y1 { 96 | Value::Integer(y1) => *y1 as f32, 97 | Value::Float(y1) => *y1, 98 | _ => return Err(Error::InvalidArgument("cubic_to".into())), 99 | }; 100 | 101 | let x2 = match x2 { 102 | Value::Integer(x2) => *x2 as f32, 103 | Value::Float(x2) => *x2, 104 | _ => return Err(Error::InvalidArgument("cubic_to".into())), 105 | }; 106 | 107 | let y2 = match y2 { 108 | Value::Integer(y2) => *y2 as f32, 109 | Value::Float(y2) => *y2, 110 | _ => return Err(Error::InvalidArgument("cubic_to".into())), 111 | }; 112 | 113 | let x = match x { 114 | Value::Integer(x) => *x as f32, 115 | Value::Float(x) => *x, 116 | _ => return Err(Error::InvalidArgument("cubic_to".into())), 117 | }; 118 | 119 | let y = match y { 120 | Value::Integer(y) => *y as f32, 121 | Value::Float(y) => *y, 122 | _ => return Err(Error::InvalidArgument("cubic_to".into())), 123 | }; 124 | 125 | let segments = vec![PathSegment::CubicTo(x1, y1, x2, y2, x, y)]; 126 | let shape = Shape::path(segments); 127 | Value::Shape(Rc::new(RefCell::new(shape))) 128 | } 129 | }); 130 | 131 | builtin_function!(close => { 132 | [] => { 133 | let segments = vec![PathSegment::Close]; 134 | let shape = Shape::path(segments); 135 | Value::Shape(Rc::new(RefCell::new(shape))) 136 | } 137 | }); 138 | 139 | #[cfg(test)] 140 | mod tests { 141 | use super::*; 142 | use rand::SeedableRng; 143 | 144 | #[test] 145 | fn test_move_to() { 146 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 147 | let data = Data::default(); 148 | 149 | // Test with integer coordinates 150 | let result_int = 151 | move_to(&mut rng, &data, &[Value::Integer(10), Value::Integer(20)]).unwrap(); 152 | 153 | // Test with float coordinates 154 | let result_float = 155 | move_to(&mut rng, &data, &[Value::Float(10.5), Value::Float(20.5)]).unwrap(); 156 | 157 | // Test with mixed coordinates 158 | let result_mixed = 159 | move_to(&mut rng, &data, &[Value::Integer(10), Value::Float(20.5)]).unwrap(); 160 | 161 | // Verify all results are shapes with MoveTo segments 162 | for result in [result_int, result_float, result_mixed] { 163 | if let Value::Shape(shape) = result { 164 | let shape = shape.borrow(); 165 | if let Shape::Path { segments, .. } = &*shape { 166 | assert_eq!(segments.len(), 1); 167 | assert!(matches!(segments[0], PathSegment::MoveTo(_, _))); 168 | } else { 169 | panic!("Expected Path shape"); 170 | } 171 | } else { 172 | panic!("Expected Shape value"); 173 | } 174 | } 175 | 176 | // Test invalid arguments 177 | assert!(move_to( 178 | &mut rng, 179 | &data, 180 | &[Value::String("10".into()), Value::Integer(20)] 181 | ) 182 | .is_err()); 183 | 184 | assert!(move_to(&mut rng, &data, &[Value::Integer(10)]).is_err()); 185 | } 186 | 187 | #[test] 188 | fn test_line_to() { 189 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 190 | let data = Data::default(); 191 | 192 | // Test with integer coordinates 193 | let result_int = 194 | line_to(&mut rng, &data, &[Value::Integer(30), Value::Integer(40)]).unwrap(); 195 | 196 | // Test with float coordinates 197 | let result_float = 198 | line_to(&mut rng, &data, &[Value::Float(30.5), Value::Float(40.5)]).unwrap(); 199 | 200 | // Verify all results are shapes with LineTo segments 201 | for result in [result_int, result_float] { 202 | if let Value::Shape(shape) = result { 203 | let shape = shape.borrow(); 204 | if let Shape::Path { segments, .. } = &*shape { 205 | assert_eq!(segments.len(), 1); 206 | assert!(matches!(segments[0], PathSegment::LineTo(_, _))); 207 | } else { 208 | panic!("Expected Path shape"); 209 | } 210 | } else { 211 | panic!("Expected Shape value"); 212 | } 213 | } 214 | } 215 | 216 | #[test] 217 | fn test_quad_to() { 218 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 219 | let data = Data::default(); 220 | 221 | // Test with integer coordinates 222 | let result_int = quad_to( 223 | &mut rng, 224 | &data, 225 | &[ 226 | Value::Integer(10), 227 | Value::Integer(15), 228 | Value::Integer(20), 229 | Value::Integer(25), 230 | ], 231 | ) 232 | .unwrap(); 233 | 234 | // Test with float coordinates 235 | let result_float = quad_to( 236 | &mut rng, 237 | &data, 238 | &[ 239 | Value::Float(10.5), 240 | Value::Float(15.5), 241 | Value::Float(20.5), 242 | Value::Float(25.5), 243 | ], 244 | ) 245 | .unwrap(); 246 | 247 | // Verify all results are shapes with QuadTo segments 248 | for result in [result_int, result_float] { 249 | if let Value::Shape(shape) = result { 250 | let shape = shape.borrow(); 251 | if let Shape::Path { segments, .. } = &*shape { 252 | assert_eq!(segments.len(), 1); 253 | assert!(matches!(segments[0], PathSegment::QuadTo(_, _, _, _))); 254 | } else { 255 | panic!("Expected Path shape"); 256 | } 257 | } else { 258 | panic!("Expected Shape value"); 259 | } 260 | } 261 | 262 | // Test with wrong number of arguments 263 | assert!(quad_to( 264 | &mut rng, 265 | &data, 266 | &[Value::Integer(10), Value::Integer(15), Value::Integer(20)] 267 | ) 268 | .is_err()); 269 | } 270 | 271 | #[test] 272 | fn test_cubic_to() { 273 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 274 | let data = Data::default(); 275 | 276 | // Test with integer coordinates 277 | let result_int = cubic_to( 278 | &mut rng, 279 | &data, 280 | &[ 281 | Value::Integer(10), 282 | Value::Integer(15), 283 | Value::Integer(20), 284 | Value::Integer(25), 285 | Value::Integer(30), 286 | Value::Integer(35), 287 | ], 288 | ) 289 | .unwrap(); 290 | 291 | // Test with float coordinates 292 | let result_float = cubic_to( 293 | &mut rng, 294 | &data, 295 | &[ 296 | Value::Float(10.5), 297 | Value::Float(15.5), 298 | Value::Float(20.5), 299 | Value::Float(25.5), 300 | Value::Float(30.5), 301 | Value::Float(35.5), 302 | ], 303 | ) 304 | .unwrap(); 305 | 306 | // Verify all results are shapes with CubicTo segments 307 | for result in [result_int, result_float] { 308 | if let Value::Shape(shape) = result { 309 | let shape = shape.borrow(); 310 | if let Shape::Path { segments, .. } = &*shape { 311 | assert_eq!(segments.len(), 1); 312 | assert!(matches!( 313 | segments[0], 314 | PathSegment::CubicTo(_, _, _, _, _, _) 315 | )); 316 | } else { 317 | panic!("Expected Path shape"); 318 | } 319 | } else { 320 | panic!("Expected Shape value"); 321 | } 322 | } 323 | 324 | // Test with wrong number of arguments 325 | assert!(cubic_to( 326 | &mut rng, 327 | &data, 328 | &[ 329 | Value::Integer(10), 330 | Value::Integer(15), 331 | Value::Integer(20), 332 | Value::Integer(25), 333 | Value::Integer(30) 334 | ] 335 | ) 336 | .is_err()); 337 | } 338 | 339 | #[test] 340 | fn test_close() { 341 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 342 | let data = Data::default(); 343 | 344 | // Test close path 345 | let result = close(&mut rng, &data, &[]).unwrap(); 346 | 347 | if let Value::Shape(shape) = result { 348 | let shape = shape.borrow(); 349 | if let Shape::Path { segments, .. } = &*shape { 350 | assert_eq!(segments.len(), 1); 351 | assert!(matches!(segments[0], PathSegment::Close)); 352 | } else { 353 | panic!("Expected Path shape"); 354 | } 355 | } else { 356 | panic!("Expected Shape value"); 357 | } 358 | 359 | // Test with arguments (should error) 360 | assert!(close(&mut rng, &data, &[Value::Integer(10)]).is_err()); 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/minify.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "no-std")] 2 | use alloc::{ 3 | format, 4 | string::{String, ToString}, 5 | vec, 6 | vec::Vec, 7 | }; 8 | 9 | use crate::error::Result; 10 | use crate::parser::{parse, Definition, Literal, Pattern, Token}; 11 | 12 | fn block_to_string(block: &[Token]) -> String { 13 | let mut index = 0; 14 | let mut stack = vec![Vec::new()]; 15 | let mut depth = 0; 16 | let mut lets = vec![false]; 17 | let mut in_if = false; 18 | 19 | while index < block.len() { 20 | match &block[index] { 21 | Token::Literal(literal) => { 22 | stack[depth].push(literal.to_string()); 23 | index += 1; 24 | } 25 | Token::List(size) => { 26 | let mut elems = Vec::with_capacity(*size); 27 | for _ in 0..*size { 28 | elems.push(stack[depth].pop().unwrap()); 29 | } 30 | elems.reverse(); 31 | 32 | stack[depth].push(format!("[{}]", elems.join(","))); 33 | index += 1; 34 | } 35 | Token::UnaryOperator(op) => { 36 | let a = stack[depth].pop().unwrap(); 37 | stack[depth].push(format!("{}({})", op.as_str(), a)); 38 | index += 1; 39 | } 40 | Token::BinaryOperator(op) => { 41 | let b = stack[depth].pop().unwrap(); 42 | let a = stack[depth].pop().unwrap(); 43 | stack[depth].push(format!("({}{}{})", a, op.as_str(), b)); 44 | index += 1; 45 | } 46 | Token::Call(name, argc) => { 47 | let mut args = Vec::new(); 48 | for _ in 0..*argc { 49 | args.push(stack[depth].pop().unwrap()); 50 | } 51 | args.reverse(); 52 | 53 | if args.is_empty() { 54 | stack[depth].push(name.to_string()); 55 | } else { 56 | stack[depth].push(format!("({}{})", name, format!(" {}", args.join(" ")))); 57 | } 58 | index += 1; 59 | } 60 | Token::Let(name, params, skip) => { 61 | if !lets[depth] { 62 | depth += 1; 63 | stack.push(Vec::new()); 64 | lets.push(true); 65 | } 66 | 67 | stack[depth].push(format!( 68 | "{}{}={}", 69 | name, 70 | if params.is_empty() { 71 | String::new() 72 | } else { 73 | format!(" {}", params.join(" ")) 74 | }, 75 | block_to_string(&block[index + 1..index + skip]) 76 | )); 77 | lets[depth] = true; 78 | index += skip + 1; 79 | } 80 | Token::Pop => { 81 | if lets[depth] { 82 | let defs = stack[depth][..stack[depth].len() - 1].join(";"); 83 | let block = stack[depth].last().unwrap().clone(); 84 | stack[depth - 1].push(format!("let {}->{}", defs, block)); 85 | 86 | stack.pop().unwrap(); 87 | lets.pop().unwrap(); 88 | depth -= 1; 89 | } 90 | index += 1; 91 | } 92 | Token::If(skip) => { 93 | stack[depth].push(block_to_string(&block[index + 1..index + skip])); 94 | in_if = true; 95 | index += skip; 96 | } 97 | Token::Jump(skip) => { 98 | if in_if { 99 | let else_block = block_to_string(&block[index + 1..index + skip + 1]); 100 | let if_block = stack[depth].pop().unwrap(); 101 | let condition = stack[depth].pop().unwrap(); 102 | stack[depth].push(format!( 103 | "if {}->{};else->{}", 104 | condition, if_block, else_block 105 | )); 106 | index += skip + 1; 107 | in_if = false; 108 | } else { 109 | index += 1; 110 | } 111 | } 112 | Token::Match(patterns) => { 113 | let a = stack[depth].pop().unwrap(); 114 | index += 1; 115 | 116 | let mut pats = Vec::new(); 117 | for (pattern, has_guard, skip) in patterns { 118 | let guard = if *has_guard { 119 | format!(" if {}", stack[depth].pop().unwrap()) 120 | } else { 121 | String::new() 122 | }; 123 | 124 | match pattern { 125 | Pattern::Matches(matches) => { 126 | let matches = matches 127 | .iter() 128 | .map(Literal::to_string) 129 | .collect::>() 130 | .join(","); 131 | pats.push(format!( 132 | "{}{}->{}", 133 | matches, 134 | guard, 135 | block_to_string(&block[index..index + skip]) 136 | )); 137 | } 138 | Pattern::Wildcard => { 139 | pats.push(format!( 140 | "_{}->{}", 141 | guard, 142 | block_to_string(&block[index..index + skip]) 143 | )); 144 | } 145 | } 146 | index += skip; 147 | } 148 | 149 | stack[depth].push(format!("match {}->{}", a, pats.join(";"))); 150 | } 151 | Token::ForStart(var) => { 152 | let iter = stack[depth].pop().unwrap(); 153 | depth += 1; 154 | stack.push(vec![format!("for {} in {}->", var, iter)]); 155 | lets.push(false); 156 | index += 1; 157 | } 158 | Token::ForEnd => { 159 | let block = stack[depth].pop().unwrap(); 160 | let def = stack[depth].pop().unwrap(); 161 | stack[depth - 1].push([def, block].concat()); 162 | 163 | stack.pop().unwrap(); 164 | lets.pop().unwrap(); 165 | depth -= 1; 166 | index += 1; 167 | } 168 | Token::LoopStart => { 169 | let count = stack[depth].pop().unwrap(); 170 | depth += 1; 171 | stack.push(vec![format!("loop {}->", count)]); 172 | lets.push(false); 173 | index += 1; 174 | } 175 | Token::LoopEnd => { 176 | let block = stack[depth].pop().unwrap(); 177 | let def = stack[depth].pop().unwrap(); 178 | stack[depth - 1].push([def, block].concat()); 179 | 180 | stack.pop().unwrap(); 181 | lets.pop().unwrap(); 182 | depth -= 1; 183 | index += 1; 184 | } 185 | Token::Return(_) => index += 1, 186 | } 187 | } 188 | 189 | assert!(stack.len() == 1); 190 | stack[0].pop().unwrap() 191 | } 192 | 193 | fn definition_to_string(definition: &Definition) -> String { 194 | if definition.weight == 1.0 { 195 | format!( 196 | "{}{}={}", 197 | definition.name, 198 | if definition.params.is_empty() { 199 | String::new() 200 | } else { 201 | format!(" {}", definition.params.join(" ")) 202 | }, 203 | block_to_string(&definition.block) 204 | ) 205 | } else { 206 | format!( 207 | "{}@{}{}={}", 208 | definition.name, 209 | definition.weight, 210 | if definition.params.is_empty() { 211 | String::new() 212 | } else { 213 | format!(" {}", definition.params.join(" ")) 214 | }, 215 | block_to_string(&definition.block) 216 | ) 217 | } 218 | } 219 | 220 | pub fn minify(input: &str) -> Result { 221 | let tree = parse(input)?; 222 | let output = tree 223 | .iter() 224 | .map(|definition| definition_to_string(definition)) 225 | .collect::>() 226 | .join("\n"); 227 | Ok(output) 228 | } 229 | 230 | #[cfg(test)] 231 | mod tests { 232 | use super::*; 233 | use crate::interpreter::execute; 234 | use crate::out::Config; 235 | 236 | fn can_execute(output: &str) -> bool { 237 | let config = Config { 238 | seed: Some([0; 32]), 239 | ..Config::default() 240 | }; 241 | parse(output).and_then(|tree| execute(tree, config)).is_ok() 242 | } 243 | 244 | #[test] 245 | fn test_binary_operation() { 246 | let output = minify( 247 | " 248 | root = square 249 | 250 | square = ss (3 + 5) SQUARE 251 | ", 252 | ); 253 | assert!(output.is_ok()); 254 | assert_eq!( 255 | output.as_ref().unwrap(), 256 | "\ 257 | root=square 258 | square=(ss (3+5) SQUARE)\ 259 | " 260 | ); 261 | assert!(can_execute(output.as_ref().unwrap())); 262 | } 263 | 264 | #[test] 265 | fn test_let_statement() { 266 | let output = minify( 267 | " 268 | root = square 269 | 270 | square = 271 | let n1 = 3 272 | n2 = 5 273 | -> 274 | let n3 = 1 275 | tx (n1 + n2 + n3) SQUARE 276 | ", 277 | ); 278 | assert!(output.is_ok()); 279 | assert_eq!( 280 | output.as_ref().unwrap(), 281 | "\ 282 | root=square 283 | square=let n1=3;n2=5;n3=1->(tx ((n1+n2)+n3) SQUARE)\ 284 | " 285 | ); 286 | assert!(can_execute(output.as_ref().unwrap())); 287 | } 288 | 289 | #[test] 290 | fn test_if_statement() { 291 | let output = minify( 292 | " 293 | root = shape true 294 | 295 | shape is_square = 296 | if is_square 297 | SQUARE 298 | else 299 | CIRCLE 300 | ", 301 | ); 302 | assert!(output.is_ok()); 303 | assert_eq!( 304 | output.as_ref().unwrap(), 305 | "\ 306 | root=(shape true) 307 | shape is_square=if is_square->SQUARE;else->CIRCLE\ 308 | " 309 | ); 310 | assert!(can_execute(output.as_ref().unwrap())); 311 | } 312 | 313 | #[test] 314 | fn test_match_statement() { 315 | let output = minify( 316 | " 317 | root = shape 1 318 | 319 | shape n = 320 | match n 321 | 1 -> SQUARE 322 | 2 if true -> CIRCLE 323 | 3 -> TRIANGLE 324 | _ -> EMPTY 325 | ", 326 | ); 327 | assert!(output.is_ok()); 328 | assert_eq!( 329 | output.as_ref().unwrap(), 330 | "\ 331 | root=(shape 1) 332 | shape n=match n->1->SQUARE;2 if true->CIRCLE;3->TRIANGLE;_->EMPTY\ 333 | " 334 | ); 335 | assert!(can_execute(output.as_ref().unwrap())); 336 | } 337 | 338 | #[test] 339 | fn test_for_statement() { 340 | let output = minify( 341 | " 342 | root = collect squares 343 | 344 | squares = 345 | for i in 0..3 346 | tx i SQUARE 347 | ", 348 | ); 349 | assert!(output.is_ok()); 350 | assert_eq!( 351 | output.as_ref().unwrap(), 352 | "\ 353 | root=(collect squares) 354 | squares=for i in (0..3)->(tx i SQUARE)\ 355 | " 356 | ); 357 | assert!(can_execute(output.as_ref().unwrap())); 358 | } 359 | 360 | #[test] 361 | fn test_loop_statement() { 362 | let output = minify( 363 | " 364 | root = collect squares 365 | 366 | squares = 367 | loop 3 368 | tx (rand * 10) SQUARE 369 | ", 370 | ); 371 | assert!(output.is_ok()); 372 | assert_eq!( 373 | output.as_ref().unwrap(), 374 | "\ 375 | root=(collect squares) 376 | squares=loop 3->(tx (rand*10) SQUARE)\ 377 | " 378 | ); 379 | assert!(can_execute(output.as_ref().unwrap())); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/format.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "no-std")] 2 | use alloc::{ 3 | format, 4 | string::{String, ToString}, 5 | vec, 6 | vec::Vec, 7 | }; 8 | 9 | use crate::error::Result; 10 | use crate::parser::{parse, Definition, Literal, Pattern, Token}; 11 | 12 | fn block_to_string(block: &[Token]) -> String { 13 | let mut index = 0; 14 | let mut stack = vec![Vec::new()]; 15 | let mut depth = 0; 16 | let mut lets = vec![false]; 17 | let mut in_if = false; 18 | 19 | while index < block.len() { 20 | match &block[index] { 21 | Token::Literal(literal) => { 22 | stack[depth].push(literal.to_string()); 23 | index += 1; 24 | } 25 | Token::List(size) => { 26 | let mut elems = Vec::with_capacity(*size); 27 | for _ in 0..*size { 28 | elems.push(stack[depth].pop().unwrap()); 29 | } 30 | elems.reverse(); 31 | 32 | stack[depth].push(format!("[{}]", elems.join(", "))); 33 | index += 1; 34 | } 35 | Token::UnaryOperator(op) => { 36 | let a = stack[depth].pop().unwrap(); 37 | stack[depth].push(format!("{}({})", op.as_str(), a)); 38 | index += 1; 39 | } 40 | Token::BinaryOperator(op) => { 41 | let b = stack[depth].pop().unwrap(); 42 | let a = stack[depth].pop().unwrap(); 43 | stack[depth].push(format!("({} {} {})", a, op.as_str(), b)); 44 | index += 1; 45 | } 46 | Token::Call(name, argc) => { 47 | let mut args = Vec::new(); 48 | for _ in 0..*argc { 49 | args.push(stack[depth].pop().unwrap()); 50 | } 51 | args.reverse(); 52 | 53 | if args.is_empty() { 54 | stack[depth].push(name.to_string()); 55 | } else { 56 | stack[depth].push(format!("({}{})", name, format!(" {}", args.join(" ")))); 57 | } 58 | index += 1; 59 | } 60 | Token::Let(name, params, skip) => { 61 | if !lets[depth] { 62 | depth += 1; 63 | stack.push(Vec::new()); 64 | lets.push(true); 65 | } 66 | 67 | stack[depth].push(format!( 68 | "{}{} = {}", 69 | name, 70 | if params.is_empty() { 71 | String::new() 72 | } else { 73 | format!(" {}", params.join(" ")) 74 | }, 75 | block_to_string(&block[index + 1..index + skip]) 76 | )); 77 | lets[depth] = true; 78 | index += skip + 1; 79 | } 80 | Token::Pop => { 81 | if lets[depth] { 82 | let defs = stack[depth][..stack[depth].len() - 1].join("\n\t\t"); 83 | let block = stack[depth].last().unwrap().clone(); 84 | stack[depth - 1].push(format!("let {}\n\t\t{}", defs, block)); 85 | 86 | stack.pop().unwrap(); 87 | lets.pop().unwrap(); 88 | depth -= 1; 89 | } 90 | index += 1; 91 | } 92 | Token::If(skip) => { 93 | stack[depth].push(block_to_string(&block[index + 1..index + skip])); 94 | in_if = true; 95 | index += skip; 96 | } 97 | Token::Jump(skip) => { 98 | if in_if { 99 | let else_block = block_to_string(&block[index + 1..index + skip + 1]); 100 | let if_block = stack[depth].pop().unwrap(); 101 | let condition = stack[depth].pop().unwrap(); 102 | stack[depth].push(format!( 103 | "if {}\n\t\t{}\n\telse\n\t\t{}", 104 | condition, if_block, else_block 105 | )); 106 | index += skip + 1; 107 | in_if = false; 108 | } else { 109 | index += 1; 110 | } 111 | } 112 | Token::Match(patterns) => { 113 | let a = stack[depth].pop().unwrap(); 114 | index += 1; 115 | 116 | let mut pats = Vec::new(); 117 | for (pattern, has_guard, skip) in patterns { 118 | let guard = if *has_guard { 119 | format!(" if {}", stack[depth].pop().unwrap()) 120 | } else { 121 | String::new() 122 | }; 123 | 124 | match pattern { 125 | Pattern::Matches(matches) => { 126 | let matches = matches 127 | .iter() 128 | .map(Literal::to_string) 129 | .collect::>() 130 | .join(","); 131 | pats.push(format!( 132 | "{}{}\n\t\t\t{}", 133 | matches, 134 | guard, 135 | block_to_string(&block[index..index + skip]) 136 | )); 137 | } 138 | Pattern::Wildcard => { 139 | pats.push(format!( 140 | "_{}\n\t\t\t{}", 141 | guard, 142 | block_to_string(&block[index..index + skip]) 143 | )); 144 | } 145 | } 146 | index += skip; 147 | } 148 | 149 | stack[depth].push(format!("match {}\n\t\t{}", a, pats.join("\n\t\t"))); 150 | } 151 | Token::ForStart(var) => { 152 | let iter = stack[depth].pop().unwrap(); 153 | depth += 1; 154 | stack.push(vec![format!("for {} in {}", var, iter)]); 155 | lets.push(false); 156 | index += 1; 157 | } 158 | Token::ForEnd => { 159 | let block = stack[depth].pop().unwrap(); 160 | let def = stack[depth].pop().unwrap(); 161 | stack[depth - 1].push([def, block].join("\n\t\t")); 162 | 163 | stack.pop().unwrap(); 164 | lets.pop().unwrap(); 165 | depth -= 1; 166 | index += 1; 167 | } 168 | Token::LoopStart => { 169 | let count = stack[depth].pop().unwrap(); 170 | depth += 1; 171 | stack.push(vec![format!("loop {}", count)]); 172 | lets.push(false); 173 | index += 1; 174 | } 175 | Token::LoopEnd => { 176 | let block = stack[depth].pop().unwrap(); 177 | let def = stack[depth].pop().unwrap(); 178 | stack[depth - 1].push([def, block].join("\n\t\t")); 179 | 180 | stack.pop().unwrap(); 181 | lets.pop().unwrap(); 182 | depth -= 1; 183 | index += 1; 184 | } 185 | Token::Return(_) => index += 1, 186 | } 187 | } 188 | 189 | assert!(stack.len() == 1); 190 | stack[0].pop().unwrap() 191 | } 192 | 193 | fn definition_to_string(definition: &Definition) -> String { 194 | if definition.weight == 1.0 { 195 | format!( 196 | "{}{} =\n\t{}", 197 | definition.name, 198 | if definition.params.is_empty() { 199 | String::new() 200 | } else { 201 | format!(" {}", definition.params.join(" ")) 202 | }, 203 | block_to_string(&definition.block) 204 | ) 205 | } else { 206 | format!( 207 | "{}@{}{} =\n\t{}", 208 | definition.name, 209 | definition.weight, 210 | if definition.params.is_empty() { 211 | String::new() 212 | } else { 213 | format!(" {}", definition.params.join(" ")) 214 | }, 215 | block_to_string(&definition.block) 216 | ) 217 | } 218 | } 219 | 220 | pub fn format(input: &str) -> Result { 221 | let tree = parse(input)?; 222 | let output = tree 223 | .iter() 224 | .map(|definition| definition_to_string(definition)) 225 | .collect::>() 226 | .join("\n\n"); 227 | Ok(format!("{}\n", output)) 228 | } 229 | 230 | #[cfg(test)] 231 | mod tests { 232 | use super::*; 233 | use crate::interpreter::execute; 234 | use crate::out::Config; 235 | 236 | fn can_execute(output: &str) -> bool { 237 | let config = Config { 238 | seed: Some([0; 32]), 239 | ..Config::default() 240 | }; 241 | parse(output).and_then(|tree| execute(tree, config)).is_ok() 242 | } 243 | 244 | #[test] 245 | fn test_binary_operation() { 246 | let output = format( 247 | " 248 | root= square 249 | 250 | square =ss (3+5) SQUARE 251 | ", 252 | ); 253 | assert!(output.is_ok()); 254 | assert_eq!( 255 | output.as_ref().unwrap(), 256 | "\ 257 | root = 258 | square 259 | 260 | square = 261 | (ss (3 + 5) SQUARE) 262 | " 263 | ); 264 | assert!(can_execute(output.as_ref().unwrap())); 265 | } 266 | 267 | #[test] 268 | fn test_let_statement() { 269 | let output = format( 270 | " 271 | root = square 272 | 273 | square = 274 | let n1 = 3; n2 = 5 -> 275 | tx (n1+n2) SQUARE 276 | ", 277 | ); 278 | assert!(output.is_ok()); 279 | assert_eq!( 280 | output.as_ref().unwrap(), 281 | "\ 282 | root = 283 | square 284 | 285 | square = 286 | let n1 = 3 287 | n2 = 5 288 | (tx (n1 + n2) SQUARE) 289 | " 290 | ); 291 | assert!(can_execute(output.as_ref().unwrap())); 292 | } 293 | 294 | #[test] 295 | fn test_if_statement() { 296 | let output = format( 297 | " 298 | root =shape true 299 | 300 | shape is_square =if is_square 301 | SQUARE 302 | else -> CIRCLE 303 | ", 304 | ); 305 | assert!(output.is_ok()); 306 | assert_eq!( 307 | output.as_ref().unwrap(), 308 | "\ 309 | root = 310 | (shape true) 311 | 312 | shape is_square = 313 | if is_square 314 | SQUARE 315 | else 316 | CIRCLE 317 | " 318 | ); 319 | assert!(can_execute(output.as_ref().unwrap())); 320 | } 321 | 322 | #[test] 323 | fn test_match_statement() { 324 | let output = format( 325 | " 326 | root = shape 1 327 | 328 | shape n = 329 | match n 330 | 1 -> SQUARE; 2 if true -> CIRCLE 331 | 3 -> TRIANGLE; _ -> EMPTY 332 | ", 333 | ); 334 | assert!(output.is_ok()); 335 | assert_eq!( 336 | output.as_ref().unwrap(), 337 | "\ 338 | root = 339 | (shape 1) 340 | 341 | shape n = 342 | match n 343 | 1 344 | SQUARE 345 | 2 if true 346 | CIRCLE 347 | 3 348 | TRIANGLE 349 | _ 350 | EMPTY 351 | " 352 | ); 353 | assert!(can_execute(output.as_ref().unwrap())); 354 | } 355 | 356 | #[test] 357 | fn test_for_statement() { 358 | let output = format( 359 | " 360 | root = collect squares 361 | 362 | squares = 363 | for i in 0..3 -> tx i SQUARE 364 | ", 365 | ); 366 | assert!(output.is_ok()); 367 | assert_eq!( 368 | output.as_ref().unwrap(), 369 | "\ 370 | root = 371 | (collect squares) 372 | 373 | squares = 374 | for i in (0 .. 3) 375 | (tx i SQUARE) 376 | " 377 | ); 378 | assert!(can_execute(output.as_ref().unwrap())); 379 | } 380 | 381 | #[test] 382 | fn test_loop_statement() { 383 | let output = format( 384 | " 385 | root = collect squares 386 | 387 | squares = 388 | loop 3 -> tx (rand * 10) SQUARE 389 | ", 390 | ); 391 | assert!(output.is_ok()); 392 | assert_eq!( 393 | output.as_ref().unwrap(), 394 | "\ 395 | root = 396 | (collect squares) 397 | 398 | squares = 399 | loop 3 400 | (tx (rand * 10) SQUARE) 401 | " 402 | ); 403 | assert!(can_execute(output.as_ref().unwrap())); 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/colors.rs: -------------------------------------------------------------------------------- 1 | use nom::branch::alt; 2 | use nom::bytes::complete::tag; 3 | use nom::combinator::value; 4 | use nom::{IResult, Parser}; 5 | 6 | pub const ALICEBLUE: [u8; 3] = [240, 248, 255]; 7 | pub const ANTIQUEWHITE: [u8; 3] = [250, 235, 215]; 8 | pub const AQUA: [u8; 3] = [0, 255, 255]; 9 | pub const AQUAMARINE: [u8; 3] = [127, 255, 212]; 10 | pub const AZURE: [u8; 3] = [240, 255, 255]; 11 | pub const BEIGE: [u8; 3] = [245, 245, 220]; 12 | pub const BISQUE: [u8; 3] = [255, 228, 196]; 13 | pub const BLACK: [u8; 3] = [0, 0, 0]; 14 | pub const BLANCHEDALMOND: [u8; 3] = [255, 235, 205]; 15 | pub const BLUE: [u8; 3] = [0, 0, 255]; 16 | pub const BLUEVIOLET: [u8; 3] = [138, 43, 226]; 17 | pub const BROWN: [u8; 3] = [165, 42, 42]; 18 | pub const BURLYWOOD: [u8; 3] = [222, 184, 135]; 19 | pub const CADETBLUE: [u8; 3] = [95, 158, 160]; 20 | pub const CHARTREUSE: [u8; 3] = [127, 255, 0]; 21 | pub const CHOCOLATE: [u8; 3] = [210, 105, 30]; 22 | pub const CORAL: [u8; 3] = [255, 127, 80]; 23 | pub const CORNFLOWERBLUE: [u8; 3] = [100, 149, 237]; 24 | pub const CORNSILK: [u8; 3] = [255, 248, 220]; 25 | pub const CRIMSON: [u8; 3] = [220, 20, 60]; 26 | pub const CYAN: [u8; 3] = [0, 255, 255]; 27 | pub const DARKBLUE: [u8; 3] = [0, 0, 139]; 28 | pub const DARKCYAN: [u8; 3] = [0, 139, 139]; 29 | pub const DARKGOLDENROD: [u8; 3] = [184, 134, 11]; 30 | pub const DARKGRAY: [u8; 3] = [169, 169, 169]; 31 | pub const DARKGREEN: [u8; 3] = [0, 100, 0]; 32 | pub const DARKGREY: [u8; 3] = [169, 169, 169]; 33 | pub const DARKKHAKI: [u8; 3] = [189, 183, 107]; 34 | pub const DARKMAGENTA: [u8; 3] = [139, 0, 139]; 35 | pub const DARKOLIVEGREEN: [u8; 3] = [85, 107, 47]; 36 | pub const DARKORANGE: [u8; 3] = [255, 140, 0]; 37 | pub const DARKORCHID: [u8; 3] = [153, 50, 204]; 38 | pub const DARKRED: [u8; 3] = [139, 0, 0]; 39 | pub const DARKSALMON: [u8; 3] = [233, 150, 122]; 40 | pub const DARKSEAGREEN: [u8; 3] = [143, 188, 143]; 41 | pub const DARKSLATEBLUE: [u8; 3] = [72, 61, 139]; 42 | pub const DARKSLATEGRAY: [u8; 3] = [47, 79, 79]; 43 | pub const DARKSLATEGREY: [u8; 3] = [47, 79, 79]; 44 | pub const DARKTURQUOISE: [u8; 3] = [0, 206, 209]; 45 | pub const DARKVIOLET: [u8; 3] = [148, 0, 211]; 46 | pub const DEEPPINK: [u8; 3] = [255, 20, 147]; 47 | pub const DEEPSKYBLUE: [u8; 3] = [0, 191, 255]; 48 | pub const DIMGRAY: [u8; 3] = [105, 105, 105]; 49 | pub const DIMGREY: [u8; 3] = [105, 105, 105]; 50 | pub const DODGERBLUE: [u8; 3] = [30, 144, 255]; 51 | pub const FIREBRICK: [u8; 3] = [178, 34, 34]; 52 | pub const FLORALWHITE: [u8; 3] = [255, 250, 240]; 53 | pub const FORESTGREEN: [u8; 3] = [34, 139, 34]; 54 | pub const FUCHSIA: [u8; 3] = [255, 0, 255]; 55 | pub const GAINSBORO: [u8; 3] = [220, 220, 220]; 56 | pub const GHOSTWHITE: [u8; 3] = [248, 248, 255]; 57 | pub const GOLD: [u8; 3] = [255, 215, 0]; 58 | pub const GOLDENROD: [u8; 3] = [218, 165, 32]; 59 | pub const GRAY: [u8; 3] = [128, 128, 128]; 60 | pub const GREY: [u8; 3] = [128, 128, 128]; 61 | pub const GREEN: [u8; 3] = [0, 128, 0]; 62 | pub const GREENYELLOW: [u8; 3] = [173, 255, 47]; 63 | pub const HONEYDEW: [u8; 3] = [240, 255, 240]; 64 | pub const HOTPINK: [u8; 3] = [255, 105, 180]; 65 | pub const INDIANRED: [u8; 3] = [205, 92, 92]; 66 | pub const INDIGO: [u8; 3] = [75, 0, 130]; 67 | pub const IVORY: [u8; 3] = [255, 255, 240]; 68 | pub const KHAKI: [u8; 3] = [240, 230, 140]; 69 | pub const LAVENDER: [u8; 3] = [230, 230, 250]; 70 | pub const LAVENDERBLUSH: [u8; 3] = [255, 240, 245]; 71 | pub const LAWNGREEN: [u8; 3] = [124, 252, 0]; 72 | pub const LEMONCHIFFON: [u8; 3] = [255, 250, 205]; 73 | pub const LIGHTBLUE: [u8; 3] = [173, 216, 230]; 74 | pub const LIGHTCORAL: [u8; 3] = [240, 128, 128]; 75 | pub const LIGHTCYAN: [u8; 3] = [224, 255, 255]; 76 | pub const LIGHTGOLDENRODYELLOW: [u8; 3] = [250, 250, 210]; 77 | pub const LIGHTGRAY: [u8; 3] = [211, 211, 211]; 78 | pub const LIGHTGREEN: [u8; 3] = [144, 238, 144]; 79 | pub const LIGHTGREY: [u8; 3] = [211, 211, 211]; 80 | pub const LIGHTPINK: [u8; 3] = [255, 182, 193]; 81 | pub const LIGHTSALMON: [u8; 3] = [255, 160, 122]; 82 | pub const LIGHTSEAGREEN: [u8; 3] = [32, 178, 170]; 83 | pub const LIGHTSKYBLUE: [u8; 3] = [135, 206, 250]; 84 | pub const LIGHTSLATEGRAY: [u8; 3] = [119, 136, 153]; 85 | pub const LIGHTSLATEGREY: [u8; 3] = [119, 136, 153]; 86 | pub const LIGHTSTEELBLUE: [u8; 3] = [176, 196, 222]; 87 | pub const LIGHTYELLOW: [u8; 3] = [255, 255, 224]; 88 | pub const LIME: [u8; 3] = [0, 255, 0]; 89 | pub const LIMEGREEN: [u8; 3] = [50, 205, 50]; 90 | pub const LINEN: [u8; 3] = [250, 240, 230]; 91 | pub const MAGENTA: [u8; 3] = [255, 0, 255]; 92 | pub const MAROON: [u8; 3] = [128, 0, 0]; 93 | pub const MEDIUMAQUAMARINE: [u8; 3] = [102, 205, 170]; 94 | pub const MEDIUMBLUE: [u8; 3] = [0, 0, 205]; 95 | pub const MEDIUMORCHID: [u8; 3] = [186, 85, 211]; 96 | pub const MEDIUMPURPLE: [u8; 3] = [147, 112, 219]; 97 | pub const MEDIUMSEAGREEN: [u8; 3] = [60, 179, 113]; 98 | pub const MEDIUMSLATEBLUE: [u8; 3] = [123, 104, 238]; 99 | pub const MEDIUMSPRINGGREEN: [u8; 3] = [0, 250, 154]; 100 | pub const MEDIUMTURQUOISE: [u8; 3] = [72, 209, 204]; 101 | pub const MEDIUMVIOLETRED: [u8; 3] = [199, 21, 133]; 102 | pub const MIDNIGHTBLUE: [u8; 3] = [25, 25, 112]; 103 | pub const MINTCREAM: [u8; 3] = [245, 255, 250]; 104 | pub const MISTYROSE: [u8; 3] = [255, 228, 225]; 105 | pub const MOCCASIN: [u8; 3] = [255, 228, 181]; 106 | pub const NAVAJOWHITE: [u8; 3] = [255, 222, 173]; 107 | pub const NAVY: [u8; 3] = [0, 0, 128]; 108 | pub const OLDLACE: [u8; 3] = [253, 245, 230]; 109 | pub const OLIVE: [u8; 3] = [128, 128, 0]; 110 | pub const OLIVEDRAB: [u8; 3] = [107, 142, 35]; 111 | pub const ORANGE: [u8; 3] = [255, 165, 0]; 112 | pub const ORANGERED: [u8; 3] = [255, 69, 0]; 113 | pub const ORCHID: [u8; 3] = [218, 112, 214]; 114 | pub const PALEGOLDENROD: [u8; 3] = [238, 232, 170]; 115 | pub const PALEGREEN: [u8; 3] = [152, 251, 152]; 116 | pub const PALETURQUOISE: [u8; 3] = [175, 238, 238]; 117 | pub const PALEVIOLETRED: [u8; 3] = [219, 112, 147]; 118 | pub const PAPAYAWHIP: [u8; 3] = [255, 239, 213]; 119 | pub const PEACHPUFF: [u8; 3] = [255, 218, 185]; 120 | pub const PERU: [u8; 3] = [205, 133, 63]; 121 | pub const PINK: [u8; 3] = [255, 192, 203]; 122 | pub const PLUM: [u8; 3] = [221, 160, 221]; 123 | pub const POWDERBLUE: [u8; 3] = [176, 224, 230]; 124 | pub const PURPLE: [u8; 3] = [128, 0, 128]; 125 | pub const REBECCAPURPLE: [u8; 3] = [102, 51, 153]; 126 | pub const RED: [u8; 3] = [255, 0, 0]; 127 | pub const ROSYBROWN: [u8; 3] = [188, 143, 143]; 128 | pub const ROYALBLUE: [u8; 3] = [65, 105, 225]; 129 | pub const SADDLEBROWN: [u8; 3] = [139, 69, 19]; 130 | pub const SALMON: [u8; 3] = [250, 128, 114]; 131 | pub const SANDYBROWN: [u8; 3] = [244, 164, 96]; 132 | pub const SEAGREEN: [u8; 3] = [46, 139, 87]; 133 | pub const SEASHELL: [u8; 3] = [255, 245, 238]; 134 | pub const SIENNA: [u8; 3] = [160, 82, 45]; 135 | pub const SILVER: [u8; 3] = [192, 192, 192]; 136 | pub const SKYBLUE: [u8; 3] = [135, 206, 235]; 137 | pub const SLATEBLUE: [u8; 3] = [106, 90, 205]; 138 | pub const SLATEGRAY: [u8; 3] = [112, 128, 144]; 139 | pub const SLATEGREY: [u8; 3] = [112, 128, 144]; 140 | pub const SNOW: [u8; 3] = [255, 250, 250]; 141 | pub const SPRINGGREEN: [u8; 3] = [0, 255, 127]; 142 | pub const STEELBLUE: [u8; 3] = [70, 130, 180]; 143 | pub const TAN: [u8; 3] = [210, 180, 140]; 144 | pub const TEAL: [u8; 3] = [0, 128, 128]; 145 | pub const THISTLE: [u8; 3] = [216, 191, 216]; 146 | pub const TOMATO: [u8; 3] = [255, 99, 71]; 147 | pub const TURQUOISE: [u8; 3] = [64, 224, 208]; 148 | pub const VIOLET: [u8; 3] = [238, 130, 238]; 149 | pub const WHEAT: [u8; 3] = [245, 222, 179]; 150 | pub const WHITE: [u8; 3] = [255, 255, 255]; 151 | pub const WHITESMOKE: [u8; 3] = [245, 245, 245]; 152 | pub const YELLOW: [u8; 3] = [255, 255, 0]; 153 | pub const YELLOWGREEN: [u8; 3] = [154, 205, 50]; 154 | 155 | pub fn color(input: &str) -> IResult<&str, [u8; 3]> { 156 | alt(( 157 | alt(( 158 | value(ALICEBLUE, tag("ALICEBLUE")), 159 | value(ANTIQUEWHITE, tag("ANTIQUEWHITE")), 160 | value(AQUA, tag("AQUA")), 161 | value(AQUAMARINE, tag("AQUAMARINE")), 162 | value(AZURE, tag("AZURE")), 163 | value(BEIGE, tag("BEIGE")), 164 | value(BISQUE, tag("BISQUE")), 165 | value(BLACK, tag("BLACK")), 166 | value(BLANCHEDALMOND, tag("BLANCHEDALMOND")), 167 | value(BLUE, tag("BLUE")), 168 | value(BLUEVIOLET, tag("BLUEVIOLET")), 169 | value(BROWN, tag("BROWN")), 170 | value(BURLYWOOD, tag("BURLYWOOD")), 171 | value(CADETBLUE, tag("CADETBLUE")), 172 | value(CHARTREUSE, tag("CHARTREUSE")), 173 | value(CHOCOLATE, tag("CHOCOLATE")), 174 | value(CORAL, tag("CORAL")), 175 | value(CORNFLOWERBLUE, tag("CORNFLOWERBLUE")), 176 | value(CORNSILK, tag("CORNSILK")), 177 | value(CRIMSON, tag("CRIMSON")), 178 | value(CYAN, tag("CYAN")), 179 | )), 180 | alt(( 181 | value(DARKBLUE, tag("DARKBLUE")), 182 | value(DARKCYAN, tag("DARKCYAN")), 183 | value(DARKGOLDENROD, tag("DARKGOLDENROD")), 184 | value(DARKGRAY, tag("DARKGRAY")), 185 | value(DARKGREEN, tag("DARKGREEN")), 186 | value(DARKGREY, tag("DARKGREY")), 187 | value(DARKKHAKI, tag("DARKKHAKI")), 188 | value(DARKMAGENTA, tag("DARKMAGENTA")), 189 | value(DARKOLIVEGREEN, tag("DARKOLIVEGREEN")), 190 | value(DARKORANGE, tag("DARKORANGE")), 191 | value(DARKORCHID, tag("DARKORCHID")), 192 | value(DARKRED, tag("DARKRED")), 193 | value(DARKSALMON, tag("DARKSALMON")), 194 | value(DARKSEAGREEN, tag("DARKSEAGREEN")), 195 | value(DARKSLATEBLUE, tag("DARKSLATEBLUE")), 196 | value(DARKSLATEGRAY, tag("DARKSLATEGRAY")), 197 | value(DARKSLATEGREY, tag("DARKSLATEGREY")), 198 | value(DARKTURQUOISE, tag("DARKTURQUOISE")), 199 | value(DARKVIOLET, tag("DARKVIOLET")), 200 | value(DEEPPINK, tag("DEEPPINK")), 201 | value(DEEPSKYBLUE, tag("DEEPSKYBLUE")), 202 | )), 203 | alt(( 204 | value(DIMGRAY, tag("DIMGRAY")), 205 | value(DIMGREY, tag("DIMGREY")), 206 | value(DODGERBLUE, tag("DODGERBLUE")), 207 | value(FIREBRICK, tag("FIREBRICK")), 208 | value(FLORALWHITE, tag("FLORALWHITE")), 209 | value(FORESTGREEN, tag("FORESTGREEN")), 210 | value(FUCHSIA, tag("FUCHSIA")), 211 | value(GAINSBORO, tag("GAINSBORO")), 212 | value(GHOSTWHITE, tag("GHOSTWHITE")), 213 | value(GOLD, tag("GOLD")), 214 | value(GOLDENROD, tag("GOLDENROD")), 215 | value(GRAY, tag("GRAY")), 216 | value(GREY, tag("GREY")), 217 | value(GREEN, tag("GREEN")), 218 | value(GREENYELLOW, tag("GREENYELLOW")), 219 | value(HONEYDEW, tag("HONEYDEW")), 220 | value(HOTPINK, tag("HOTPINK")), 221 | value(INDIANRED, tag("INDIANRED")), 222 | value(INDIGO, tag("INDIGO")), 223 | value(IVORY, tag("IVORY")), 224 | value(KHAKI, tag("KHAKI")), 225 | )), 226 | alt(( 227 | value(LAVENDER, tag("LAVENDER")), 228 | value(LAVENDERBLUSH, tag("LAVENDERBLUSH")), 229 | value(LAWNGREEN, tag("LAWNGREEN")), 230 | value(LEMONCHIFFON, tag("LEMONCHIFFON")), 231 | value(LIGHTBLUE, tag("LIGHTBLUE")), 232 | value(LIGHTCORAL, tag("LIGHTCORAL")), 233 | value(LIGHTCYAN, tag("LIGHTCYAN")), 234 | value(LIGHTGOLDENRODYELLOW, tag("LIGHTGOLDENRODYELLOW")), 235 | value(LIGHTGRAY, tag("LIGHTGRAY")), 236 | value(LIGHTGREEN, tag("LIGHTGREEN")), 237 | value(LIGHTGREY, tag("LIGHTGREY")), 238 | value(LIGHTPINK, tag("LIGHTPINK")), 239 | value(LIGHTSALMON, tag("LIGHTSALMON")), 240 | value(LIGHTSEAGREEN, tag("LIGHTSEAGREEN")), 241 | value(LIGHTSKYBLUE, tag("LIGHTSKYBLUE")), 242 | value(LIGHTSLATEGRAY, tag("LIGHTSLATEGRAY")), 243 | value(LIGHTSLATEGREY, tag("LIGHTSLATEGREY")), 244 | value(LIGHTSTEELBLUE, tag("LIGHTSTEELBLUE")), 245 | value(LIGHTYELLOW, tag("LIGHTYELLOW")), 246 | value(LIME, tag("LIME")), 247 | value(LIMEGREEN, tag("LIMEGREEN")), 248 | )), 249 | alt(( 250 | value(LINEN, tag("LINEN")), 251 | value(MAGENTA, tag("MAGENTA")), 252 | value(MAROON, tag("MAROON")), 253 | value(MEDIUMAQUAMARINE, tag("MEDIUMAQUAMARINE")), 254 | value(MEDIUMBLUE, tag("MEDIUMBLUE")), 255 | value(MEDIUMORCHID, tag("MEDIUMORCHID")), 256 | value(MEDIUMPURPLE, tag("MEDIUMPURPLE")), 257 | value(MEDIUMSEAGREEN, tag("MEDIUMSEAGREEN")), 258 | value(MEDIUMSLATEBLUE, tag("MEDIUMSLATEBLUE")), 259 | value(MEDIUMSPRINGGREEN, tag("MEDIUMSPRINGGREEN")), 260 | value(MEDIUMTURQUOISE, tag("MEDIUMTURQUOISE")), 261 | value(MEDIUMVIOLETRED, tag("MEDIUMVIOLETRED")), 262 | value(MIDNIGHTBLUE, tag("MIDNIGHTBLUE")), 263 | value(MINTCREAM, tag("MINTCREAM")), 264 | value(MISTYROSE, tag("MISTYROSE")), 265 | value(MOCCASIN, tag("MOCCASIN")), 266 | value(NAVAJOWHITE, tag("NAVAJOWHITE")), 267 | value(NAVY, tag("NAVY")), 268 | value(OLDLACE, tag("OLDLACE")), 269 | value(OLIVE, tag("OLIVE")), 270 | value(OLIVEDRAB, tag("OLIVEDRAB")), 271 | )), 272 | alt(( 273 | value(ORANGE, tag("ORANGE")), 274 | value(ORANGERED, tag("ORANGERED")), 275 | value(ORCHID, tag("ORCHID")), 276 | value(PALEGOLDENROD, tag("PALEGOLDENROD")), 277 | value(PALEGREEN, tag("PALEGREEN")), 278 | value(PALETURQUOISE, tag("PALETURQUOISE")), 279 | value(PALEVIOLETRED, tag("PALEVIOLETRED")), 280 | value(PAPAYAWHIP, tag("PAPAYAWHIP")), 281 | value(PEACHPUFF, tag("PEACHPUFF")), 282 | value(PERU, tag("PERU")), 283 | value(PINK, tag("PINK")), 284 | value(PLUM, tag("PLUM")), 285 | value(POWDERBLUE, tag("POWDERBLUE")), 286 | value(PURPLE, tag("PURPLE")), 287 | value(REBECCAPURPLE, tag("REBECCAPURPLE")), 288 | value(RED, tag("RED")), 289 | value(ROSYBROWN, tag("ROSYBROWN")), 290 | value(ROYALBLUE, tag("ROYALBLUE")), 291 | value(SADDLEBROWN, tag("SADDLEBROWN")), 292 | value(SALMON, tag("SALMON")), 293 | value(SANDYBROWN, tag("SANDYBROWN")), 294 | )), 295 | alt(( 296 | value(SEAGREEN, tag("SEAGREEN")), 297 | value(SEASHELL, tag("SEASHELL")), 298 | value(SIENNA, tag("SIENNA")), 299 | value(SILVER, tag("SILVER")), 300 | value(SKYBLUE, tag("SKYBLUE")), 301 | value(SLATEBLUE, tag("SLATEBLUE")), 302 | value(SLATEGRAY, tag("SLATEGRAY")), 303 | value(SLATEGREY, tag("SLATEGREY")), 304 | value(SNOW, tag("SNOW")), 305 | value(SPRINGGREEN, tag("SPRINGGREEN")), 306 | value(STEELBLUE, tag("STEELBLUE")), 307 | value(TAN, tag("TAN")), 308 | value(TEAL, tag("TEAL")), 309 | value(THISTLE, tag("THISTLE")), 310 | value(TOMATO, tag("TOMATO")), 311 | value(TURQUOISE, tag("TURQUOISE")), 312 | value(VIOLET, tag("VIOLET")), 313 | value(WHEAT, tag("WHEAT")), 314 | value(WHITE, tag("WHITE")), 315 | value(WHITESMOKE, tag("WHITESMOKE")), 316 | value(YELLOW, tag("YELLOW")), 317 | )), 318 | value(YELLOWGREEN, tag("YELLOWGREEN")), 319 | )) 320 | .parse(input) 321 | } 322 | -------------------------------------------------------------------------------- /src/functions/rand.rs: -------------------------------------------------------------------------------- 1 | use crate::builtin_function; 2 | use crate::error::{Error, Result}; 3 | use crate::interpreter::{Data, Value}; 4 | 5 | use noise::NoiseFn; 6 | use rand::prelude::*; 7 | use rand_chacha::ChaCha8Rng; 8 | 9 | builtin_function!(rand rng => { 10 | [] => |rng: &mut ChaCha8Rng| Ok(Value::Float(rng.random())), 11 | }); 12 | 13 | builtin_function!(randi rng => { 14 | [] => |rng: &mut ChaCha8Rng| { 15 | if rng.random() { 16 | Ok(Value::Integer(1)) 17 | } else { 18 | Ok(Value::Integer(0)) 19 | } 20 | }, 21 | }); 22 | 23 | builtin_function!(rand_range rng => { 24 | [Value::Integer(from), Value::Integer(to)] => |rng: &mut ChaCha8Rng| { 25 | let a = *from as f32; 26 | let b = *to as f32; 27 | if a >= b { 28 | return Err(Error::InvalidRange); 29 | } 30 | Ok(Value::Float(rng.random_range(a..b))) 31 | }, 32 | [Value::Float(from), Value::Float(to)] => |rng: &mut ChaCha8Rng| { 33 | if from >= to { 34 | return Err(Error::InvalidRange); 35 | } 36 | Ok(Value::Float(rng.random_range(*from..*to))) 37 | }, 38 | [Value::Integer(from), Value::Float(to)] => |rng: &mut ChaCha8Rng| { 39 | let a = *from as f32; 40 | if a >= *to { 41 | return Err(Error::InvalidRange); 42 | } 43 | Ok(Value::Float(rng.random_range(a..*to))) 44 | }, 45 | [Value::Float(from), Value::Integer(to)] => |rng: &mut ChaCha8Rng| { 46 | let b = *to as f32; 47 | if *from >= b { 48 | return Err(Error::InvalidRange); 49 | } 50 | Ok(Value::Float(rng.random_range(*from..b))) 51 | } 52 | }); 53 | 54 | builtin_function!(rand_rangei rng => { 55 | [Value::Integer(from), Value::Integer(to)] => |rng: &mut ChaCha8Rng| { 56 | let a = *from as f32; 57 | let b = *to as f32; 58 | if a > b { 59 | return Err(Error::InvalidRange); 60 | } 61 | Ok(Value::Float(rng.random_range(a..=b))) 62 | }, 63 | [Value::Float(from), Value::Float(to)] => |rng: &mut ChaCha8Rng| { 64 | if from > to { 65 | return Err(Error::InvalidRange); 66 | } 67 | Ok(Value::Float(rng.random_range(*from..=*to))) 68 | }, 69 | [Value::Integer(from), Value::Float(to)] => |rng: &mut ChaCha8Rng| { 70 | let a = *from as f32; 71 | if a > *to { 72 | return Err(Error::InvalidRange); 73 | } 74 | Ok(Value::Float(rng.random_range(a..=*to))) 75 | }, 76 | [Value::Float(from), Value::Integer(to)] => |rng: &mut ChaCha8Rng| { 77 | let b = *to as f32; 78 | if *from > b { 79 | return Err(Error::InvalidRange); 80 | } 81 | Ok(Value::Float(rng.random_range(*from..=b))) 82 | } 83 | }); 84 | 85 | builtin_function!(randi_range rng => { 86 | [Value::Integer(a), Value::Integer(b)] => |rng: &mut ChaCha8Rng| { 87 | if a >= b { 88 | return Err(Error::InvalidRange); 89 | } 90 | Ok(Value::Integer(rng.random_range(*a..*b))) 91 | }, 92 | [Value::Float(a), Value::Float(b)] => |rng: &mut ChaCha8Rng| { 93 | if a >= b { 94 | return Err(Error::InvalidRange); 95 | } 96 | Ok(Value::Integer(rng.random_range((*a as i32)..(*b as i32)))) 97 | }, 98 | [Value::Integer(a), Value::Float(b)] => |rng: &mut ChaCha8Rng| { 99 | if *a as f32 >= *b { 100 | return Err(Error::InvalidRange); 101 | } 102 | Ok(Value::Integer(rng.random_range(*a..(*b as i32)))) 103 | }, 104 | [Value::Float(a), Value::Integer(b)] => |rng: &mut ChaCha8Rng| { 105 | if *a >= *b as f32 { 106 | return Err(Error::InvalidRange); 107 | } 108 | Ok(Value::Integer(rng.random_range((*a as i32)..*b))) 109 | } 110 | }); 111 | 112 | builtin_function!(randi_rangei rng => { 113 | [Value::Integer(a), Value::Integer(b)] => |rng: &mut ChaCha8Rng| { 114 | if a > b { 115 | return Err(Error::InvalidRange); 116 | } 117 | Ok(Value::Integer(rng.random_range(*a..=*b))) 118 | }, 119 | [Value::Float(a), Value::Float(b)] => |rng: &mut ChaCha8Rng| { 120 | if a > b { 121 | return Err(Error::InvalidRange); 122 | } 123 | Ok(Value::Integer(rng.random_range((*a as i32)..=(*b as i32)))) 124 | }, 125 | [Value::Integer(a), Value::Float(b)] => |rng: &mut ChaCha8Rng| { 126 | if *a as f32 > *b { 127 | return Err(Error::InvalidRange); 128 | } 129 | Ok(Value::Integer(rng.random_range(*a..=(*b as i32)))) 130 | }, 131 | [Value::Float(a), Value::Integer(b)] => |rng: &mut ChaCha8Rng| { 132 | if *a > *b as f32 { 133 | return Err(Error::InvalidRange); 134 | } 135 | Ok(Value::Integer(rng.random_range((*a as i32)..=*b))) 136 | } 137 | }); 138 | 139 | builtin_function!(shuffle rng => { 140 | [Value::List(list)] => |rng: &mut ChaCha8Rng| { 141 | let mut list = list.clone(); 142 | list.shuffle(rng); 143 | Ok(Value::List(list)) 144 | } 145 | }); 146 | 147 | builtin_function!(choose rng => { 148 | [Value::List(list)] => |rng: &mut ChaCha8Rng| { 149 | list.choose(rng).cloned().ok_or(Error::InvalidList) 150 | } 151 | }); 152 | 153 | builtin_function!(noise1 data => { 154 | [a] => |data: &Data| { 155 | let a = match a { 156 | Value::Integer(a) => *a as f64, 157 | Value::Float(a) => *a as f64, 158 | _ => return Err(Error::InvalidArgument("noise1".into())), 159 | }; 160 | 161 | Ok(Value::Float(data.perlin.get([a]) as f32)) 162 | } 163 | }); 164 | 165 | builtin_function!(noise2 data => { 166 | [a, b] => |data: &Data| { 167 | let a = match a { 168 | Value::Integer(a) => *a as f64, 169 | Value::Float(a) => *a as f64, 170 | _ => return Err(Error::InvalidArgument("noise2".into())), 171 | }; 172 | 173 | let b = match b { 174 | Value::Integer(b) => *b as f64, 175 | Value::Float(b) => *b as f64, 176 | _ => return Err(Error::InvalidArgument("noise2".into())), 177 | }; 178 | 179 | Ok(Value::Float(data.perlin.get([a, b]) as f32)) 180 | } 181 | }); 182 | 183 | builtin_function!(noise3 data => { 184 | [a, b, c] => |data: &Data| { 185 | let a = match a { 186 | Value::Integer(a) => *a as f64, 187 | Value::Float(a) => *a as f64, 188 | _ => return Err(Error::InvalidArgument("noise3".into())), 189 | }; 190 | 191 | let b = match b { 192 | Value::Integer(b) => *b as f64, 193 | Value::Float(b) => *b as f64, 194 | _ => return Err(Error::InvalidArgument("noise3".into())), 195 | }; 196 | 197 | let c = match c { 198 | Value::Integer(c) => *c as f64, 199 | Value::Float(c) => *c as f64, 200 | _ => return Err(Error::InvalidArgument("noise3".into())), 201 | }; 202 | 203 | Ok(Value::Float(data.perlin.get([a, b, c]) as f32)) 204 | } 205 | }); 206 | 207 | builtin_function!(noise4 data => { 208 | [a, b, c, d] => |data: &Data| { 209 | let a = match a { 210 | Value::Integer(a) => *a as f64, 211 | Value::Float(a) => *a as f64, 212 | _ => return Err(Error::InvalidArgument("noise4".into())), 213 | }; 214 | 215 | let b = match b { 216 | Value::Integer(b) => *b as f64, 217 | Value::Float(b) => *b as f64, 218 | _ => return Err(Error::InvalidArgument("noise4".into())), 219 | }; 220 | 221 | let c = match c { 222 | Value::Integer(c) => *c as f64, 223 | Value::Float(c) => *c as f64, 224 | _ => return Err(Error::InvalidArgument("noise4".into())), 225 | }; 226 | 227 | let d = match d { 228 | Value::Integer(d) => *d as f64, 229 | Value::Float(d) => *d as f64, 230 | _ => return Err(Error::InvalidArgument("noise4".into())), 231 | }; 232 | 233 | Ok(Value::Float(data.perlin.get([a, b, c, d]) as f32)) 234 | } 235 | }); 236 | 237 | #[cfg(test)] 238 | mod tests { 239 | use super::*; 240 | 241 | #[cfg(feature = "no-std")] 242 | use alloc::vec; 243 | 244 | use noise::Perlin; 245 | use rand::SeedableRng; 246 | 247 | #[test] 248 | fn test_random_functions() { 249 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 250 | let data = Data::default(); 251 | 252 | // Test rand 253 | for _ in 0..100 { 254 | if let Ok(Value::Float(n)) = rand(&mut rng, &data, &[]) { 255 | assert!(n >= 0.0 && n < 1.0); 256 | } else { 257 | panic!("rand failed"); 258 | } 259 | } 260 | 261 | // Test randi 262 | for _ in 0..100 { 263 | if let Ok(Value::Integer(n)) = randi(&mut rng, &data, &[]) { 264 | assert!(n == 0 || n == 1); 265 | } else { 266 | panic!("randi failed"); 267 | } 268 | } 269 | 270 | // Test rand_range with various input types 271 | let range_tests = vec![ 272 | (Value::Integer(1), Value::Integer(10)), // int, int 273 | (Value::Float(1.0), Value::Float(10.0)), // float, float 274 | (Value::Integer(1), Value::Float(10.0)), // int, float 275 | (Value::Float(1.0), Value::Integer(10)), // float, int 276 | ]; 277 | 278 | for _ in 0..100 { 279 | for (from, to) in &range_tests { 280 | if let Ok(Value::Float(n)) = 281 | rand_range(&mut rng, &data, &[from.clone(), to.clone()]) 282 | { 283 | let from_f = match from { 284 | Value::Integer(i) => *i as f32, 285 | Value::Float(f) => *f, 286 | _ => unreachable!(), 287 | }; 288 | let to_f = match to { 289 | Value::Integer(i) => *i as f32, 290 | Value::Float(f) => *f, 291 | _ => unreachable!(), 292 | }; 293 | assert!(n >= from_f && n < to_f); 294 | } else { 295 | panic!("rand_range failed"); 296 | } 297 | } 298 | } 299 | 300 | // Test rand_rangei 301 | for _ in 0..100 { 302 | if let Ok(Value::Float(n)) = 303 | rand_rangei(&mut rng, &data, &[Value::Integer(1), Value::Integer(10)]) 304 | { 305 | assert!(n >= 1.0 && n <= 10.0); 306 | } else { 307 | panic!("rand_rangei failed"); 308 | } 309 | } 310 | 311 | // Test randi_range with various input types 312 | for _ in 0..100 { 313 | for (from, to) in &range_tests { 314 | if let Ok(Value::Integer(n)) = 315 | randi_range(&mut rng, &data, &[from.clone(), to.clone()]) 316 | { 317 | let from_i = match from { 318 | Value::Integer(i) => *i, 319 | Value::Float(f) => *f as i32, 320 | _ => unreachable!(), 321 | }; 322 | let to_i = match to { 323 | Value::Integer(i) => *i, 324 | Value::Float(f) => *f as i32, 325 | _ => unreachable!(), 326 | }; 327 | assert!(n >= from_i && n < to_i); 328 | } else { 329 | panic!("randi_range failed"); 330 | } 331 | } 332 | } 333 | 334 | // Test randi_rangei 335 | for _ in 0..100 { 336 | if let Ok(Value::Integer(n)) = 337 | randi_rangei(&mut rng, &data, &[Value::Integer(1), Value::Integer(10)]) 338 | { 339 | assert!(n >= 1 && n <= 10); 340 | } else { 341 | panic!("randi_rangei failed"); 342 | } 343 | } 344 | 345 | // Test shuffle 346 | let original = vec![ 347 | Value::Integer(1), 348 | Value::Integer(2), 349 | Value::Integer(3), 350 | Value::Integer(4), 351 | ]; 352 | if let Ok(Value::List(shuffled)) = 353 | shuffle(&mut rng, &data, &[Value::List(original.clone())]) 354 | { 355 | assert_eq!(shuffled.len(), 4); 356 | assert_ne!(shuffled, original); 357 | // Check all elements are still present 358 | for item in original { 359 | assert!(shuffled.contains(&item)); 360 | } 361 | } else { 362 | panic!("shuffle failed"); 363 | } 364 | 365 | // Test choose 366 | let list = Value::List(vec![ 367 | Value::Integer(1), 368 | Value::Integer(2), 369 | Value::Integer(3), 370 | ]); 371 | if let Ok(chosen) = choose(&mut rng, &data, &[list]) { 372 | assert!(matches!(chosen, Value::Integer(1 | 2 | 3))); 373 | } else { 374 | panic!("choose failed"); 375 | } 376 | } 377 | 378 | #[test] 379 | fn test_noise_functions() { 380 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 381 | let data = Data::default(); 382 | 383 | // Test noise1 with various input types 384 | let noise1_tests = vec![Value::Integer(42), Value::Float(3.14)]; 385 | 386 | for input in noise1_tests { 387 | if let Ok(Value::Float(n)) = noise1(&mut rng, &data, &[input]) { 388 | assert!(n >= -1.0 && n <= 1.0); 389 | } else { 390 | panic!("noise1 failed"); 391 | } 392 | } 393 | 394 | // Test noise2 395 | let inputs = vec![ 396 | (Value::Integer(1), Value::Integer(2)), 397 | (Value::Float(1.0), Value::Float(2.0)), 398 | (Value::Integer(1), Value::Float(2.0)), 399 | (Value::Float(1.0), Value::Integer(2)), 400 | ]; 401 | 402 | for (a, b) in inputs { 403 | if let Ok(Value::Float(n)) = noise2(&mut rng, &data, &[a, b]) { 404 | assert!(n >= -1.0 && n <= 1.0); 405 | } else { 406 | panic!("noise2 failed"); 407 | } 408 | } 409 | 410 | // Test noise3 411 | if let Ok(Value::Float(n)) = noise3( 412 | &mut rng, 413 | &data, 414 | &[Value::Integer(1), Value::Integer(2), Value::Integer(3)], 415 | ) { 416 | assert!(n >= -1.0 && n <= 1.0); 417 | } else { 418 | panic!("noise3 failed"); 419 | } 420 | 421 | // Test noise4 422 | if let Ok(Value::Float(n)) = noise4( 423 | &mut rng, 424 | &data, 425 | &[ 426 | Value::Integer(1), 427 | Value::Integer(2), 428 | Value::Integer(3), 429 | Value::Integer(4), 430 | ], 431 | ) { 432 | assert!(n >= -1.0 && n <= 1.0); 433 | } else { 434 | panic!("noise4 failed"); 435 | } 436 | 437 | // Test invalid inputs 438 | assert!(noise1(&mut rng, &data, &[Value::String("invalid".into())]).is_err()); 439 | assert!(noise2( 440 | &mut rng, 441 | &data, 442 | &[Value::Integer(1), Value::String("invalid".into())] 443 | ) 444 | .is_err()); 445 | } 446 | 447 | #[test] 448 | fn test_edge_cases() { 449 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 450 | let data = Data { 451 | perlin: Perlin::new(0), 452 | ..Default::default() 453 | }; 454 | 455 | // Test empty list for choose 456 | assert!(choose(&mut rng, &data, &[Value::List(vec![])]).is_err()); 457 | 458 | // Test equal bounds for ranges 459 | assert!(rand_range(&mut rng, &data, &[Value::Integer(5), Value::Integer(5)]).is_err()); 460 | assert_eq!( 461 | rand_rangei(&mut rng, &data, &[Value::Integer(5), Value::Integer(5)]).ok(), 462 | Some(Value::Float(5.0)) 463 | ); 464 | assert!(randi_range(&mut rng, &data, &[Value::Integer(5), Value::Integer(5)]).is_err()); 465 | assert_eq!( 466 | randi_rangei(&mut rng, &data, &[Value::Integer(5), Value::Integer(5)]).ok(), 467 | Some(Value::Integer(5)) 468 | ); 469 | 470 | // Test reverse bounds 471 | assert!(rand_range(&mut rng, &data, &[Value::Integer(10), Value::Integer(5)]).is_err()); 472 | assert!(rand_rangei(&mut rng, &data, &[Value::Integer(10), Value::Integer(5)]).is_err()); 473 | assert!(randi_range(&mut rng, &data, &[Value::Integer(10), Value::Integer(5)]).is_err()); 474 | assert!(randi_rangei(&mut rng, &data, &[Value::Integer(10), Value::Integer(5)]).is_err()); 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /src/functions/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "std")] 2 | use std::rc::Rc; 3 | 4 | #[cfg(feature = "no-std")] 5 | use alloc::rc::Rc; 6 | 7 | use crate::error::{Error, Result}; 8 | use crate::interpreter::{Data, Value}; 9 | use crate::shape::Shape; 10 | 11 | use core::cell::RefCell; 12 | use rand_chacha::ChaCha8Rng; 13 | 14 | mod character; 15 | mod color; 16 | mod compare; 17 | mod func; 18 | mod image; 19 | mod list; 20 | mod math; 21 | mod path; 22 | mod rand; 23 | mod shape; 24 | mod system; 25 | mod transform; 26 | 27 | macro_rules! define_builtins { 28 | ( 29 | $( 30 | $name:literal => {$func:path, $param_count:literal} 31 | ),* $(,)? 32 | ) => { 33 | // Generate the static BUILTIN_FUNCTIONS array 34 | pub static BUILTIN_FUNCTIONS: &[&str] = &[ 35 | "map", 36 | $($name),* 37 | ]; 38 | 39 | // Generate the handle_builtin function with match statements 40 | pub fn handle_builtin(name: &str, rng: &mut ChaCha8Rng, data: &Data, args: &[Value]) -> Result { 41 | match name { 42 | $( 43 | $name => $func(rng, data, args), 44 | )* 45 | _ => Err(Error::UnknownFunction(name.into())), 46 | } 47 | } 48 | 49 | // Generate the builtin_param_count function with match statements 50 | pub fn builtin_param_count(name: &str) -> usize { 51 | match name { 52 | "map" => 2, 53 | $( 54 | $name => $param_count, 55 | )* 56 | _ => unreachable!(), 57 | } 58 | } 59 | }; 60 | } 61 | 62 | define_builtins! { 63 | "width" => {system::width, 0}, 64 | "height" => {system::height, 0}, 65 | "neg" => {math::neg, 1}, 66 | "!" => {compare::not, 1}, 67 | "not" => {compare::not, 1}, 68 | "~" => {math::bitnot, 1}, 69 | "bitnot" => {math::bitnot, 1}, 70 | "+" => {math::add, 2}, 71 | "add" => {math::add, 2}, 72 | "-" => {math::sub, 2}, 73 | "sub" => {math::sub, 2}, 74 | "*" => {math::mul, 2}, 75 | "mul" => {math::mul, 2}, 76 | "/" => {math::div, 2}, 77 | "div" => {math::div, 2}, 78 | "%" => {math::modulo, 2}, 79 | "mod" => {math::modulo, 2}, 80 | "**" => {math::pow, 2}, 81 | "pow" => {math::pow, 2}, 82 | "&" => {math::bitand, 2}, 83 | "bitand" => {math::bitand, 2}, 84 | "|" => {math::bitor, 2}, 85 | "bitor" => {math::bitor, 2}, 86 | "^" => {math::bitxor, 2}, 87 | "bitxor" => {math::bitxor, 2}, 88 | "<<" => {math::bitleft, 2}, 89 | "bitleft" => {math::bitleft, 2}, 90 | ">>" => {math::bitright, 2}, 91 | "bitright" => {math::bitright, 2}, 92 | "pi" => {math::pi, 0}, 93 | "π" => {math::pi, 0}, 94 | "tau" => {math::tau, 0}, 95 | "τ" => {math::tau, 0}, 96 | "e" => {math::e, 0}, 97 | "ℯ" => {math::e, 0}, 98 | "phi" => {math::phi, 0}, 99 | "φ" => {math::phi, 0}, 100 | "int" => {math::int, 1}, 101 | "float" => {math::float, 1}, 102 | "complex" => {math::complex, 2}, 103 | "real" => {math::real, 1}, 104 | "imag" => {math::imag, 1}, 105 | "deg_to_rad" => {math::deg_to_rad, 1}, 106 | "rad_to_deg" => {math::rad_to_deg, 1}, 107 | "sin" => {math::sin, 1}, 108 | "cos" => {math::cos, 1}, 109 | "tan" => {math::tan, 1}, 110 | "asin" => {math::asin, 1}, 111 | "acos" => {math::acos, 1}, 112 | "atan" => {math::atan, 1}, 113 | "atan2" => {math::atan2, 2}, 114 | "sinh" => {math::sinh, 1}, 115 | "cosh" => {math::cosh, 1}, 116 | "tanh" => {math::tanh, 1}, 117 | "asinh" => {math::asinh, 1}, 118 | "acosh" => {math::acosh, 1}, 119 | "atanh" => {math::atanh, 1}, 120 | "ln" => {math::ln, 1}, 121 | "log10" => {math::log10, 1}, 122 | "log" => {math::log, 2}, 123 | "floor" => {math::floor, 1}, 124 | "ceil" => {math::ceil, 1}, 125 | "abs" => {math::abs, 1}, 126 | "sqrt" => {math::sqrt, 1}, 127 | "cbrt" => {math::cbrt, 1}, 128 | "fact" => {math::fact, 1}, 129 | "fact2" => {math::fact2, 1}, 130 | "min" => {math::min, 2}, 131 | "max" => {math::max, 2}, 132 | "==" => {compare::eq, 2}, 133 | "eq" => {compare::eq, 2}, 134 | "!=" => {compare::neq, 2}, 135 | "neq" => {compare::neq, 2}, 136 | "<" => {compare::lt, 2}, 137 | "lt" => {compare::lt, 2}, 138 | "<=" => {compare::lte, 2}, 139 | "lte" => {compare::lte, 2}, 140 | ">" => {compare::gt, 2}, 141 | "gt" => {compare::gt, 2}, 142 | ">=" => {compare::gte, 2}, 143 | "gte" => {compare::gte, 2}, 144 | "&&" => {compare::and, 2}, 145 | "and" => {compare::and, 2}, 146 | "||" => {compare::or, 2}, 147 | "or" => {compare::or, 2}, 148 | ".." => {list::range, 2}, 149 | "range" => {list::range, 2}, 150 | "..=" => {list::rangei, 2}, 151 | "rangei" => {list::rangei, 2}, 152 | "++" => {list::concat, 2}, 153 | "concat" => {list::concat, 2}, 154 | "+>" => {list::prepend, 2}, 155 | "prepend" => {list::prepend, 2}, 156 | "<+" => {list::append, 2}, 157 | "append" => {list::append, 2}, 158 | "nth" => {list::nth, 2}, 159 | "set" => {list::set, 3}, 160 | "length" => {list::length, 1}, 161 | "is_empty" => {list::is_empty, 1}, 162 | "head" => {list::head, 1}, 163 | "tail" => {list::tail, 1}, 164 | "init" => {list::init, 1}, 165 | "last" => {list::last, 1}, 166 | "contains" => {list::contains, 2}, 167 | "take" => {list::take, 2}, 168 | "drop" => {list::drop, 2}, 169 | "index_of" => {list::index_of, 2}, 170 | "reverse" => {list::reverse, 1}, 171 | "slice" => {list::slice, 3}, 172 | "split" => {list::split, 2}, 173 | "unique" => {list::unique, 1}, 174 | "min_of" => {list::min_of, 1}, 175 | "max_of" => {list::max_of, 1}, 176 | "sum" => {list::sum, 1}, 177 | "product" => {list::product, 1}, 178 | "sort" => {list::sort, 1}, 179 | "flatten" => {list::flatten, 1}, 180 | "join" => {list::join, 2}, 181 | "intercalate" => {list::intercalate, 2}, 182 | "intersperse" => {list::intersperse, 2}, 183 | "is_control" => {character::is_control, 1}, 184 | "is_uppercase" => {character::is_uppercase, 1}, 185 | "is_lowercase" => {character::is_lowercase, 1}, 186 | "is_alpha" => {character::is_alpha, 1}, 187 | "is_alphanumeric" => {character::is_alphanumeric, 1}, 188 | "is_digit" => {character::is_digit, 1}, 189 | "is_hex_digit" => {character::is_hex_digit, 1}, 190 | "is_numeric" => {character::is_numeric, 1}, 191 | "is_punctuation" => {character::is_punctuation, 1}, 192 | "is_whitespace" => {character::is_whitespace, 1}, 193 | "is_ascii" => {character::is_ascii, 1}, 194 | "to_uppercase" => {character::to_uppercase, 1}, 195 | "to_lowercase" => {character::to_lowercase, 1}, 196 | "digit_to_int" => {character::digit_to_int, 1}, 197 | "int_to_digit" => {character::int_to_digit, 1}, 198 | "words" => {character::words, 1}, 199 | "lines" => {character::lines, 1}, 200 | "string" => {character::string, 1}, 201 | "|>" => {func::pipe, 2}, 202 | "pipe" => {func::pipe, 2}, 203 | // "." => compose_fn 2, 204 | // "compose_fn" => compose_fn 2, 205 | "rand" => {rand::rand, 0}, 206 | "randi" => {rand::randi, 0}, 207 | "rand_range" => {rand::rand_range, 2}, 208 | "randi_range" => {rand::randi_range, 2}, 209 | "rand_rangei" => {rand::rand_rangei, 2}, 210 | "randi_rangei" => {rand::randi_rangei, 2}, 211 | "shuffle" => {rand::shuffle, 1}, 212 | "choose" => {rand::choose, 1}, 213 | "noise1" => {rand::noise1, 1}, 214 | "noise2" => {rand::noise2, 2}, 215 | "noise3" => {rand::noise3, 3}, 216 | "noise4" => {rand::noise4, 4}, 217 | ":" => {shape::compose, 2}, 218 | "compose" => {shape::compose, 2}, 219 | "collect" => {shape::collect, 1}, 220 | "blend" => {shape::blend, 2}, 221 | "anti_alias" => {shape::anti_alias, 2}, 222 | "fill" => {shape::fill, 1}, 223 | "winding" => {shape::winding, 1}, 224 | "even_odd" => {shape::even_odd, 1}, 225 | "stroke" => {shape::stroke, 2}, 226 | "miter_limit" => {shape::miter_limit, 2}, 227 | "line_cap" => {shape::line_cap, 2}, 228 | "line_join" => {shape::line_join, 2}, 229 | "dash" => {shape::dash, 3}, 230 | "no_dash" => {shape::no_dash, 1}, 231 | "mask" => {shape::mask, 2}, 232 | "pattern" => {shape::pattern, 3}, 233 | "voronoi" => {shape::voronoi, 2}, 234 | "t" => {transform::translate, 3}, 235 | "translate" => {transform::translate, 3}, 236 | "tx" => {transform::translatex, 2}, 237 | "translatex" => {transform::translatex, 2}, 238 | "ty" => {transform::translatey, 2}, 239 | "translatey" => {transform::translatey, 2}, 240 | "tt" => {transform::translateb, 2}, 241 | "translateb" => {transform::translateb, 2}, 242 | "r" => {transform::rotate, 2}, 243 | "rotate" => {transform::rotate, 2}, 244 | "ra" => {transform::rotate_at, 4}, 245 | "rotate_at" => {transform::rotate_at, 4}, 246 | "s" => {transform::scale, 3}, 247 | "scale" => {transform::scale, 3}, 248 | "sx" => {transform::scalex, 2}, 249 | "scalex" => {transform::scalex, 2}, 250 | "sy" => {transform::scaley, 2}, 251 | "scaley" => {transform::scaley, 2}, 252 | "ss" => {transform::scaleb, 2}, 253 | "scaleb" => {transform::scaleb, 2}, 254 | "k" => {transform::skew, 3}, 255 | "skew" => {transform::skew, 3}, 256 | "kx" => {transform::skewx, 2}, 257 | "skewx" => {transform::skewx, 2}, 258 | "ky" => {transform::skewy, 2}, 259 | "skewy" => {transform::skewy, 2}, 260 | "kk" => {transform::skewb, 2}, 261 | "skewb" => {transform::skewb, 2}, 262 | "f" => {transform::flip, 2}, 263 | "flip" => {transform::flip, 2}, 264 | "fh" => {transform::fliph, 1}, 265 | "fliph" => {transform::fliph, 1}, 266 | "fv" => {transform::flipv, 1}, 267 | "flipv" => {transform::flipv, 1}, 268 | "fd" => {transform::flipd, 1}, 269 | "flipd" => {transform::flipd, 1}, 270 | "z" => {transform::zindex, 2}, 271 | "zindex" => {transform::zindex, 2}, 272 | "zshift" => {transform::zshift, 2}, 273 | "hsl" => {color::hsl, 4}, 274 | "hsla" => {color::hsla, 5}, 275 | "h" => {color::hue, 2}, 276 | "hue" => {color::hue, 2}, 277 | "sat" => {color::saturation, 2}, 278 | "saturation" => {color::saturation, 2}, 279 | "l" => {color::lightness, 2}, 280 | "lightness" => {color::lightness, 2}, 281 | "a" => {color::alpha, 2}, 282 | "alpha" => {color::alpha, 2}, 283 | "hshift" => {color::hshift, 2}, 284 | "satshift" => {color::satshift, 2}, 285 | "lshift" => {color::lshift, 2}, 286 | "ashift" => {color::ashift, 2}, 287 | "hex" => {color::hex, 2}, 288 | "solid" => {color::solid, 1}, 289 | "g" => {color::gradient, 2}, 290 | "gradient" => {color::gradient, 2}, 291 | "linear_grad" => {color::linear_grad, 4}, 292 | "radial_grad" => {color::radial_grad, 5}, 293 | "grad_start" => {color::grad_start, 3}, 294 | "grad_end" => {color::grad_end, 3}, 295 | "to_linear_grad" => {color::to_linear_grad, 1}, 296 | "grad_radius" => {color::grad_radius, 2}, 297 | "grad_stop_hsl" => {color::grad_stop_hsl, 5}, 298 | "grad_stop_hsla" => {color::grad_stop_hsla, 6}, 299 | "grad_stop_hex" => {color::grad_stop_hex, 3}, 300 | "grad_spread_mode" => {color::grad_spread_mode, 2}, 301 | "move_to" => {path::move_to, 2}, 302 | "line_to" => {path::line_to, 2}, 303 | "quad_to" => {path::quad_to, 4}, 304 | "cubic_to" => {path::cubic_to, 6}, 305 | "close" => {path::close, 0}, 306 | "import_image" => {image::import_image, 1}, 307 | "text" => {image::text, 3}, 308 | "image_quality" => {image::image_quality, 2}, 309 | "brighten" => {image::brighten, 2}, 310 | "contrast" => {image::contrast, 2}, 311 | "grayscale" => {image::grayscale, 1}, 312 | "grayscale_alpha" => {image::grayscale_alpha, 1}, 313 | "huerotate" => {image::huerotate, 2}, 314 | "invert" => {image::invert, 1}, 315 | "blur" => {image::blur, 2}, 316 | "fast_blur" => {image::fast_blur, 2}, 317 | "crop" => {image::crop, 5}, 318 | "filter3x3" => {image::filter3x3, 2}, 319 | "fliph_image" => {image::fliph_image, 1}, 320 | "flipv_image" => {image::flipv_image, 1}, 321 | "flipd_image" => {image::flipd_image, 1}, 322 | "gradienth" => {image::gradienth, 5}, 323 | "gradientv" => {image::gradientv, 5}, 324 | "overlay" => {image::overlay, 4}, 325 | "replace" => {image::replace, 4}, 326 | "resize" => {image::resize, 4}, 327 | "rotate90" => {image::rotate90, 1}, 328 | "rotate180" => {image::rotate180, 1}, 329 | "rotate270" => {image::rotate270, 1}, 330 | "thumbnail" => {image::thumbnail, 3}, 331 | "tile" => {image::tile, 2}, 332 | "unsharpen" => {image::unsharpen, 3}, 333 | "adaptive_threshold" => {image::adaptive_threshold, 2}, 334 | "equalize_histogram" => {image::equalize_histogram, 1}, 335 | "match_histogram" => {image::match_histogram, 2}, 336 | "stretch_contrast" => {image::stretch_contrast, 5}, 337 | "threshold" => {image::threshold, 3}, 338 | "distance_transform" => {image::distance_transform, 2}, 339 | "euclidean_squared_distance_transform" => {image::euclidean_squared_distance_transform, 1}, 340 | "canny" => {image::canny, 3}, 341 | "bilateral_filter" => {image::bilateral_filter, 4}, 342 | "box_filter" => {image::box_filter, 3}, 343 | "gaussian_blur" => {image::gaussian_blur, 2}, 344 | "sharpen_gaussian" => {image::sharpen_gaussian, 3}, 345 | "horizontal_filter" => {image::horizontal_filter, 2}, 346 | "vertical_filter" => {image::vertical_filter, 2}, 347 | "laplacian_filter" => {image::laplacian_filter, 1}, 348 | "median_filter" => {image::median_filter, 3}, 349 | "separable_filter" => {image::separable_filter, 3}, 350 | "separable_filter_equal" => {image::separable_filter_equal, 2}, 351 | "sharpen3x3" => {image::sharpen3x3, 1}, 352 | "translate_image" => {image::translate_image, 3}, 353 | "rotate_image" => {image::rotate_image, 5}, 354 | "rotate_image_about_center" => {image::rotate_image_about_center, 3}, 355 | "warp_image" => {image::warp_image, 3}, 356 | "horizontal_prewitt" => {image::horizontal_prewitt, 1}, 357 | "horizontal_scharr" => {image::horizontal_scharr, 1}, 358 | "horizontal_sobel" => {image::horizontal_sobel, 1}, 359 | "vertical_prewitt" => {image::vertical_prewitt, 1}, 360 | "vertical_scharr" => {image::vertical_scharr, 1}, 361 | "vertical_sobel" => {image::vertical_sobel, 1}, 362 | "prewitt_gradients" => {image::prewitt_gradients, 1}, 363 | "sobel_gradients" => {image::sobel_gradients, 1}, 364 | "integral_image" => {image::integral_image, 1}, 365 | "integral_squared_image" => {image::integral_squared_image, 1}, 366 | "red_channel" => {image::red_channel, 1}, 367 | "green_channel" => {image::green_channel, 1}, 368 | "blue_channel" => {image::blue_channel, 1}, 369 | "image_close" => {image::image_close, 3}, 370 | "image_dilate" => {image::image_dilate, 3}, 371 | "image_erode" => {image::image_erode, 3}, 372 | "image_open" => {image::image_open, 3}, 373 | "gaussian_noise" => {image::gaussian_noise, 4}, 374 | "salt_and_pepper_noise" => {image::salt_and_pepper_noise, 3}, 375 | "suppress_non_maximum" => {image::suppress_non_maximum, 2}, 376 | "pixel_sort" => {image::pixel_sort, 3}, 377 | } 378 | 379 | #[macro_export] 380 | macro_rules! builtin_function { 381 | ($name:ident => { 382 | $( 383 | $pattern:pat => $body:expr 384 | ),* $(,)? 385 | }) => { 386 | pub fn $name(_rng: &mut ChaCha8Rng, _data: &Data, args: &[Value]) -> Result { 387 | match args { 388 | $( 389 | $pattern => Ok($body), 390 | )* 391 | _ => Err(Error::InvalidArgument(stringify!($name).into())), 392 | } 393 | } 394 | }; 395 | 396 | ($name:ident rng => { 397 | $( 398 | $pattern:pat => $body:expr 399 | ),* $(,)? 400 | }) => { 401 | pub fn $name(rng: &mut ChaCha8Rng, _data: &Data, args: &[Value]) -> Result { 402 | match args { 403 | $( 404 | $pattern => $body(rng), 405 | )* 406 | _ => Err(Error::InvalidArgument( 407 | stringify!($name).into(), 408 | )), 409 | } 410 | } 411 | }; 412 | 413 | ($name:ident data => { 414 | $( 415 | $pattern:pat => $body:expr 416 | ),* $(,)? 417 | }) => { 418 | pub fn $name(_rng: &mut ChaCha8Rng, data: &Data, args: &[Value]) -> Result { 419 | match args { 420 | $( 421 | $pattern => $body(data), 422 | )* 423 | _ => Err(Error::InvalidArgument( 424 | stringify!($name).into(), 425 | )), 426 | } 427 | } 428 | }; 429 | } 430 | 431 | fn dedup_shape(shape: &Rc>) -> Rc> { 432 | if Rc::strong_count(shape) > 2 { 433 | // If there's duplicates, clone the underlying data of the shape and create a new reference 434 | Rc::new(RefCell::new(shape.borrow().clone())) 435 | } else { 436 | // Otherwise, clone the reference to the shape 437 | shape.clone() 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /src/functions/shape.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "std")] 2 | use std::rc::Rc; 3 | 4 | #[cfg(feature = "no-std")] 5 | use alloc::{rc::Rc, vec, vec::Vec}; 6 | 7 | use crate::builtin_function; 8 | use crate::error::{Error, Result}; 9 | use crate::functions::dedup_shape; 10 | use crate::interpreter::{Data, Value}; 11 | use crate::shape::{Color, ColorChange, HslaChange, PathSegment, Shape, Style, IDENTITY, WHITE}; 12 | use core::cell::RefCell; 13 | 14 | use rand_chacha::ChaCha8Rng; 15 | use tiny_skia::{BlendMode, FillRule, StrokeDash}; 16 | 17 | builtin_function!(compose => { 18 | [Value::Shape(a), Value::Shape(b)] => { 19 | let shape = match (&*a.borrow(), &*b.borrow()) { 20 | ( 21 | Shape::Path { 22 | segments: a, 23 | transform: a_transform, 24 | zindex, 25 | color, 26 | blend_mode, 27 | anti_alias, 28 | style, 29 | mask, 30 | pattern, 31 | }, 32 | Shape::Path { 33 | segments: b, 34 | transform: b_transform, 35 | .. 36 | }, 37 | ) => { 38 | let mut segments = Vec::with_capacity(a.len() + b.len()); 39 | segments.extend(a); 40 | segments.extend(b); 41 | Shape::Path { 42 | segments, 43 | transform: a_transform.post_concat(*b_transform), 44 | zindex: *zindex, 45 | color: color.clone(), 46 | blend_mode: *blend_mode, 47 | anti_alias: *anti_alias, 48 | style: style.clone(), 49 | mask: mask.clone(), 50 | pattern: pattern.clone(), 51 | } 52 | } 53 | _ => { 54 | let b = if Rc::ptr_eq(a, b) { 55 | Rc::new(RefCell::new(b.borrow().clone())) 56 | } else { 57 | b.clone() 58 | }; 59 | 60 | Shape::composite(a.clone(), b) 61 | }, 62 | }; 63 | Value::Shape(Rc::new(RefCell::new(shape))) 64 | } 65 | }); 66 | 67 | builtin_function!(collect => { 68 | [Value::List(list)] => { 69 | let shapes: Result>>> = list 70 | .iter() 71 | .map(|item| match item { 72 | Value::Shape(shape) => Ok(shape.clone()), 73 | _ => return Err(Error::InvalidArgument("collect".into())), 74 | }) 75 | .collect(); 76 | let shapes = shapes?; 77 | 78 | if shapes.len() < 1 { 79 | return Ok(Value::Shape(Rc::new(RefCell::new(Shape::empty())))); 80 | } 81 | 82 | let is_path = shapes.iter().all(|shape| match &*shape.borrow() { 83 | Shape::Path { .. } => true, 84 | _ => false, 85 | }); 86 | let shape = if is_path { 87 | let mut segments = Vec::with_capacity(shapes.len()); 88 | let mut transform = IDENTITY; 89 | let mut zindex = None; 90 | let mut color = Color::Solid(WHITE); 91 | let mut blend_mode = BlendMode::SourceOver; 92 | let mut anti_alias = true; 93 | let mut style = Style::default(); 94 | let mut mask = None; 95 | let mut pattern = None; 96 | 97 | for path in shapes { 98 | match &*path.borrow() { 99 | Shape::Path { 100 | segments: other_segments, 101 | transform: other_transform, 102 | zindex: other_zindex, 103 | color: other_color, 104 | blend_mode: other_blend_mode, 105 | anti_alias: other_anti_alias, 106 | style: other_style, 107 | mask: other_mask, 108 | pattern: other_pattern, 109 | } => { 110 | segments.extend(other_segments); 111 | transform = transform.post_concat(*other_transform); 112 | zindex = *other_zindex; 113 | color = other_color.clone(); 114 | blend_mode = *other_blend_mode; 115 | anti_alias = *other_anti_alias; 116 | style = other_style.clone(); 117 | mask = other_mask.clone(); 118 | pattern = other_pattern.clone(); 119 | } 120 | _ => unreachable!(), 121 | } 122 | } 123 | 124 | Shape::Path { 125 | segments, 126 | transform, 127 | zindex, 128 | color, 129 | blend_mode, 130 | anti_alias, 131 | style, 132 | mask, 133 | pattern, 134 | } 135 | } else { 136 | Shape::collection(shapes) 137 | }; 138 | Value::Shape(Rc::new(RefCell::new(shape))) 139 | } 140 | }); 141 | 142 | builtin_function!(blend => { 143 | [Value::BlendMode(blend_mode), Value::Shape(shape)] => { 144 | let shape = dedup_shape(shape); 145 | shape.borrow_mut().set_blend_mode(*blend_mode); 146 | Value::Shape(shape.clone()) 147 | } 148 | }); 149 | 150 | builtin_function!(anti_alias => { 151 | [Value::Boolean(anti_alias), Value::Shape(shape)] => { 152 | let shape = dedup_shape(shape); 153 | shape.borrow_mut().set_anti_alias(*anti_alias); 154 | Value::Shape(shape.clone()) 155 | } 156 | }); 157 | 158 | builtin_function!(fill => { 159 | [Value::Shape(shape)] => { 160 | let shape = dedup_shape(shape); 161 | shape.borrow_mut().set_fill_rule(FillRule::Winding); 162 | Value::Shape(shape.clone()) 163 | } 164 | }); 165 | 166 | builtin_function!(winding => { 167 | [Value::Shape(shape)] => { 168 | let shape = dedup_shape(shape); 169 | shape.borrow_mut().set_fill_rule(FillRule::Winding); 170 | Value::Shape(shape.clone()) 171 | } 172 | }); 173 | 174 | builtin_function!(even_odd => { 175 | [Value::Shape(shape)] => { 176 | let shape = dedup_shape(shape); 177 | shape.borrow_mut().set_fill_rule(FillRule::EvenOdd); 178 | Value::Shape(shape.clone()) 179 | } 180 | }); 181 | 182 | builtin_function!(stroke => { 183 | [width, Value::Shape(shape)] => { 184 | let width = match width { 185 | Value::Integer(width) => *width as f32, 186 | Value::Float(width) => *width, 187 | _ => return Err(Error::InvalidArgument("stroke".into())), 188 | }; 189 | 190 | let shape = dedup_shape(shape); 191 | shape.borrow_mut().set_stroke_width(width); 192 | Value::Shape(shape.clone()) 193 | } 194 | }); 195 | 196 | builtin_function!(miter_limit => { 197 | [n, Value::Shape(shape)] => { 198 | let n = match n { 199 | Value::Integer(n) => *n as f32, 200 | Value::Float(n) => *n, 201 | _ => return Err(Error::InvalidArgument("miter_limit".into())), 202 | }; 203 | 204 | let shape = dedup_shape(shape); 205 | shape.borrow_mut().set_miter_limit(n); 206 | Value::Shape(shape.clone()) 207 | } 208 | }); 209 | 210 | builtin_function!(line_cap => { 211 | [Value::LineCap(lc), Value::Shape(shape)] => { 212 | let shape = dedup_shape(shape); 213 | shape.borrow_mut().set_line_cap(*lc); 214 | Value::Shape(shape.clone()) 215 | } 216 | }); 217 | 218 | builtin_function!(line_join => { 219 | [Value::LineJoin(lj), Value::Shape(shape)] => { 220 | let shape = dedup_shape(shape); 221 | shape.borrow_mut().set_line_join(*lj); 222 | Value::Shape(shape.clone()) 223 | } 224 | }); 225 | 226 | builtin_function!(dash => { 227 | [Value::List(array), offset, Value::Shape(shape)] => { 228 | let array = match array.get(0) { 229 | Some(Value::Integer(_)) => array.iter().map(|value| match value { 230 | Value::Integer(n) => *n as f32, 231 | _ => unreachable!(), 232 | }).collect(), 233 | Some(Value::Float(_)) => array.iter().map(|value| match value { 234 | Value::Float(n) => *n, 235 | _ => unreachable!(), 236 | }).collect(), 237 | _ => return Err(Error::InvalidArgument("dash".into())), 238 | }; 239 | 240 | let offset = match offset { 241 | Value::Integer(offset) => *offset as f32, 242 | Value::Float(offset) => *offset, 243 | _ => return Err(Error::InvalidArgument("dash".into())), 244 | }; 245 | 246 | let shape = dedup_shape(shape); 247 | shape.borrow_mut().set_dash(StrokeDash::new(array, offset)); 248 | Value::Shape(shape.clone()) 249 | } 250 | }); 251 | 252 | builtin_function!(no_dash => { 253 | [Value::Shape(shape)] => { 254 | let shape = dedup_shape(shape); 255 | shape.borrow_mut().set_dash(None); 256 | Value::Shape(shape.clone()) 257 | } 258 | }); 259 | 260 | builtin_function!(mask => { 261 | [Value::Shape(mask), Value::Shape(shape)] => { 262 | let mask = dedup_shape(mask); 263 | let shape = dedup_shape(shape); 264 | shape.borrow_mut().set_mask(mask.clone()); 265 | Value::Shape(shape.clone()) 266 | } 267 | }); 268 | 269 | builtin_function!(pattern => { 270 | [Value::Shape(pattern), Value::SpreadMode(sm), Value::Shape(shape)] => { 271 | let pattern = dedup_shape(pattern); 272 | let shape = dedup_shape(shape); 273 | shape.borrow_mut().set_pattern(pattern.clone(), *sm); 274 | Value::Shape(shape.clone()) 275 | } 276 | }); 277 | 278 | builtin_function!(voronoi => { 279 | [Value::List(sites), boxsize] => { 280 | use voronoi::{voronoi, make_polygons, Point}; 281 | 282 | let sites = match sites.get(0) { 283 | Some(Value::List(_)) => sites.iter().map(|value| match value { 284 | Value::List(point) => match point[..] { 285 | [Value::Integer(x), Value::Integer(y)] => Ok(Point::new(x as f64, y as f64)), 286 | [Value::Float(x), Value::Float(y)] => Ok(Point::new(x as f64, y as f64)), 287 | _ => Err(Error::InvalidArgument("voronoi".into())), 288 | } 289 | _ => Err(Error::InvalidArgument("voronoi".into())), 290 | }).collect::>>()?, 291 | _ => return Err(Error::InvalidArgument("voronoi".into())), 292 | }; 293 | 294 | let boxsize = match boxsize { 295 | Value::Integer(boxsize) => *boxsize as f64, 296 | Value::Float(boxsize) => *boxsize as f64, 297 | _ => return Err(Error::InvalidArgument("voronoi".into())), 298 | }; 299 | 300 | let polygons = make_polygons(&voronoi(sites, boxsize)); 301 | let shapes = polygons.iter().map(|points| { 302 | let mut segments = vec![PathSegment::MoveTo((*points[0].x) as f32, (*points[0].y) as f32)]; 303 | for point in &points[1..] { 304 | segments.push(PathSegment::LineTo((*point.x) as f32, (*point.y) as f32)); 305 | } 306 | segments.push(PathSegment::Close); 307 | 308 | Value::Shape(Rc::new(RefCell::new(Shape::Composite { 309 | a: Rc::new(RefCell::new(Shape::empty())), 310 | b: Rc::new(RefCell::new(Shape::path(segments))), 311 | transform: IDENTITY.post_translate(-boxsize as f32 / 2.0, -boxsize as f32 / 2.0), 312 | zindex_overwrite: None, 313 | zindex_shift: None, 314 | color_overwrite: ColorChange::default(), 315 | color_shift: HslaChange::default(), 316 | blend_mode_overwrite: None, 317 | anti_alias_overwrite: None, 318 | style_overwrite: None, 319 | mask_overwrite: None, 320 | pattern_overwrite: None, 321 | }))) 322 | }).collect(); 323 | 324 | Value::List(shapes) 325 | } 326 | }); 327 | 328 | #[cfg(test)] 329 | mod tests { 330 | use super::*; 331 | use crate::shape::{BasicShape, Pattern, Shape, CIRCLE, SQUARE}; 332 | use rand::SeedableRng; 333 | use tiny_skia::{LineCap, SpreadMode, Stroke}; 334 | 335 | #[test] 336 | fn test_compose() { 337 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 338 | let data = Data::default(); 339 | 340 | // Create two simple shapes 341 | let shape1 = Rc::new(RefCell::new(Shape::square())); 342 | let shape2 = Rc::new(RefCell::new(Shape::circle())); 343 | 344 | // Compose them 345 | let result = compose( 346 | &mut rng, 347 | &data, 348 | &[Value::Shape(shape1), Value::Shape(shape2)], 349 | ) 350 | .unwrap(); 351 | 352 | // Verify the composed shape structure 353 | assert_eq!( 354 | result, 355 | Value::Shape(Rc::new(RefCell::new(Shape::composite( 356 | Rc::new(RefCell::new(Shape::Basic(SQUARE.clone(), None, None))), 357 | Rc::new(RefCell::new(Shape::Basic(CIRCLE.clone(), None, None))) 358 | )))) 359 | ); 360 | } 361 | 362 | #[test] 363 | fn test_styling_functions() { 364 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 365 | let data = Data::default(); 366 | 367 | // Create a test shape 368 | let shape = Rc::new(RefCell::new(Shape::square())); 369 | 370 | // Test stroke width 371 | let stroked = stroke( 372 | &mut rng, 373 | &data, 374 | &[Value::Integer(2), Value::Shape(shape.clone())], 375 | ) 376 | .unwrap(); 377 | 378 | assert_eq!( 379 | stroked, 380 | Value::Shape(Rc::new(RefCell::new(Shape::Basic( 381 | BasicShape::Square { 382 | x: -1.0, 383 | y: -1.0, 384 | width: 2.0, 385 | height: 2.0, 386 | transform: IDENTITY, 387 | zindex: None, 388 | color: Color::Solid(WHITE), 389 | blend_mode: BlendMode::SourceOver, 390 | anti_alias: true, 391 | style: Style::Stroke(Stroke { 392 | width: 2.0, 393 | ..Stroke::default() 394 | }), 395 | }, 396 | None, 397 | None, 398 | )))) 399 | ); 400 | 401 | // Test line cap 402 | let round = line_cap( 403 | &mut rng, 404 | &data, 405 | &[Value::LineCap(LineCap::Round), Value::Shape(shape.clone())], 406 | ) 407 | .unwrap(); 408 | 409 | assert_eq!( 410 | round, 411 | Value::Shape(Rc::new(RefCell::new(Shape::Basic( 412 | BasicShape::Square { 413 | x: -1.0, 414 | y: -1.0, 415 | width: 2.0, 416 | height: 2.0, 417 | transform: IDENTITY, 418 | zindex: None, 419 | color: Color::Solid(WHITE), 420 | blend_mode: BlendMode::SourceOver, 421 | anti_alias: true, 422 | style: Style::Stroke(Stroke { 423 | width: 2.0, 424 | line_cap: LineCap::Round, 425 | ..Stroke::default() 426 | }), 427 | }, 428 | None, 429 | None, 430 | )))) 431 | ); 432 | } 433 | 434 | #[test] 435 | fn test_fill_rules() { 436 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 437 | let data = Data::default(); 438 | 439 | // Create a test triangle shape 440 | let shape = Rc::new(RefCell::new(Shape::triangle())); 441 | 442 | // Test winding fill rule 443 | let winding_result = winding(&mut rng, &data, &[Value::Shape(shape.clone())]).unwrap(); 444 | 445 | assert_eq!( 446 | winding_result, 447 | Value::Shape(Rc::new(RefCell::new(Shape::Basic( 448 | BasicShape::Triangle { 449 | points: [-1.0, 0.577350269, 1.0, 0.577350269, 0.0, -1.154700538], 450 | transform: IDENTITY, 451 | zindex: None, 452 | color: Color::Solid(WHITE), 453 | blend_mode: BlendMode::SourceOver, 454 | anti_alias: true, 455 | style: Style::Fill(FillRule::Winding), 456 | }, 457 | None, 458 | None, 459 | )))) 460 | ); 461 | 462 | // Test even-odd fill rule 463 | let even_odd_result = even_odd(&mut rng, &data, &[Value::Shape(shape.clone())]).unwrap(); 464 | 465 | assert_eq!( 466 | even_odd_result, 467 | Value::Shape(Rc::new(RefCell::new(Shape::Basic( 468 | BasicShape::Triangle { 469 | points: [-1.0, 0.577350269, 1.0, 0.577350269, 0.0, -1.154700538], 470 | transform: IDENTITY, 471 | zindex: None, 472 | color: Color::Solid(WHITE), 473 | blend_mode: BlendMode::SourceOver, 474 | anti_alias: true, 475 | style: Style::Fill(FillRule::EvenOdd), 476 | }, 477 | None, 478 | None, 479 | )))) 480 | ); 481 | } 482 | 483 | #[test] 484 | fn test_mask_and_pattern() { 485 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 486 | let data = Data::default(); 487 | 488 | // Create test shapes 489 | let shape = Rc::new(RefCell::new(Shape::square())); 490 | let mask_val = Rc::new(RefCell::new(Shape::circle())); 491 | let pattern_val = Rc::new(RefCell::new(Shape::triangle())); 492 | 493 | // Test mask application 494 | let masked = mask( 495 | &mut rng, 496 | &data, 497 | &[Value::Shape(mask_val.clone()), Value::Shape(shape.clone())], 498 | ) 499 | .unwrap(); 500 | 501 | assert_eq!( 502 | masked, 503 | Value::Shape(Rc::new(RefCell::new(Shape::Basic( 504 | SQUARE.clone(), 505 | Some(mask_val.clone()), 506 | None 507 | )))) 508 | ); 509 | 510 | // Test pattern application 511 | let patterned = pattern( 512 | &mut rng, 513 | &data, 514 | &[ 515 | Value::Shape(pattern_val.clone()), 516 | Value::SpreadMode(SpreadMode::Pad), 517 | Value::Shape(shape.clone()), 518 | ], 519 | ) 520 | .unwrap(); 521 | 522 | assert_eq!( 523 | patterned, 524 | Value::Shape(Rc::new(RefCell::new(Shape::Basic( 525 | SQUARE.clone(), 526 | Some(mask_val), 527 | Some(Pattern { 528 | pattern: pattern_val, 529 | spread_mode: SpreadMode::Pad, 530 | }) 531 | )))) 532 | ); 533 | } 534 | 535 | #[test] 536 | fn test_voronoi() { 537 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 538 | let data = Data::default(); 539 | 540 | // Test with simple sites 541 | let sites = vec![ 542 | Value::List(vec![Value::Integer(0), Value::Integer(0)]), 543 | Value::List(vec![Value::Integer(10), Value::Integer(10)]), 544 | ]; 545 | 546 | let result = voronoi(&mut rng, &data, &[Value::List(sites), Value::Integer(20)]).unwrap(); 547 | 548 | // Should return a list of polygon shapes 549 | if let Value::List(shapes) = result { 550 | assert_eq!(shapes.len(), 2); 551 | for shape in shapes { 552 | if let Value::Shape(rc_shape) = shape { 553 | assert!(matches!(*rc_shape.borrow(), Shape::Composite { .. })); 554 | } else { 555 | panic!("Expected Shape value"); 556 | } 557 | } 558 | } else { 559 | panic!("Expected List value"); 560 | } 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /src/functions/compare.rs: -------------------------------------------------------------------------------- 1 | use crate::builtin_function; 2 | use crate::error::{Error, Result}; 3 | use crate::interpreter::{Data, Value}; 4 | 5 | use rand_chacha::ChaCha8Rng; 6 | 7 | builtin_function!(not => { 8 | [Value::Boolean(b)] => Value::Boolean(!b) 9 | }); 10 | 11 | builtin_function!(eq => { 12 | [a, b] => Value::Boolean(a == b), 13 | }); 14 | 15 | builtin_function!(neq => { 16 | [a, b] => Value::Boolean(a != b), 17 | }); 18 | 19 | builtin_function!(lt => { 20 | [Value::Integer(a), Value::Integer(b)] => Value::Boolean(a < b), 21 | [Value::Float(a), Value::Float(b)] => Value::Boolean(a < b), 22 | [Value::Integer(a), Value::Float(b)] => Value::Boolean((*a as f32) < *b), 23 | [Value::Float(a), Value::Integer(b)] => Value::Boolean(*a < *b as f32), 24 | [Value::Char(a), Value::Char(b)] => Value::Boolean(a < b), 25 | }); 26 | 27 | builtin_function!(lte => { 28 | [Value::Integer(a), Value::Integer(b)] => Value::Boolean(a <= b), 29 | [Value::Float(a), Value::Float(b)] => Value::Boolean(a <= b), 30 | [Value::Integer(a), Value::Float(b)] => Value::Boolean((*a as f32) <= *b), 31 | [Value::Float(a), Value::Integer(b)] => Value::Boolean(*a <= *b as f32), 32 | [Value::Char(a), Value::Char(b)] => Value::Boolean(a <= b), 33 | }); 34 | 35 | builtin_function!(gt => { 36 | [Value::Integer(a), Value::Integer(b)] => Value::Boolean(a > b), 37 | [Value::Float(a), Value::Float(b)] => Value::Boolean(a > b), 38 | [Value::Integer(a), Value::Float(b)] => Value::Boolean((*a as f32) > *b), 39 | [Value::Float(a), Value::Integer(b)] => Value::Boolean(*a > *b as f32), 40 | [Value::Char(a), Value::Char(b)] => Value::Boolean(a > b), 41 | }); 42 | 43 | builtin_function!(gte => { 44 | [Value::Integer(a), Value::Integer(b)] => Value::Boolean(a >= b), 45 | [Value::Float(a), Value::Float(b)] => Value::Boolean(a >= b), 46 | [Value::Integer(a), Value::Float(b)] => Value::Boolean((*a as f32) >= *b), 47 | [Value::Float(a), Value::Integer(b)] => Value::Boolean(*a >= *b as f32), 48 | [Value::Char(a), Value::Char(b)] => Value::Boolean(a >= b), 49 | }); 50 | 51 | builtin_function!(and => { 52 | [Value::Boolean(a), Value::Boolean(b)] => Value::Boolean(*a && *b) 53 | }); 54 | 55 | builtin_function!(or => { 56 | [Value::Boolean(a), Value::Boolean(b)] => Value::Boolean(*a || *b) 57 | }); 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::*; 62 | use num::Complex; 63 | use rand::SeedableRng; 64 | 65 | #[test] 66 | fn test_not() { 67 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 68 | let data = Data::default(); 69 | assert_eq!( 70 | not(&mut rng, &data, &[Value::Boolean(true)]).ok(), 71 | Some(Value::Boolean(false)) 72 | ); 73 | assert_eq!( 74 | not(&mut rng, &data, &[Value::Boolean(false)]).ok(), 75 | Some(Value::Boolean(true)) 76 | ); 77 | } 78 | 79 | #[test] 80 | fn test_eq() { 81 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 82 | let data = Data::default(); 83 | 84 | // Integer comparisons 85 | assert_eq!( 86 | eq(&mut rng, &data, &[Value::Integer(3), Value::Integer(3)]).ok(), 87 | Some(Value::Boolean(true)) 88 | ); 89 | assert_eq!( 90 | eq(&mut rng, &data, &[Value::Integer(3), Value::Integer(2)]).ok(), 91 | Some(Value::Boolean(false)) 92 | ); 93 | 94 | // Float comparisons 95 | assert_eq!( 96 | eq(&mut rng, &data, &[Value::Float(3.14), Value::Float(3.14)]).ok(), 97 | Some(Value::Boolean(true)) 98 | ); 99 | assert_eq!( 100 | eq(&mut rng, &data, &[Value::Float(3.14), Value::Float(2.71)]).ok(), 101 | Some(Value::Boolean(false)) 102 | ); 103 | 104 | // Mixed numeric comparisons 105 | assert_eq!( 106 | eq(&mut rng, &data, &[Value::Integer(3), Value::Float(3.0)]).ok(), 107 | Some(Value::Boolean(true)) 108 | ); 109 | assert_eq!( 110 | eq(&mut rng, &data, &[Value::Float(3.0), Value::Integer(3)]).ok(), 111 | Some(Value::Boolean(true)) 112 | ); 113 | 114 | // Boolean comparisons 115 | assert_eq!( 116 | eq( 117 | &mut rng, 118 | &data, 119 | &[Value::Boolean(true), Value::Boolean(true)] 120 | ) 121 | .ok(), 122 | Some(Value::Boolean(true)) 123 | ); 124 | assert_eq!( 125 | eq( 126 | &mut rng, 127 | &data, 128 | &[Value::Boolean(true), Value::Boolean(false)] 129 | ) 130 | .ok(), 131 | Some(Value::Boolean(false)) 132 | ); 133 | 134 | // Character comparisons 135 | assert_eq!( 136 | eq(&mut rng, &data, &[Value::Char('a'), Value::Char('a')]).ok(), 137 | Some(Value::Boolean(true)) 138 | ); 139 | assert_eq!( 140 | eq(&mut rng, &data, &[Value::Char('a'), Value::Char('b')]).ok(), 141 | Some(Value::Boolean(false)) 142 | ); 143 | 144 | // String comparisons 145 | assert_eq!( 146 | eq( 147 | &mut rng, 148 | &data, 149 | &[Value::String("hello".into()), Value::String("hello".into())] 150 | ) 151 | .ok(), 152 | Some(Value::Boolean(true)) 153 | ); 154 | assert_eq!( 155 | eq( 156 | &mut rng, 157 | &data, 158 | &[Value::String("hello".into()), Value::String("world".into())] 159 | ) 160 | .ok(), 161 | Some(Value::Boolean(false)) 162 | ); 163 | 164 | // Complex comparisons 165 | let complex1 = Value::Complex(Complex::new(1.0, 2.0)); 166 | let complex2 = Value::Complex(Complex::new(1.0, 2.0)); 167 | let complex3 = Value::Complex(Complex::new(3.0, 4.0)); 168 | assert_eq!( 169 | eq(&mut rng, &data, &[complex1.clone(), complex2.clone()]).ok(), 170 | Some(Value::Boolean(true)) 171 | ); 172 | assert_eq!( 173 | eq(&mut rng, &data, &[complex1.clone(), complex3.clone()]).ok(), 174 | Some(Value::Boolean(false)) 175 | ); 176 | 177 | // Invalid comparisons 178 | assert_eq!( 179 | eq( 180 | &mut rng, 181 | &data, 182 | &[Value::Integer(42), Value::String("42".into())] 183 | ) 184 | .ok(), 185 | Some(Value::Boolean(false)) 186 | ); 187 | assert_eq!( 188 | eq(&mut rng, &data, &[Value::Boolean(true), Value::Integer(1)]).ok(), 189 | Some(Value::Boolean(false)) 190 | ); 191 | } 192 | 193 | #[test] 194 | fn test_neq() { 195 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 196 | let data = Data::default(); 197 | 198 | // Integer comparisons 199 | assert_eq!( 200 | neq(&mut rng, &data, &[Value::Integer(3), Value::Integer(3)]).ok(), 201 | Some(Value::Boolean(false)) 202 | ); 203 | assert_eq!( 204 | neq(&mut rng, &data, &[Value::Integer(3), Value::Integer(2)]).ok(), 205 | Some(Value::Boolean(true)) 206 | ); 207 | 208 | // Float comparisons 209 | assert_eq!( 210 | neq(&mut rng, &data, &[Value::Float(3.14), Value::Float(3.14)]).ok(), 211 | Some(Value::Boolean(false)) 212 | ); 213 | assert_eq!( 214 | neq(&mut rng, &data, &[Value::Float(3.14), Value::Float(2.71)]).ok(), 215 | Some(Value::Boolean(true)) 216 | ); 217 | 218 | // Mixed numeric comparisons 219 | assert_eq!( 220 | neq(&mut rng, &data, &[Value::Integer(3), Value::Float(3.0)]).ok(), 221 | Some(Value::Boolean(false)) 222 | ); 223 | assert_eq!( 224 | neq(&mut rng, &data, &[Value::Float(3.0), Value::Integer(3)]).ok(), 225 | Some(Value::Boolean(false)) 226 | ); 227 | 228 | // Boolean comparisons 229 | assert_eq!( 230 | neq( 231 | &mut rng, 232 | &data, 233 | &[Value::Boolean(true), Value::Boolean(true)] 234 | ) 235 | .ok(), 236 | Some(Value::Boolean(false)) 237 | ); 238 | assert_eq!( 239 | neq( 240 | &mut rng, 241 | &data, 242 | &[Value::Boolean(true), Value::Boolean(false)] 243 | ) 244 | .ok(), 245 | Some(Value::Boolean(true)) 246 | ); 247 | 248 | // Character comparisons 249 | assert_eq!( 250 | neq(&mut rng, &data, &[Value::Char('a'), Value::Char('a')]).ok(), 251 | Some(Value::Boolean(false)) 252 | ); 253 | assert_eq!( 254 | neq(&mut rng, &data, &[Value::Char('a'), Value::Char('b')]).ok(), 255 | Some(Value::Boolean(true)) 256 | ); 257 | 258 | // String comparisons 259 | assert_eq!( 260 | neq( 261 | &mut rng, 262 | &data, 263 | &[Value::String("hello".into()), Value::String("hello".into())] 264 | ) 265 | .ok(), 266 | Some(Value::Boolean(false)) 267 | ); 268 | assert_eq!( 269 | neq( 270 | &mut rng, 271 | &data, 272 | &[Value::String("hello".into()), Value::String("world".into())] 273 | ) 274 | .ok(), 275 | Some(Value::Boolean(true)) 276 | ); 277 | 278 | // Complex comparisons 279 | let complex1 = Value::Complex(Complex::new(1.0, 2.0)); 280 | let complex2 = Value::Complex(Complex::new(1.0, 2.0)); 281 | let complex3 = Value::Complex(Complex::new(3.0, 4.0)); 282 | assert_eq!( 283 | neq(&mut rng, &data, &[complex1.clone(), complex2.clone()]).ok(), 284 | Some(Value::Boolean(false)) 285 | ); 286 | assert_eq!( 287 | neq(&mut rng, &data, &[complex1.clone(), complex3.clone()]).ok(), 288 | Some(Value::Boolean(true)) 289 | ); 290 | 291 | // Invalid comparisons 292 | assert_eq!( 293 | neq( 294 | &mut rng, 295 | &data, 296 | &[Value::Integer(42), Value::String("42".into())] 297 | ) 298 | .ok(), 299 | Some(Value::Boolean(true)) 300 | ); 301 | assert_eq!( 302 | neq(&mut rng, &data, &[Value::Boolean(true), Value::Integer(1)]).ok(), 303 | Some(Value::Boolean(true)) 304 | ); 305 | } 306 | 307 | #[test] 308 | fn test_lt() { 309 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 310 | let data = Data::default(); 311 | 312 | // Integer comparisons 313 | assert_eq!( 314 | lt(&mut rng, &data, &[Value::Integer(2), Value::Integer(3)]).ok(), 315 | Some(Value::Boolean(true)) 316 | ); 317 | assert_eq!( 318 | lt(&mut rng, &data, &[Value::Integer(3), Value::Integer(3)]).ok(), 319 | Some(Value::Boolean(false)) 320 | ); 321 | assert_eq!( 322 | lt(&mut rng, &data, &[Value::Integer(4), Value::Integer(3)]).ok(), 323 | Some(Value::Boolean(false)) 324 | ); 325 | 326 | // Float comparisons 327 | assert_eq!( 328 | lt(&mut rng, &data, &[Value::Float(2.5), Value::Float(3.5)]).ok(), 329 | Some(Value::Boolean(true)) 330 | ); 331 | assert_eq!( 332 | lt(&mut rng, &data, &[Value::Float(3.5), Value::Float(3.5)]).ok(), 333 | Some(Value::Boolean(false)) 334 | ); 335 | 336 | // Mixed integer/float comparisons 337 | assert_eq!( 338 | lt(&mut rng, &data, &[Value::Integer(2), Value::Float(3.5)]).ok(), 339 | Some(Value::Boolean(true)) 340 | ); 341 | assert_eq!( 342 | lt(&mut rng, &data, &[Value::Float(2.5), Value::Integer(3)]).ok(), 343 | Some(Value::Boolean(true)) 344 | ); 345 | 346 | // Char comparisons 347 | assert_eq!( 348 | lt(&mut rng, &data, &[Value::Char('a'), Value::Char('b')]).ok(), 349 | Some(Value::Boolean(true)) 350 | ); 351 | assert_eq!( 352 | lt(&mut rng, &data, &[Value::Char('b'), Value::Char('a')]).ok(), 353 | Some(Value::Boolean(false)) 354 | ); 355 | } 356 | 357 | #[test] 358 | fn test_lte() { 359 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 360 | let data = Data::default(); 361 | 362 | // Integer comparisons 363 | assert_eq!( 364 | lte(&mut rng, &data, &[Value::Integer(2), Value::Integer(3)]).ok(), 365 | Some(Value::Boolean(true)) 366 | ); 367 | assert_eq!( 368 | lte(&mut rng, &data, &[Value::Integer(3), Value::Integer(3)]).ok(), 369 | Some(Value::Boolean(true)) 370 | ); 371 | assert_eq!( 372 | lte(&mut rng, &data, &[Value::Integer(4), Value::Integer(3)]).ok(), 373 | Some(Value::Boolean(false)) 374 | ); 375 | 376 | // Float comparisons 377 | assert_eq!( 378 | lte(&mut rng, &data, &[Value::Float(2.5), Value::Float(3.5)]).ok(), 379 | Some(Value::Boolean(true)) 380 | ); 381 | assert_eq!( 382 | lte(&mut rng, &data, &[Value::Float(3.5), Value::Float(3.5)]).ok(), 383 | Some(Value::Boolean(true)) 384 | ); 385 | 386 | // Mixed integer/float comparisons 387 | assert_eq!( 388 | lte(&mut rng, &data, &[Value::Integer(3), Value::Float(3.0)]).ok(), 389 | Some(Value::Boolean(true)) 390 | ); 391 | assert_eq!( 392 | lte(&mut rng, &data, &[Value::Float(3.0), Value::Integer(3)]).ok(), 393 | Some(Value::Boolean(true)) 394 | ); 395 | 396 | // Char comparisons 397 | assert_eq!( 398 | lte(&mut rng, &data, &[Value::Char('a'), Value::Char('b')]).ok(), 399 | Some(Value::Boolean(true)) 400 | ); 401 | assert_eq!( 402 | lte(&mut rng, &data, &[Value::Char('b'), Value::Char('a')]).ok(), 403 | Some(Value::Boolean(false)) 404 | ); 405 | assert_eq!( 406 | lte(&mut rng, &data, &[Value::Char('a'), Value::Char('a')]).ok(), 407 | Some(Value::Boolean(true)) 408 | ); 409 | } 410 | 411 | #[test] 412 | fn test_gt() { 413 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 414 | let data = Data::default(); 415 | 416 | // Integer comparisons 417 | assert_eq!( 418 | gt(&mut rng, &data, &[Value::Integer(2), Value::Integer(3)]).ok(), 419 | Some(Value::Boolean(false)) 420 | ); 421 | assert_eq!( 422 | gt(&mut rng, &data, &[Value::Integer(3), Value::Integer(3)]).ok(), 423 | Some(Value::Boolean(false)) 424 | ); 425 | assert_eq!( 426 | gt(&mut rng, &data, &[Value::Integer(4), Value::Integer(3)]).ok(), 427 | Some(Value::Boolean(true)) 428 | ); 429 | 430 | // Float comparisons 431 | assert_eq!( 432 | gt(&mut rng, &data, &[Value::Float(2.5), Value::Float(3.5)]).ok(), 433 | Some(Value::Boolean(false)) 434 | ); 435 | assert_eq!( 436 | gt(&mut rng, &data, &[Value::Float(3.5), Value::Float(3.5)]).ok(), 437 | Some(Value::Boolean(false)) 438 | ); 439 | assert_eq!( 440 | gt(&mut rng, &data, &[Value::Float(4.5), Value::Float(3.5)]).ok(), 441 | Some(Value::Boolean(true)) 442 | ); 443 | 444 | // Mixed integer/float comparisons 445 | assert_eq!( 446 | gt(&mut rng, &data, &[Value::Integer(4), Value::Float(3.5)]).ok(), 447 | Some(Value::Boolean(true)) 448 | ); 449 | assert_eq!( 450 | gt(&mut rng, &data, &[Value::Float(4.5), Value::Integer(4)]).ok(), 451 | Some(Value::Boolean(true)) 452 | ); 453 | 454 | // Char comparisons 455 | assert_eq!( 456 | gt(&mut rng, &data, &[Value::Char('b'), Value::Char('a')]).ok(), 457 | Some(Value::Boolean(true)) 458 | ); 459 | assert_eq!( 460 | gt(&mut rng, &data, &[Value::Char('a'), Value::Char('b')]).ok(), 461 | Some(Value::Boolean(false)) 462 | ); 463 | } 464 | 465 | #[test] 466 | fn test_gte() { 467 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 468 | let data = Data::default(); 469 | 470 | // Integer comparisons 471 | assert_eq!( 472 | gte(&mut rng, &data, &[Value::Integer(2), Value::Integer(3)]).ok(), 473 | Some(Value::Boolean(false)) 474 | ); 475 | assert_eq!( 476 | gte(&mut rng, &data, &[Value::Integer(3), Value::Integer(3)]).ok(), 477 | Some(Value::Boolean(true)) 478 | ); 479 | assert_eq!( 480 | gte(&mut rng, &data, &[Value::Integer(4), Value::Integer(3)]).ok(), 481 | Some(Value::Boolean(true)) 482 | ); 483 | 484 | // Float comparisons 485 | assert_eq!( 486 | gte(&mut rng, &data, &[Value::Float(2.5), Value::Float(3.5)]).ok(), 487 | Some(Value::Boolean(false)) 488 | ); 489 | assert_eq!( 490 | gte(&mut rng, &data, &[Value::Float(3.5), Value::Float(3.5)]).ok(), 491 | Some(Value::Boolean(true)) 492 | ); 493 | assert_eq!( 494 | gte(&mut rng, &data, &[Value::Float(4.5), Value::Float(3.5)]).ok(), 495 | Some(Value::Boolean(true)) 496 | ); 497 | 498 | // Mixed integer/float comparisons 499 | assert_eq!( 500 | gte(&mut rng, &data, &[Value::Integer(3), Value::Float(3.0)]).ok(), 501 | Some(Value::Boolean(true)) 502 | ); 503 | assert_eq!( 504 | gte(&mut rng, &data, &[Value::Float(3.0), Value::Integer(3)]).ok(), 505 | Some(Value::Boolean(true)) 506 | ); 507 | 508 | // Char comparisons 509 | assert_eq!( 510 | gte(&mut rng, &data, &[Value::Char('b'), Value::Char('a')]).ok(), 511 | Some(Value::Boolean(true)) 512 | ); 513 | assert_eq!( 514 | gte(&mut rng, &data, &[Value::Char('a'), Value::Char('b')]).ok(), 515 | Some(Value::Boolean(false)) 516 | ); 517 | assert_eq!( 518 | gte(&mut rng, &data, &[Value::Char('a'), Value::Char('a')]).ok(), 519 | Some(Value::Boolean(true)) 520 | ); 521 | } 522 | 523 | #[test] 524 | fn test_and() { 525 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 526 | let data = Data::default(); 527 | 528 | // All combinations of boolean values 529 | assert_eq!( 530 | and( 531 | &mut rng, 532 | &data, 533 | &[Value::Boolean(true), Value::Boolean(true)] 534 | ) 535 | .ok(), 536 | Some(Value::Boolean(true)) 537 | ); 538 | assert_eq!( 539 | and( 540 | &mut rng, 541 | &data, 542 | &[Value::Boolean(true), Value::Boolean(false)] 543 | ) 544 | .ok(), 545 | Some(Value::Boolean(false)) 546 | ); 547 | assert_eq!( 548 | and( 549 | &mut rng, 550 | &data, 551 | &[Value::Boolean(false), Value::Boolean(true)] 552 | ) 553 | .ok(), 554 | Some(Value::Boolean(false)) 555 | ); 556 | assert_eq!( 557 | and( 558 | &mut rng, 559 | &data, 560 | &[Value::Boolean(false), Value::Boolean(false)] 561 | ) 562 | .ok(), 563 | Some(Value::Boolean(false)) 564 | ); 565 | } 566 | 567 | #[test] 568 | fn test_or() { 569 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 570 | let data = Data::default(); 571 | 572 | // All combinations of boolean values 573 | assert_eq!( 574 | or( 575 | &mut rng, 576 | &data, 577 | &[Value::Boolean(true), Value::Boolean(true)] 578 | ) 579 | .ok(), 580 | Some(Value::Boolean(true)) 581 | ); 582 | assert_eq!( 583 | or( 584 | &mut rng, 585 | &data, 586 | &[Value::Boolean(true), Value::Boolean(false)] 587 | ) 588 | .ok(), 589 | Some(Value::Boolean(true)) 590 | ); 591 | assert_eq!( 592 | or( 593 | &mut rng, 594 | &data, 595 | &[Value::Boolean(false), Value::Boolean(true)] 596 | ) 597 | .ok(), 598 | Some(Value::Boolean(true)) 599 | ); 600 | assert_eq!( 601 | or( 602 | &mut rng, 603 | &data, 604 | &[Value::Boolean(false), Value::Boolean(false)] 605 | ) 606 | .ok(), 607 | Some(Value::Boolean(false)) 608 | ); 609 | } 610 | } 611 | -------------------------------------------------------------------------------- /src/functions/color.rs: -------------------------------------------------------------------------------- 1 | use crate::builtin_function; 2 | use crate::functions::dedup_shape; 3 | use crate::interpreter::{Data, Value}; 4 | use crate::shape::{Color, Gradient, WHITE}; 5 | 6 | use crate::error::{Error, Result}; 7 | use rand_chacha::ChaCha8Rng; 8 | 9 | builtin_function!(hsl => { 10 | [h, s, l, Value::Shape(shape)] => { 11 | let h = match h { 12 | Value::Integer(h) => *h as f32, 13 | Value::Float(h) => *h, 14 | _ => return Err(Error::InvalidArgument("hsl".into())), 15 | }; 16 | let s = match s { 17 | Value::Integer(s) => *s as f32, 18 | Value::Float(s) => *s, 19 | _ => return Err(Error::InvalidArgument("hsl".into())), 20 | }; 21 | let l = match l { 22 | Value::Integer(l) => *l as f32, 23 | Value::Float(l) => *l, 24 | _ => return Err(Error::InvalidArgument("hsl".into())), 25 | }; 26 | 27 | let shape = dedup_shape(shape); 28 | shape.borrow_mut().set_hsl(h, s, l); 29 | Value::Shape(shape.clone()) 30 | } 31 | }); 32 | 33 | builtin_function!(hsla => { 34 | [h, s, l, a, Value::Shape(shape)] => { 35 | let h = match h { 36 | Value::Integer(h) => *h as f32, 37 | Value::Float(h) => *h, 38 | _ => return Err(Error::InvalidArgument("hsla".into())), 39 | }; 40 | let s = match s { 41 | Value::Integer(s) => *s as f32, 42 | Value::Float(s) => *s, 43 | _ => return Err(Error::InvalidArgument("hsla".into())), 44 | }; 45 | let l = match l { 46 | Value::Integer(l) => *l as f32, 47 | Value::Float(l) => *l, 48 | _ => return Err(Error::InvalidArgument("hsla".into())), 49 | }; 50 | let a = match a { 51 | Value::Integer(a) => *a as f32, 52 | Value::Float(a) => *a, 53 | _ => return Err(Error::InvalidArgument("hsla".into())), 54 | }; 55 | 56 | let shape = dedup_shape(shape); 57 | shape.borrow_mut().set_hsla(h, s, l, a); 58 | Value::Shape(shape.clone()) 59 | } 60 | }); 61 | 62 | builtin_function!(hue => { 63 | [Value::Integer(h), Value::Shape(shape)] => { 64 | let shape = dedup_shape(shape); 65 | shape.borrow_mut().set_hue(*h as f32); 66 | Value::Shape(shape.clone()) 67 | }, 68 | [Value::Float(h), Value::Shape(shape)] => { 69 | let shape = dedup_shape(shape); 70 | shape.borrow_mut().set_hue(*h); 71 | Value::Shape(shape.clone()) 72 | } 73 | }); 74 | 75 | builtin_function!(saturation => { 76 | [Value::Integer(s), Value::Shape(shape)] => { 77 | let shape = dedup_shape(shape); 78 | shape.borrow_mut().set_saturation(*s as f32); 79 | Value::Shape(shape.clone()) 80 | }, 81 | [Value::Float(s), Value::Shape(shape)] => { 82 | let shape = dedup_shape(shape); 83 | shape.borrow_mut().set_saturation(*s); 84 | Value::Shape(shape.clone()) 85 | } 86 | }); 87 | 88 | builtin_function!(lightness => { 89 | [Value::Integer(l), Value::Shape(shape)] => { 90 | let shape = dedup_shape(shape); 91 | shape.borrow_mut().set_lightness(*l as f32); 92 | Value::Shape(shape.clone()) 93 | }, 94 | [Value::Float(l), Value::Shape(shape)] => { 95 | let shape = dedup_shape(shape); 96 | shape.borrow_mut().set_lightness(*l); 97 | Value::Shape(shape.clone()) 98 | } 99 | }); 100 | 101 | builtin_function!(alpha => { 102 | [Value::Integer(a), Value::Shape(shape)] => { 103 | let shape = dedup_shape(shape); 104 | shape.borrow_mut().set_alpha(*a as f32); 105 | Value::Shape(shape.clone()) 106 | }, 107 | [Value::Float(a), Value::Shape(shape)] => { 108 | let shape = dedup_shape(shape); 109 | shape.borrow_mut().set_alpha(*a); 110 | Value::Shape(shape.clone()) 111 | } 112 | }); 113 | 114 | builtin_function!(hshift => { 115 | [Value::Integer(h), Value::Shape(shape)] => { 116 | let shape = dedup_shape(shape); 117 | shape.borrow_mut().shift_hue(*h as f32); 118 | Value::Shape(shape.clone()) 119 | }, 120 | [Value::Float(h), Value::Shape(shape)] => { 121 | let shape = dedup_shape(shape); 122 | shape.borrow_mut().shift_hue(*h); 123 | Value::Shape(shape.clone()) 124 | } 125 | }); 126 | 127 | builtin_function!(satshift => { 128 | [Value::Integer(s), Value::Shape(shape)] => { 129 | let shape = dedup_shape(shape); 130 | shape.borrow_mut().shift_saturation(*s as f32); 131 | Value::Shape(shape.clone()) 132 | }, 133 | [Value::Float(s), Value::Shape(shape)] => { 134 | let shape = dedup_shape(shape); 135 | shape.borrow_mut().shift_saturation(*s); 136 | Value::Shape(shape.clone()) 137 | } 138 | }); 139 | 140 | builtin_function!(lshift => { 141 | [Value::Integer(l), Value::Shape(shape)] => { 142 | let shape = dedup_shape(shape); 143 | shape.borrow_mut().shift_lightness(*l as f32); 144 | Value::Shape(shape.clone()) 145 | }, 146 | [Value::Float(l), Value::Shape(shape)] => { 147 | let shape = dedup_shape(shape); 148 | shape.borrow_mut().shift_lightness(*l); 149 | Value::Shape(shape.clone()) 150 | } 151 | }); 152 | 153 | builtin_function!(ashift => { 154 | [Value::Integer(a), Value::Shape(shape)] => { 155 | let shape = dedup_shape(shape); 156 | shape.borrow_mut().shift_alpha(*a as f32); 157 | Value::Shape(shape.clone()) 158 | }, 159 | [Value::Float(a), Value::Shape(shape)] => { 160 | let shape = dedup_shape(shape); 161 | shape.borrow_mut().shift_alpha(*a); 162 | Value::Shape(shape.clone()) 163 | } 164 | }); 165 | 166 | builtin_function!(hex => { 167 | [Value::Hex(hex), Value::Shape(shape)] => { 168 | let shape = dedup_shape(shape); 169 | shape.borrow_mut().set_hex(*hex); 170 | Value::Shape(shape.clone()) 171 | } 172 | }); 173 | 174 | builtin_function!(solid => { 175 | [Value::Shape(shape)] => { 176 | let shape = dedup_shape(shape); 177 | shape.borrow_mut().set_color(Color::Solid(WHITE)); 178 | Value::Shape(shape.clone()) 179 | } 180 | }); 181 | 182 | builtin_function!(gradient => { 183 | [Value::Gradient(g), Value::Shape(shape)] => { 184 | let shape = dedup_shape(shape); 185 | shape.borrow_mut().set_color(Color::Gradient(g.clone())); 186 | Value::Shape(shape.clone()) 187 | } 188 | }); 189 | 190 | builtin_function!(linear_grad => { 191 | [start_x, start_y, end_x, end_y] => { 192 | let start_x = match start_x { 193 | Value::Integer(start_x) => *start_x as f32, 194 | Value::Float(start_x) => *start_x, 195 | _ => return Err(Error::InvalidArgument("linear_grad".into())), 196 | }; 197 | let start_y = match start_y { 198 | Value::Integer(start_y) => *start_y as f32, 199 | Value::Float(start_y) => *start_y, 200 | _ => return Err(Error::InvalidArgument("linear_grad".into())), 201 | }; 202 | let end_x = match end_x { 203 | Value::Integer(end_x) => *end_x as f32, 204 | Value::Float(end_x) => *end_x, 205 | _ => return Err(Error::InvalidArgument("linear_grad".into())), 206 | }; 207 | let end_y = match end_y { 208 | Value::Integer(end_y) => *end_y as f32, 209 | Value::Float(end_y) => *end_y, 210 | _ => return Err(Error::InvalidArgument("linear_grad".into())), 211 | }; 212 | Value::Gradient(Gradient::linear(start_x, start_y, end_x, end_y)) 213 | } 214 | }); 215 | 216 | builtin_function!(radial_grad => { 217 | [start_x, start_y, end_x, end_y, radius] => { 218 | let start_x = match start_x { 219 | Value::Integer(start_x) => *start_x as f32, 220 | Value::Float(start_x) => *start_x, 221 | _ => return Err(Error::InvalidArgument("radial_grad".into())), 222 | }; 223 | let start_y = match start_y { 224 | Value::Integer(start_y) => *start_y as f32, 225 | Value::Float(start_y) => *start_y, 226 | _ => return Err(Error::InvalidArgument("radial_grad".into())), 227 | }; 228 | let end_x = match end_x { 229 | Value::Integer(end_x) => *end_x as f32, 230 | Value::Float(end_x) => *end_x, 231 | _ => return Err(Error::InvalidArgument("radial_grad".into())), 232 | }; 233 | let end_y = match end_y { 234 | Value::Integer(end_y) => *end_y as f32, 235 | Value::Float(end_y) => *end_y, 236 | _ => return Err(Error::InvalidArgument("radial_grad".into())), 237 | }; 238 | let radius = match radius { 239 | Value::Integer(radius) => *radius as f32, 240 | Value::Float(radius) => *radius, 241 | _ => return Err(Error::InvalidArgument("radial_grad".into())), 242 | }; 243 | Value::Gradient(Gradient::radial(start_x, start_y, end_x, end_y, radius)) 244 | } 245 | }); 246 | 247 | builtin_function!(grad_start => { 248 | [start_x, start_y, Value::Gradient(g)] => { 249 | let start_x = match start_x { 250 | Value::Integer(start_x) => *start_x as f32, 251 | Value::Float(start_x) => *start_x, 252 | _ => return Err(Error::InvalidArgument("grad_start".into())), 253 | }; 254 | let start_y = match start_y { 255 | Value::Integer(start_y) => *start_y as f32, 256 | Value::Float(start_y) => *start_y, 257 | _ => return Err(Error::InvalidArgument("grad_start".into())), 258 | }; 259 | 260 | let mut g = g.clone(); 261 | g.set_start(start_x, start_y); 262 | Value::Gradient(g) 263 | } 264 | }); 265 | 266 | builtin_function!(grad_end => { 267 | [end_x, end_y, Value::Gradient(g)] => { 268 | let end_x = match end_x { 269 | Value::Integer(end_x) => *end_x as f32, 270 | Value::Float(end_x) => *end_x, 271 | _ => return Err(Error::InvalidArgument("grad_end".into())), 272 | }; 273 | let end_y = match end_y { 274 | Value::Integer(end_y) => *end_y as f32, 275 | Value::Float(end_y) => *end_y, 276 | _ => return Err(Error::InvalidArgument("grad_end".into())), 277 | }; 278 | 279 | let mut g = g.clone(); 280 | g.set_end(end_x, end_y); 281 | Value::Gradient(g) 282 | } 283 | }); 284 | 285 | builtin_function!(to_linear_grad => { 286 | [Value::Gradient(g)] => { 287 | let mut g = g.clone(); 288 | g.set_radius(None); 289 | Value::Gradient(g) 290 | } 291 | }); 292 | 293 | builtin_function!(grad_radius => { 294 | [radius, Value::Gradient(g)] => { 295 | let radius = match radius { 296 | Value::Integer(radius) => *radius as f32, 297 | Value::Float(radius) => *radius, 298 | _ => return Err(Error::InvalidArgument("grad_radius".into())), 299 | }; 300 | 301 | let mut g = g.clone(); 302 | g.set_radius(Some(radius)); 303 | Value::Gradient(g) 304 | } 305 | }); 306 | 307 | builtin_function!(grad_stop_hsl => { 308 | [pos, h, s, l, Value::Gradient(g)] => { 309 | let pos = match pos { 310 | Value::Integer(pos) => *pos as f32, 311 | Value::Float(pos) => *pos, 312 | _ => return Err(Error::InvalidArgument("grad_stop_hsl".into())), 313 | }; 314 | let h = match h { 315 | Value::Integer(h) => *h as f32, 316 | Value::Float(h) => *h, 317 | _ => return Err(Error::InvalidArgument("grad_stop_hsl".into())), 318 | }; 319 | let s = match s { 320 | Value::Integer(s) => *s as f32, 321 | Value::Float(s) => *s, 322 | _ => return Err(Error::InvalidArgument("grad_stop_hsl".into())), 323 | }; 324 | let l = match l { 325 | Value::Integer(l) => *l as f32, 326 | Value::Float(l) => *l, 327 | _ => return Err(Error::InvalidArgument("grad_stop_hsl".into())), 328 | }; 329 | 330 | let mut g = g.clone(); 331 | g.set_stop_hsl(pos, h, s, l); 332 | Value::Gradient(g) 333 | } 334 | }); 335 | 336 | builtin_function!(grad_stop_hsla => { 337 | [pos, h, s, l, a, Value::Gradient(g)] => { 338 | let pos = match pos { 339 | Value::Integer(pos) => *pos as f32, 340 | Value::Float(pos) => *pos, 341 | _ => return Err(Error::InvalidArgument("grad_stop_hsla".into())), 342 | }; 343 | let h = match h { 344 | Value::Integer(h) => *h as f32, 345 | Value::Float(h) => *h, 346 | _ => return Err(Error::InvalidArgument("grad_stop_hsla".into())), 347 | }; 348 | let s = match s { 349 | Value::Integer(s) => *s as f32, 350 | Value::Float(s) => *s, 351 | _ => return Err(Error::InvalidArgument("grad_stop_hsla".into())), 352 | }; 353 | let l = match l { 354 | Value::Integer(l) => *l as f32, 355 | Value::Float(l) => *l, 356 | _ => return Err(Error::InvalidArgument("grad_stop_hsla".into())), 357 | }; 358 | let a = match a { 359 | Value::Integer(a) => *a as f32, 360 | Value::Float(a) => *a, 361 | _ => return Err(Error::InvalidArgument("grad_stop_hsla".into())), 362 | }; 363 | 364 | let mut g = g.clone(); 365 | g.set_stop_hsla(pos, h, s, l, a); 366 | Value::Gradient(g) 367 | } 368 | }); 369 | 370 | builtin_function!(grad_stop_hex => { 371 | [pos, Value::Hex(hex), Value::Gradient(g)] => { 372 | let pos = match pos { 373 | Value::Integer(pos) => *pos as f32, 374 | Value::Float(pos) => *pos, 375 | _ => return Err(Error::InvalidArgument("grad_stop_hex".into())), 376 | }; 377 | 378 | let mut g = g.clone(); 379 | g.set_stop_hex(pos, *hex); 380 | Value::Gradient(g) 381 | } 382 | }); 383 | 384 | builtin_function!(grad_spread_mode => { 385 | [Value::SpreadMode(spread_mode), Value::Gradient(g)] => { 386 | let mut g = g.clone(); 387 | g.set_spread_mode(*spread_mode); 388 | Value::Gradient(g) 389 | } 390 | }); 391 | 392 | #[cfg(test)] 393 | mod tests { 394 | use super::*; 395 | 396 | #[cfg(feature = "std")] 397 | use std::rc::Rc; 398 | 399 | #[cfg(feature = "no-std")] 400 | use alloc::{rc::Rc, vec}; 401 | 402 | use crate::shape::Shape; 403 | use core::cell::RefCell; 404 | use rand::SeedableRng; 405 | use tiny_skia::SpreadMode; 406 | 407 | #[test] 408 | fn test_hsl_functions() { 409 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 410 | let data = Data::default(); 411 | let shape = Rc::new(RefCell::new(Shape::path(vec![]))); 412 | 413 | // Test hsl with various input types 414 | let hsl_tests = vec![ 415 | (Value::Integer(180), Value::Integer(50), Value::Integer(50)), // int, int, int 416 | (Value::Float(180.0), Value::Float(0.5), Value::Float(0.5)), // float, float, float 417 | (Value::Integer(180), Value::Float(0.5), Value::Integer(50)), // mixed types 418 | ]; 419 | 420 | for (h, s, l) in hsl_tests { 421 | let result = hsl(&mut rng, &data, &[h, s, l, Value::Shape(shape.clone())]).unwrap(); 422 | 423 | assert!(matches!(result, Value::Shape(_))); 424 | } 425 | 426 | // Test hsla 427 | let hsla_result = hsla( 428 | &mut rng, 429 | &data, 430 | &[ 431 | Value::Integer(180), 432 | Value::Integer(50), 433 | Value::Integer(50), 434 | Value::Float(0.8), 435 | Value::Shape(shape.clone()), 436 | ], 437 | ) 438 | .unwrap(); 439 | assert!(matches!(hsla_result, Value::Shape(_))); 440 | 441 | // Test individual components 442 | let hue_result = hue( 443 | &mut rng, 444 | &data, 445 | &[Value::Integer(90), Value::Shape(shape.clone())], 446 | ) 447 | .unwrap(); 448 | assert!(matches!(hue_result, Value::Shape(_))); 449 | 450 | let sat_result = saturation( 451 | &mut rng, 452 | &data, 453 | &[Value::Float(0.75), Value::Shape(shape.clone())], 454 | ) 455 | .unwrap(); 456 | assert!(matches!(sat_result, Value::Shape(_))); 457 | 458 | // Test shifts 459 | let hshift_result = hshift( 460 | &mut rng, 461 | &data, 462 | &[Value::Integer(30), Value::Shape(shape.clone())], 463 | ) 464 | .unwrap(); 465 | assert!(matches!(hshift_result, Value::Shape(_))); 466 | } 467 | 468 | #[test] 469 | fn test_solid_and_hex() { 470 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 471 | let data = Data::default(); 472 | let shape = Rc::new(RefCell::new(Shape::path(vec![]))); 473 | 474 | // Test solid 475 | let solid_result = solid(&mut rng, &data, &[Value::Shape(shape.clone())]).unwrap(); 476 | assert!(matches!(solid_result, Value::Shape(_))); 477 | 478 | // Test hex 479 | let hex_result = hex( 480 | &mut rng, 481 | &data, 482 | &[Value::Hex([255, 0, 0]), Value::Shape(shape.clone())], 483 | ) 484 | .unwrap(); 485 | assert!(matches!(hex_result, Value::Shape(_))); 486 | } 487 | 488 | #[test] 489 | fn test_gradient_creation() { 490 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 491 | let data = Data::default(); 492 | 493 | // Test linear gradient 494 | let linear_grad = linear_grad( 495 | &mut rng, 496 | &data, 497 | &[ 498 | Value::Integer(0), 499 | Value::Integer(0), 500 | Value::Integer(100), 501 | Value::Integer(100), 502 | ], 503 | ) 504 | .unwrap(); 505 | assert!(matches!(linear_grad, Value::Gradient(_))); 506 | 507 | // Test radial gradient 508 | let radial_grad = radial_grad( 509 | &mut rng, 510 | &data, 511 | &[ 512 | Value::Integer(50), 513 | Value::Integer(50), 514 | Value::Integer(100), 515 | Value::Integer(100), 516 | Value::Integer(50), 517 | ], 518 | ) 519 | .unwrap(); 520 | assert!(matches!(radial_grad, Value::Gradient(_))); 521 | 522 | // Test gradient application to shape 523 | let shape = Rc::new(RefCell::new(Shape::path(vec![]))); 524 | let grad_result = 525 | gradient(&mut rng, &data, &[linear_grad, Value::Shape(shape.clone())]).unwrap(); 526 | assert!(matches!(grad_result, Value::Shape(_))); 527 | } 528 | 529 | #[test] 530 | fn test_gradient_manipulation() { 531 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 532 | let data = Data::default(); 533 | 534 | // Create base gradient 535 | let grad = linear_grad( 536 | &mut rng, 537 | &data, 538 | &[ 539 | Value::Integer(0), 540 | Value::Integer(0), 541 | Value::Integer(100), 542 | Value::Integer(100), 543 | ], 544 | ) 545 | .unwrap(); 546 | 547 | // Test gradient start/end modification 548 | let new_start = grad_start( 549 | &mut rng, 550 | &data, 551 | &[Value::Integer(10), Value::Integer(10), grad.clone()], 552 | ) 553 | .unwrap(); 554 | assert!(matches!(new_start, Value::Gradient(_))); 555 | 556 | let new_end = grad_end( 557 | &mut rng, 558 | &data, 559 | &[Value::Integer(90), Value::Integer(90), new_start.clone()], 560 | ) 561 | .unwrap(); 562 | assert!(matches!(new_end, Value::Gradient(_))); 563 | 564 | // Test gradient stops 565 | let with_stop = grad_stop_hsl( 566 | &mut rng, 567 | &data, 568 | &[ 569 | Value::Float(0.5), 570 | Value::Integer(180), 571 | Value::Integer(50), 572 | Value::Integer(50), 573 | new_end.clone(), 574 | ], 575 | ) 576 | .unwrap(); 577 | assert!(matches!(with_stop, Value::Gradient(_))); 578 | 579 | // Test spread mode 580 | let with_spread = grad_spread_mode( 581 | &mut rng, 582 | &data, 583 | &[Value::SpreadMode(SpreadMode::Repeat), with_stop.clone()], 584 | ) 585 | .unwrap(); 586 | assert!(matches!(with_spread, Value::Gradient(_))); 587 | } 588 | 589 | #[test] 590 | fn test_gradient_conversion() { 591 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 592 | let data = Data::default(); 593 | 594 | // Create radial gradient 595 | let radial = radial_grad( 596 | &mut rng, 597 | &data, 598 | &[ 599 | Value::Integer(50), 600 | Value::Integer(50), 601 | Value::Integer(100), 602 | Value::Integer(100), 603 | Value::Integer(50), 604 | ], 605 | ) 606 | .unwrap(); 607 | 608 | // Convert to linear 609 | let linear = to_linear_grad(&mut rng, &data, &[radial.clone()]).unwrap(); 610 | assert!(matches!(linear, Value::Gradient(_))); 611 | 612 | // Convert back to radial 613 | let radial_again = 614 | grad_radius(&mut rng, &data, &[Value::Integer(75), linear.clone()]).unwrap(); 615 | assert!(matches!(radial_again, Value::Gradient(_))); 616 | } 617 | 618 | #[test] 619 | fn test_invalid_inputs() { 620 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 621 | let data = Data::default(); 622 | let shape = Rc::new(RefCell::new(Shape::path(vec![]))); 623 | 624 | // Test hsl with wrong number of arguments 625 | assert!(hsl( 626 | &mut rng, 627 | &data, 628 | &[ 629 | Value::Integer(180), 630 | Value::Integer(50), 631 | Value::Shape(shape.clone()) 632 | ] 633 | ) 634 | .is_err()); 635 | 636 | // Test gradient with invalid position 637 | let grad = linear_grad( 638 | &mut rng, 639 | &data, 640 | &[ 641 | Value::Integer(0), 642 | Value::Integer(0), 643 | Value::Integer(100), 644 | Value::Integer(100), 645 | ], 646 | ) 647 | .unwrap(); 648 | 649 | assert!(grad_stop_hsl( 650 | &mut rng, 651 | &data, 652 | &[ 653 | Value::String("0.5".into()), // invalid position 654 | Value::Integer(180), 655 | Value::Integer(50), 656 | Value::Integer(50), 657 | grad.clone() 658 | ] 659 | ) 660 | .is_err()); 661 | } 662 | } 663 | -------------------------------------------------------------------------------- /src/functions/transform.rs: -------------------------------------------------------------------------------- 1 | use crate::builtin_function; 2 | use crate::error::{Error, Result}; 3 | use crate::functions::dedup_shape; 4 | use crate::interpreter::{Data, Value}; 5 | 6 | use rand_chacha::ChaCha8Rng; 7 | 8 | builtin_function!(translate => { 9 | [Value::Integer(tx), Value::Integer(ty), Value::Shape(shape)] => { 10 | let shape = dedup_shape(shape); 11 | shape.borrow_mut().translate(*tx as f32, *ty as f32); 12 | Value::Shape(shape.clone()) 13 | }, 14 | [Value::Float(tx), Value::Float(ty), Value::Shape(shape)] => { 15 | let shape = dedup_shape(shape); 16 | shape.borrow_mut().translate(*tx, *ty); 17 | Value::Shape(shape.clone()) 18 | }, 19 | [Value::Integer(tx), Value::Float(ty), Value::Shape(shape)] => { 20 | let shape = dedup_shape(shape); 21 | shape.borrow_mut().translate(*tx as f32, *ty); 22 | Value::Shape(shape.clone()) 23 | }, 24 | [Value::Float(tx), Value::Integer(ty), Value::Shape(shape)] => { 25 | let shape = dedup_shape(shape); 26 | shape.borrow_mut().translate(*tx, *ty as f32); 27 | Value::Shape(shape.clone()) 28 | }, 29 | [Value::Integer(tx), Value::Integer(ty), Value::Gradient(grad)] => { 30 | let mut grad = grad.clone(); 31 | grad.translate(*tx as f32, *ty as f32); 32 | Value::Gradient(grad) 33 | }, 34 | [Value::Float(tx), Value::Float(ty), Value::Gradient(grad)] => { 35 | let mut grad = grad.clone(); 36 | grad.translate(*tx, *ty); 37 | Value::Gradient(grad) 38 | }, 39 | [Value::Integer(tx), Value::Float(ty), Value::Gradient(grad)] => { 40 | let mut grad = grad.clone(); 41 | grad.translate(*tx as f32, *ty); 42 | Value::Gradient(grad) 43 | }, 44 | [Value::Float(tx), Value::Integer(ty), Value::Gradient(grad)] => { 45 | let mut grad = grad.clone(); 46 | grad.translate(*tx, *ty as f32); 47 | Value::Gradient(grad) 48 | }, 49 | }); 50 | 51 | builtin_function!(translatex => { 52 | [Value::Integer(tx), Value::Shape(shape)] => { 53 | let shape = dedup_shape(shape); 54 | shape.borrow_mut().translate(*tx as f32, 0.0); 55 | Value::Shape(shape.clone()) 56 | }, 57 | [Value::Float(tx), Value::Shape(shape)] => { 58 | let shape = dedup_shape(shape); 59 | shape.borrow_mut().translate(*tx, 0.0); 60 | Value::Shape(shape.clone()) 61 | }, 62 | [Value::Integer(tx), Value::Gradient(grad)] => { 63 | let mut grad = grad.clone(); 64 | grad.translate(*tx as f32, 0.0); 65 | Value::Gradient(grad) 66 | }, 67 | [Value::Float(tx), Value::Gradient(grad)] => { 68 | let mut grad = grad.clone(); 69 | grad.translate(*tx, 0.0); 70 | Value::Gradient(grad) 71 | }, 72 | }); 73 | 74 | builtin_function!(translatey => { 75 | [Value::Integer(ty), Value::Shape(shape)] => { 76 | let shape = dedup_shape(shape); 77 | shape.borrow_mut().translate(0.0, *ty as f32); 78 | Value::Shape(shape.clone()) 79 | }, 80 | [Value::Float(ty), Value::Shape(shape)] => { 81 | let shape = dedup_shape(shape); 82 | shape.borrow_mut().translate(0.0, *ty); 83 | Value::Shape(shape.clone()) 84 | }, 85 | [Value::Integer(ty), Value::Gradient(grad)] => { 86 | let mut grad = grad.clone(); 87 | grad.translate(0.0, *ty as f32); 88 | Value::Gradient(grad) 89 | }, 90 | [Value::Float(ty), Value::Gradient(grad)] => { 91 | let mut grad = grad.clone(); 92 | grad.translate(0.0, *ty); 93 | Value::Gradient(grad) 94 | }, 95 | }); 96 | 97 | builtin_function!(translateb => { 98 | [Value::Integer(t), Value::Shape(shape)] => { 99 | let shape = dedup_shape(shape); 100 | shape.borrow_mut().translate(*t as f32, *t as f32); 101 | Value::Shape(shape.clone()) 102 | }, 103 | [Value::Float(t), Value::Shape(shape)] => { 104 | let shape = dedup_shape(shape); 105 | shape.borrow_mut().translate(*t, *t); 106 | Value::Shape(shape.clone()) 107 | }, 108 | [Value::Integer(t), Value::Gradient(grad)] => { 109 | let mut grad = grad.clone(); 110 | grad.translate(*t as f32, *t as f32); 111 | Value::Gradient(grad) 112 | }, 113 | [Value::Float(t), Value::Gradient(grad)] => { 114 | let mut grad = grad.clone(); 115 | grad.translate(*t, *t); 116 | Value::Gradient(grad) 117 | }, 118 | }); 119 | 120 | builtin_function!(rotate => { 121 | [Value::Integer(r), Value::Shape(shape)] => { 122 | let shape = dedup_shape(shape); 123 | shape.borrow_mut().rotate(*r as f32); 124 | Value::Shape(shape.clone()) 125 | }, 126 | [Value::Float(r), Value::Shape(shape)] => { 127 | let shape = dedup_shape(shape); 128 | shape.borrow_mut().rotate(*r); 129 | Value::Shape(shape.clone()) 130 | }, 131 | [Value::Integer(r), Value::Gradient(grad)] => { 132 | let mut grad = grad.clone(); 133 | grad.rotate(*r as f32); 134 | Value::Gradient(grad) 135 | }, 136 | [Value::Float(r), Value::Gradient(grad)] => { 137 | let mut grad = grad.clone(); 138 | grad.rotate(*r); 139 | Value::Gradient(grad) 140 | }, 141 | }); 142 | 143 | builtin_function!(rotate_at => { 144 | [r, tx, ty, Value::Shape(shape)] => { 145 | let shape = dedup_shape(shape); 146 | let r = match r { 147 | Value::Integer(n) => *n as f32, 148 | Value::Float(n) => *n, 149 | _ => return Err(Error::InvalidArgument("rotate_at".into())), 150 | }; 151 | let tx = match tx { 152 | Value::Integer(n) => *n as f32, 153 | Value::Float(n) => *n, 154 | _ => return Err(Error::InvalidArgument("rotate_at".into())), 155 | }; 156 | let ty = match ty { 157 | Value::Integer(n) => *n as f32, 158 | Value::Float(n) => *n, 159 | _ => return Err(Error::InvalidArgument("rotate_at".into())), 160 | }; 161 | shape.borrow_mut().rotate_at(r, tx, ty); 162 | Value::Shape(shape.clone()) 163 | }, 164 | [r, tx, ty, Value::Gradient(grad)] => { 165 | let mut grad = grad.clone(); 166 | let r = match r { 167 | Value::Integer(n) => *n as f32, 168 | Value::Float(n) => *n, 169 | _ => return Err(Error::InvalidArgument("rotate_at".into())), 170 | }; 171 | let tx = match tx { 172 | Value::Integer(n) => *n as f32, 173 | Value::Float(n) => *n, 174 | _ => return Err(Error::InvalidArgument("rotate_at".into())), 175 | }; 176 | let ty = match ty { 177 | Value::Integer(n) => *n as f32, 178 | Value::Float(n) => *n, 179 | _ => return Err(Error::InvalidArgument("rotate_at".into())), 180 | }; 181 | grad.rotate_at(r, tx, ty); 182 | Value::Gradient(grad) 183 | }, 184 | }); 185 | 186 | builtin_function!(scale => { 187 | [Value::Integer(sx), Value::Integer(sy), Value::Shape(shape)] => { 188 | let shape = dedup_shape(shape); 189 | shape.borrow_mut().scale(*sx as f32, *sy as f32); 190 | Value::Shape(shape.clone()) 191 | }, 192 | [Value::Float(sx), Value::Float(sy), Value::Shape(shape)] => { 193 | let shape = dedup_shape(shape); 194 | shape.borrow_mut().scale(*sx, *sy); 195 | Value::Shape(shape.clone()) 196 | }, 197 | [Value::Integer(sx), Value::Float(sy), Value::Shape(shape)] => { 198 | let shape = dedup_shape(shape); 199 | shape.borrow_mut().scale(*sx as f32, *sy); 200 | Value::Shape(shape.clone()) 201 | }, 202 | [Value::Float(sx), Value::Integer(sy), Value::Shape(shape)] => { 203 | let shape = dedup_shape(shape); 204 | shape.borrow_mut().scale(*sx, *sy as f32); 205 | Value::Shape(shape.clone()) 206 | }, 207 | [Value::Integer(sx), Value::Integer(sy), Value::Gradient(grad)] => { 208 | let mut grad = grad.clone(); 209 | grad.scale(*sx as f32, *sy as f32); 210 | Value::Gradient(grad) 211 | }, 212 | [Value::Float(sx), Value::Float(sy), Value::Gradient(grad)] => { 213 | let mut grad = grad.clone(); 214 | grad.scale(*sx, *sy); 215 | Value::Gradient(grad) 216 | }, 217 | [Value::Integer(sx), Value::Float(sy), Value::Gradient(grad)] => { 218 | let mut grad = grad.clone(); 219 | grad.scale(*sx as f32, *sy); 220 | Value::Gradient(grad) 221 | }, 222 | [Value::Float(sx), Value::Integer(sy), Value::Gradient(grad)] => { 223 | let mut grad = grad.clone(); 224 | grad.scale(*sx, *sy as f32); 225 | Value::Gradient(grad) 226 | }, 227 | }); 228 | 229 | builtin_function!(scalex => { 230 | [Value::Integer(sx), Value::Shape(shape)] => { 231 | let shape = dedup_shape(shape); 232 | shape.borrow_mut().scale(*sx as f32, 1.0); 233 | Value::Shape(shape.clone()) 234 | }, 235 | [Value::Float(sx), Value::Shape(shape)] => { 236 | let shape = dedup_shape(shape); 237 | shape.borrow_mut().scale(*sx, 1.0); 238 | Value::Shape(shape.clone()) 239 | }, 240 | [Value::Integer(sx), Value::Gradient(grad)] => { 241 | let mut grad = grad.clone(); 242 | grad.scale(*sx as f32, 1.0); 243 | Value::Gradient(grad) 244 | }, 245 | [Value::Float(sx), Value::Gradient(grad)] => { 246 | let mut grad = grad.clone(); 247 | grad.scale(*sx, 1.0); 248 | Value::Gradient(grad) 249 | }, 250 | }); 251 | 252 | builtin_function!(scaley => { 253 | [Value::Integer(sy), Value::Shape(shape)] => { 254 | let shape = dedup_shape(shape); 255 | shape.borrow_mut().scale(1.0, *sy as f32); 256 | Value::Shape(shape.clone()) 257 | }, 258 | [Value::Float(sy), Value::Shape(shape)] => { 259 | let shape = dedup_shape(shape); 260 | shape.borrow_mut().scale(1.0, *sy); 261 | Value::Shape(shape.clone()) 262 | }, 263 | [Value::Integer(sy), Value::Gradient(grad)] => { 264 | let mut grad = grad.clone(); 265 | grad.scale(1.0, *sy as f32); 266 | Value::Gradient(grad) 267 | }, 268 | [Value::Float(sy), Value::Gradient(grad)] => { 269 | let mut grad = grad.clone(); 270 | grad.scale(1.0, *sy); 271 | Value::Gradient(grad) 272 | }, 273 | }); 274 | 275 | builtin_function!(scaleb => { 276 | [Value::Integer(s), Value::Shape(shape)] => { 277 | let shape = dedup_shape(shape); 278 | 279 | shape.borrow_mut().scale(*s as f32, *s as f32); 280 | Value::Shape(shape) 281 | }, 282 | [Value::Float(s), Value::Shape(shape)] => { 283 | let shape = dedup_shape(shape); 284 | shape.borrow_mut().scale(*s, *s); 285 | Value::Shape(shape.clone()) 286 | }, 287 | [Value::Integer(s), Value::Gradient(grad)] => { 288 | let mut grad = grad.clone(); 289 | grad.scale(*s as f32, *s as f32); 290 | Value::Gradient(grad) 291 | }, 292 | [Value::Float(s), Value::Gradient(grad)] => { 293 | let mut grad = grad.clone(); 294 | grad.scale(*s, *s); 295 | Value::Gradient(grad) 296 | }, 297 | }); 298 | 299 | builtin_function!(skew => { 300 | [Value::Integer(kx), Value::Integer(ky), Value::Shape(shape)] => { 301 | let shape = dedup_shape(shape); 302 | shape.borrow_mut().skew(*kx as f32, *ky as f32); 303 | Value::Shape(shape.clone()) 304 | }, 305 | [Value::Float(kx), Value::Float(ky), Value::Shape(shape)] => { 306 | let shape = dedup_shape(shape); 307 | shape.borrow_mut().skew(*kx, *ky); 308 | Value::Shape(shape.clone()) 309 | }, 310 | [Value::Integer(kx), Value::Float(ky), Value::Shape(shape)] => { 311 | let shape = dedup_shape(shape); 312 | shape.borrow_mut().skew(*kx as f32, *ky); 313 | Value::Shape(shape.clone()) 314 | }, 315 | [Value::Float(kx), Value::Integer(ky), Value::Shape(shape)] => { 316 | let shape = dedup_shape(shape); 317 | shape.borrow_mut().skew(*kx, *ky as f32); 318 | Value::Shape(shape.clone()) 319 | }, 320 | [Value::Integer(kx), Value::Integer(ky), Value::Gradient(grad)] => { 321 | let mut grad = grad.clone(); 322 | grad.skew(*kx as f32, *ky as f32); 323 | Value::Gradient(grad) 324 | }, 325 | [Value::Float(kx), Value::Float(ky), Value::Gradient(grad)] => { 326 | let mut grad = grad.clone(); 327 | grad.skew(*kx, *ky); 328 | Value::Gradient(grad) 329 | }, 330 | [Value::Integer(kx), Value::Float(ky), Value::Gradient(grad)] => { 331 | let mut grad = grad.clone(); 332 | grad.skew(*kx as f32, *ky); 333 | Value::Gradient(grad) 334 | }, 335 | [Value::Float(kx), Value::Integer(ky), Value::Gradient(grad)] => { 336 | let mut grad = grad.clone(); 337 | grad.skew(*kx, *ky as f32); 338 | Value::Gradient(grad) 339 | }, 340 | }); 341 | 342 | builtin_function!(skewx => { 343 | [Value::Integer(kx), Value::Shape(shape)] => { 344 | let shape = dedup_shape(shape); 345 | shape.borrow_mut().skew(*kx as f32, 0.0); 346 | Value::Shape(shape.clone()) 347 | }, 348 | [Value::Float(kx), Value::Shape(shape)] => { 349 | let shape = dedup_shape(shape); 350 | shape.borrow_mut().skew(*kx, 0.0); 351 | Value::Shape(shape.clone()) 352 | }, 353 | [Value::Integer(kx), Value::Gradient(grad)] => { 354 | let mut grad = grad.clone(); 355 | grad.skew(*kx as f32, 0.0); 356 | Value::Gradient(grad) 357 | }, 358 | [Value::Float(kx), Value::Gradient(grad)] => { 359 | let mut grad = grad.clone(); 360 | grad.skew(*kx, 0.0); 361 | Value::Gradient(grad) 362 | }, 363 | }); 364 | 365 | builtin_function!(skewy => { 366 | [Value::Integer(ky), Value::Shape(shape)] => { 367 | let shape = dedup_shape(shape); 368 | shape.borrow_mut().skew(0.0, *ky as f32); 369 | Value::Shape(shape.clone()) 370 | }, 371 | [Value::Float(ky), Value::Shape(shape)] => { 372 | let shape = dedup_shape(shape); 373 | shape.borrow_mut().skew(0.0, *ky); 374 | Value::Shape(shape.clone()) 375 | }, 376 | [Value::Integer(ky), Value::Gradient(grad)] => { 377 | let mut grad = grad.clone(); 378 | grad.skew(0.0, *ky as f32); 379 | Value::Gradient(grad) 380 | }, 381 | [Value::Float(ky), Value::Gradient(grad)] => { 382 | let mut grad = grad.clone(); 383 | grad.skew(0.0, *ky); 384 | Value::Gradient(grad) 385 | }, 386 | }); 387 | 388 | builtin_function!(skewb => { 389 | [Value::Integer(k), Value::Shape(shape)] => { 390 | let shape = dedup_shape(shape); 391 | shape.borrow_mut().skew(*k as f32, *k as f32); 392 | Value::Shape(shape.clone()) 393 | }, 394 | [Value::Float(k), Value::Shape(shape)] => { 395 | let shape = dedup_shape(shape); 396 | shape.borrow_mut().skew(*k, *k); 397 | Value::Shape(shape.clone()) 398 | }, 399 | [Value::Integer(k), Value::Gradient(grad)] => { 400 | let mut grad = grad.clone(); 401 | grad.skew(*k as f32, *k as f32); 402 | Value::Gradient(grad) 403 | }, 404 | [Value::Float(k), Value::Gradient(grad)] => { 405 | let mut grad = grad.clone(); 406 | grad.skew(*k, *k); 407 | Value::Gradient(grad) 408 | }, 409 | }); 410 | 411 | builtin_function!(flip => { 412 | [Value::Integer(f), Value::Shape(shape)] => { 413 | let shape = dedup_shape(shape); 414 | shape.borrow_mut().flip(*f as f32); 415 | Value::Shape(shape.clone()) 416 | }, 417 | [Value::Float(f), Value::Shape(shape)] => { 418 | let shape = dedup_shape(shape); 419 | shape.borrow_mut().flip(*f); 420 | Value::Shape(shape.clone()) 421 | }, 422 | [Value::Integer(f), Value::Gradient(grad)] => { 423 | let mut grad = grad.clone(); 424 | grad.flip(*f as f32); 425 | Value::Gradient(grad) 426 | }, 427 | [Value::Float(f), Value::Gradient(grad)] => { 428 | let mut grad = grad.clone(); 429 | grad.flip(*f); 430 | Value::Gradient(grad) 431 | }, 432 | }); 433 | 434 | builtin_function!(fliph => { 435 | [Value::Shape(shape)] => { 436 | let shape = dedup_shape(shape); 437 | shape.borrow_mut().fliph(); 438 | Value::Shape(shape.clone()) 439 | }, 440 | [Value::Gradient(grad)] => { 441 | let mut grad = grad.clone(); 442 | grad.fliph(); 443 | Value::Gradient(grad) 444 | }, 445 | }); 446 | 447 | builtin_function!(flipv => { 448 | [Value::Shape(shape)] => { 449 | let shape = dedup_shape(shape); 450 | shape.borrow_mut().flipv(); 451 | Value::Shape(shape.clone()) 452 | }, 453 | [Value::Gradient(grad)] => { 454 | let mut grad = grad.clone(); 455 | grad.flipv(); 456 | Value::Gradient(grad) 457 | }, 458 | }); 459 | 460 | builtin_function!(flipd => { 461 | [Value::Shape(shape)] => { 462 | let shape = dedup_shape(shape); 463 | shape.borrow_mut().flipd(); 464 | Value::Shape(shape.clone()) 465 | }, 466 | [Value::Gradient(grad)] => { 467 | let mut grad = grad.clone(); 468 | grad.flipd(); 469 | Value::Gradient(grad) 470 | }, 471 | }); 472 | 473 | builtin_function!(zindex => { 474 | [Value::Integer(z), Value::Shape(shape)] => { 475 | let shape = dedup_shape(shape); 476 | shape.borrow_mut().set_zindex(*z as f32); 477 | Value::Shape(shape.clone()) 478 | }, 479 | [Value::Float(z), Value::Shape(shape)] => { 480 | let shape = dedup_shape(shape); 481 | shape.borrow_mut().set_zindex(*z); 482 | Value::Shape(shape.clone()) 483 | }, 484 | }); 485 | 486 | builtin_function!(zshift => { 487 | [Value::Integer(z), Value::Shape(shape)] => { 488 | let shape = dedup_shape(shape); 489 | shape.borrow_mut().shift_zindex(*z as f32); 490 | Value::Shape(shape.clone()) 491 | }, 492 | [Value::Float(z), Value::Shape(shape)] => { 493 | let shape = dedup_shape(shape); 494 | shape.borrow_mut().shift_zindex(*z); 495 | Value::Shape(shape.clone()) 496 | }, 497 | }); 498 | 499 | #[cfg(test)] 500 | mod tests { 501 | use super::*; 502 | 503 | #[cfg(feature = "std")] 504 | use std::rc::Rc; 505 | 506 | #[cfg(feature = "no-std")] 507 | use alloc::{rc::Rc, vec}; 508 | 509 | use crate::shape::{PathSegment, Shape}; 510 | use core::cell::RefCell; 511 | use rand::SeedableRng; 512 | 513 | #[test] 514 | fn test_translation_functions() { 515 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 516 | let data = Data::default(); 517 | 518 | // Create a test shape 519 | let shape = Rc::new(RefCell::new(Shape::path(vec![ 520 | PathSegment::MoveTo(0.0, 0.0), 521 | PathSegment::LineTo(10.0, 10.0), 522 | ]))); 523 | 524 | // Test translate with various input types 525 | let translate_tests = vec![ 526 | (Value::Integer(5), Value::Integer(10)), // int, int 527 | (Value::Float(5.0), Value::Float(10.0)), // float, float 528 | (Value::Integer(5), Value::Float(10.0)), // int, float 529 | (Value::Float(5.0), Value::Integer(10)), // float, int 530 | ]; 531 | 532 | for (tx, ty) in translate_tests { 533 | let result = 534 | translate(&mut rng, &data, &[tx, ty, Value::Shape(shape.clone())]).unwrap(); 535 | 536 | assert!(matches!(result, Value::Shape(_))); 537 | } 538 | 539 | // Test translatex 540 | let translated_x = translatex( 541 | &mut rng, 542 | &data, 543 | &[Value::Integer(5), Value::Shape(shape.clone())], 544 | ) 545 | .unwrap(); 546 | assert!(matches!(translated_x, Value::Shape(_))); 547 | 548 | // Test translatey 549 | let translated_y = translatey( 550 | &mut rng, 551 | &data, 552 | &[Value::Integer(5), Value::Shape(shape.clone())], 553 | ) 554 | .unwrap(); 555 | assert!(matches!(translated_y, Value::Shape(_))); 556 | 557 | // Test translateb 558 | let translated_both = translateb( 559 | &mut rng, 560 | &data, 561 | &[Value::Integer(5), Value::Shape(shape.clone())], 562 | ) 563 | .unwrap(); 564 | assert!(matches!(translated_both, Value::Shape(_))); 565 | } 566 | 567 | #[test] 568 | fn test_rotation_functions() { 569 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 570 | let data = Data::default(); 571 | 572 | // Create a test shape 573 | let shape = Rc::new(RefCell::new(Shape::path(vec![ 574 | PathSegment::MoveTo(0.0, 0.0), 575 | PathSegment::LineTo(10.0, 10.0), 576 | ]))); 577 | 578 | // Test basic rotation 579 | let rotated = rotate( 580 | &mut rng, 581 | &data, 582 | &[Value::Integer(45), Value::Shape(shape.clone())], 583 | ) 584 | .unwrap(); 585 | assert!(matches!(rotated, Value::Shape(_))); 586 | 587 | // Test rotation at point 588 | let rotated_at = rotate_at( 589 | &mut rng, 590 | &data, 591 | &[ 592 | Value::Integer(45), 593 | Value::Integer(5), 594 | Value::Integer(5), 595 | Value::Shape(shape.clone()), 596 | ], 597 | ) 598 | .unwrap(); 599 | assert!(matches!(rotated_at, Value::Shape(_))); 600 | } 601 | 602 | #[test] 603 | fn test_scale_functions() { 604 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 605 | let data = Data::default(); 606 | 607 | // Create a test shape 608 | let shape = Rc::new(RefCell::new(Shape::path(vec![ 609 | PathSegment::MoveTo(0.0, 0.0), 610 | PathSegment::LineTo(10.0, 10.0), 611 | ]))); 612 | 613 | // Test scale with various input types 614 | let scale_tests = vec![ 615 | (Value::Integer(2), Value::Integer(3)), // int, int 616 | (Value::Float(2.0), Value::Float(3.0)), // float, float 617 | (Value::Integer(2), Value::Float(3.0)), // int, float 618 | (Value::Float(2.0), Value::Integer(3)), // float, int 619 | ]; 620 | 621 | for (sx, sy) in scale_tests { 622 | let result = scale(&mut rng, &data, &[sx, sy, Value::Shape(shape.clone())]).unwrap(); 623 | 624 | assert!(matches!(result, Value::Shape(_))); 625 | } 626 | 627 | // Test scalex 628 | let scaled_x = scalex( 629 | &mut rng, 630 | &data, 631 | &[Value::Integer(2), Value::Shape(shape.clone())], 632 | ) 633 | .unwrap(); 634 | assert!(matches!(scaled_x, Value::Shape(_))); 635 | 636 | // Test scaley 637 | let scaled_y = scaley( 638 | &mut rng, 639 | &data, 640 | &[Value::Integer(2), Value::Shape(shape.clone())], 641 | ) 642 | .unwrap(); 643 | assert!(matches!(scaled_y, Value::Shape(_))); 644 | 645 | // Test scaleb 646 | let scaled_both = scaleb( 647 | &mut rng, 648 | &data, 649 | &[Value::Integer(2), Value::Shape(shape.clone())], 650 | ) 651 | .unwrap(); 652 | assert!(matches!(scaled_both, Value::Shape(_))); 653 | } 654 | 655 | #[test] 656 | fn test_skew_functions() { 657 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 658 | let data = Data::default(); 659 | 660 | // Create a test shape 661 | let shape = Rc::new(RefCell::new(Shape::path(vec![ 662 | PathSegment::MoveTo(0.0, 0.0), 663 | PathSegment::LineTo(10.0, 10.0), 664 | ]))); 665 | 666 | // Test skew with various input types 667 | let skew_tests = vec![ 668 | (Value::Integer(10), Value::Integer(20)), // int, int 669 | (Value::Float(10.0), Value::Float(20.0)), // float, float 670 | (Value::Integer(10), Value::Float(20.0)), // int, float 671 | (Value::Float(10.0), Value::Integer(20)), // float, int 672 | ]; 673 | 674 | for (kx, ky) in skew_tests { 675 | let result = skew(&mut rng, &data, &[kx, ky, Value::Shape(shape.clone())]).unwrap(); 676 | 677 | assert!(matches!(result, Value::Shape(_))); 678 | } 679 | 680 | // Test skewx 681 | let skewed_x = skewx( 682 | &mut rng, 683 | &data, 684 | &[Value::Integer(10), Value::Shape(shape.clone())], 685 | ) 686 | .unwrap(); 687 | assert!(matches!(skewed_x, Value::Shape(_))); 688 | 689 | // Test skewy 690 | let skewed_y = skewy( 691 | &mut rng, 692 | &data, 693 | &[Value::Integer(10), Value::Shape(shape.clone())], 694 | ) 695 | .unwrap(); 696 | assert!(matches!(skewed_y, Value::Shape(_))); 697 | 698 | // Test skewb 699 | let skewed_both = skewb( 700 | &mut rng, 701 | &data, 702 | &[Value::Integer(10), Value::Shape(shape.clone())], 703 | ) 704 | .unwrap(); 705 | assert!(matches!(skewed_both, Value::Shape(_))); 706 | } 707 | 708 | #[test] 709 | fn test_flip_functions() { 710 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 711 | let data = Data::default(); 712 | 713 | // Create a test shape 714 | let shape = Rc::new(RefCell::new(Shape::path(vec![ 715 | PathSegment::MoveTo(0.0, 0.0), 716 | PathSegment::LineTo(10.0, 10.0), 717 | ]))); 718 | 719 | // Test basic flip 720 | let flipped = flip( 721 | &mut rng, 722 | &data, 723 | &[Value::Integer(45), Value::Shape(shape.clone())], 724 | ) 725 | .unwrap(); 726 | assert!(matches!(flipped, Value::Shape(_))); 727 | 728 | // Test fliph 729 | let flipped_h = fliph(&mut rng, &data, &[Value::Shape(shape.clone())]).unwrap(); 730 | assert!(matches!(flipped_h, Value::Shape(_))); 731 | 732 | // Test flipv 733 | let flipped_v = flipv(&mut rng, &data, &[Value::Shape(shape.clone())]).unwrap(); 734 | assert!(matches!(flipped_v, Value::Shape(_))); 735 | 736 | // Test flipd 737 | let flipped_d = flipd(&mut rng, &data, &[Value::Shape(shape.clone())]).unwrap(); 738 | assert!(matches!(flipped_d, Value::Shape(_))); 739 | } 740 | 741 | #[test] 742 | fn test_zindex_functions() { 743 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 744 | let data = Data::default(); 745 | 746 | // Create a test shape 747 | let shape = Rc::new(RefCell::new(Shape::path(vec![ 748 | PathSegment::MoveTo(0.0, 0.0), 749 | PathSegment::LineTo(10.0, 10.0), 750 | ]))); 751 | 752 | // Test zindex 753 | let zindexed = zindex( 754 | &mut rng, 755 | &data, 756 | &[Value::Integer(5), Value::Shape(shape.clone())], 757 | ) 758 | .unwrap(); 759 | assert!(matches!(zindexed, Value::Shape(_))); 760 | 761 | // Test zshift 762 | let zshifted = zshift( 763 | &mut rng, 764 | &data, 765 | &[Value::Integer(5), Value::Shape(shape.clone())], 766 | ) 767 | .unwrap(); 768 | assert!(matches!(zshifted, Value::Shape(_))); 769 | } 770 | 771 | #[test] 772 | fn test_invalid_inputs() { 773 | let mut rng = ChaCha8Rng::from_seed([0; 32]); 774 | let data = Data::default(); 775 | 776 | // Create a test shape 777 | let shape = Rc::new(RefCell::new(Shape::path(vec![ 778 | PathSegment::MoveTo(0.0, 0.0), 779 | PathSegment::LineTo(10.0, 10.0), 780 | ]))); 781 | 782 | // Test with invalid shape argument 783 | assert!(translate( 784 | &mut rng, 785 | &data, 786 | &[Value::Integer(5), Value::Integer(10), Value::Integer(0)] 787 | ) 788 | .is_err()); 789 | 790 | // Test with wrong number of arguments 791 | assert!(translate(&mut rng, &data, &[Value::Integer(5)]).is_err()); 792 | 793 | // Test with invalid rotation angle type 794 | assert!(rotate( 795 | &mut rng, 796 | &data, 797 | &[Value::String("45".into()), Value::Shape(shape.clone())] 798 | ) 799 | .is_err()); 800 | } 801 | } 802 | --------------------------------------------------------------------------------