diff --git a/Cargo.toml b/Cargo.toml index 84db4e1..cc72574 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "apiel", "apiel-cli", + "apiel-wasm", ] [workspace.dependencies] diff --git a/README.md b/README.md index d912522..f4f9e4f 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,188 @@ -# apiel -Apiel is a small subset of the [APL programming language](https://en.wikipedia.org/wiki/APL_(programming_language)) implemented in Rust. - -The ultimate goal of the project is to export a macro that allows evaluating APL expressions from Rust code, providing a way to solve some problems in a very conscise manner. - -## Array languages - -APL was the first language in an "Array programming" or "Iversonian" paradigm. These languages are closer to mathematical notation than to C-like programming languages. The concepts proposed by APL inspired many similar languages, influenced the development of the functional programming paradigm, and had a giant impact on programming as a whole. - -## Approach - -The project utilizes [Yacc](https://en.wikipedia.org/wiki/Yacc) **(Yet-Another-Compiler-Compiler)** implementation in Rust through [grmtools](https://github.com/softdevteam/grmtools) to build the lexer and parser. - -`apiel.l` contains the tokens for the **lexer**, `apiel.y` describes the **Yacc grammar**. The build.rs generaters Rust code for the lexer and parser generator. - -My main entry point is apiel/src/parse/mod.rs. There is `fn parse_and_evaluate()` that runs `parse::eval()` (located in /parse/eval.rs) on the expression passed to it. The `parse::eval()` contains a single match expression that performs operations on the data contained in the `Expr` enumeration according to the expression type (it always calls parse::eval() recursively for `lhs` and `rhs` of the expression). The `Expr` enumeration is defined in `aliel.y`. Ech variant of the Expr usually contains a `Span` identifying where it's located in the original input, and boxed arguments, which allows for unlimited recursion inside the expression. - -## Usage - -```cargo run``` or ```RUST_LOG=debug cargo run``` for debugging output. - -Enter commands in the terminal. - -List of supported glyphs and operations: - -| Glyph | Monadic operation | Impl. | Dyadic operation | Impl. -| --- | ---------------- | ----------- | ----------- | ----------- | -| + | Conjugate | ✅* | Addition | ✅ -| - | Negate | ✅ | Subtraction | ✅ -| × | Direction | ✅ | Multiplication | ✅ -| ÷ | Reciprocal | ✅ | Division | ✅ -| * | Exponentiation | ✅ | Raising to power | ✅ -| ⍟ | Natural logarithm | ✅ | Logarithm | ✅ -| ⌹ | Matrix inverse | ✅ | Matrix divide | ✅ -| ○ | Pi Multiple | ✅ | Circular functions | ✅ -| ! | Factorial | ✅ | Binomial | ✅ -| ? | Roll | ✅ | Deal | ✅ -| \| | Magnitude | ✅ | Residue | ✅ -| ⌈ | Ceil | ✅ | Maximum | ✅ -| ⌊ | Floor | ✅ | Minimum | ✅ -| ⍳ | Generate index | ✅ | Index of | ✅ -| ⍸ | Where | ✅ | Interval index | ✅ -| / | - | - | Replicate | ✅ -| / | - | - | Reduce | ✅ -| \ | - | - | Expand | ✅ -| \ | - | - | Scan | ✅ -| , | Ravel | ✅ | Catenate | ✅ -| ⍴ | Shape | ✅ | Reshape | ✅ -| ⌽ | Reverse | ✅ | Rotate | ✅ -| ⍉ | Transpose | ✅ | - | - -| = | - | - | Equality | ✅ -| ≠ | - | - | Not Equal | ✅ -| < | - | - | Less Than | ✅ -| > | - | - | Greater Than | ✅ -| ≤ | - | - | Less or Equal | ✅ -| ≥ | - | - | Greater or Equal | ✅ -| ∧ | - | - | And | ✅ -| ∨ | - | - | Or | ✅ -| ⍲ | - | - | Nand | ✅ -| ⍱ | - | - | Nor | ✅ -| ↑ | - | - | Take | ✅ -| ↓ | - | - | Drop | ✅ -| ⍋ | Grade Up | ✅ | - | - -| ⍒ | Grade Down | ✅ | - | - -| ¯ | High minus (negative literal) | ✅ | - | - -| ∘. | - | - | Outer Product | ✅ -| f.g | - | - | Inner Product | ✅ -| ← | - | - | Assignment | ✅ -| {⍵} | - | - | Dfns (lambdas) | ✅ -| ⍵ ⍺ | Right/Left arg | ✅ | - | - -| ∇ | Self-reference | ✅ | - | - -| ⋄ : | Guards / Statements | ✅ | - | - -| ⊃ | First | ✅ | - | - -| ∪ | Unique | ✅ | Union | ✅ -| ∩ | - | - | Intersection | ✅ -| ~ | Not | ✅ | Without | ✅ -| ⊥ | - | - | Decode | ✅ -| ⊤ | - | - | Encode | ✅ -| ⌷ | - | - | Index | ✅ -| ⊂ | Enclose | ✅ | - | - -| ⊃ | First / Disclose | ✅ | - | - -| ⊆ | - | - | Partition | ✅ -| ¨ | Each (monadic) | ✅ | Each (dyadic) | ✅ -| '' | String literals | ✅ | - | - - -- \* - Not implemented for complex numbers - -## Usage examples - -``` ->>> 5 25 125 ÷ 5 -1 5 25 ->>> 1 2 3 + 4 5 6 -5 7 9 ->>> - 1 2 3 -¯1 ¯2 ¯3 ->>> 1 2 3 * 2 4 6 -1 16 729 ->>> 10 ⍟ 100 -2 ->>> ⍳ 5 -1 2 3 4 5 ->>> +/ ⍳ 10 -55 ->>> 2 3 ⍴ ⍳ 6 -1 2 3 4 5 6 ->>> ⍴ 2 3 ⍴ ⍳ 6 -2 3 ->>> ⌽ 1 2 3 4 5 -5 4 3 2 1 ->>> 1 2 3 = 1 3 3 -1 0 1 ->>> 5 ⍴ 1 2 -1 2 1 2 1 -``` - -## Affiliation - -This was implemented as my capstone project for the [rustcamp](https://github.com/rust-lang-ua/rustcamp), a Rust bootcamp organized by the Ukrainian Rust Community ([website](https://www.uarust.com), [linked in](https://www.linkedin.com/company/ukrainian-rust-community), [telegram](https://t.me/rustlang_ua), [github](https://github.com/rust-lang-ua), [youtube](https://www.youtube.com/channel/UCmkAFUu2MVOX8ly0LjB6TMA), [twitter](https://twitter.com/rustukraine)). +# apiel + +Apiel is a subset of the [APL programming language](https://en.wikipedia.org/wiki/APL_(programming_language)) implemented in Rust. + +The project exports a macro (`apl!`) for evaluating APL expressions from Rust code, and a CLI (`apiel-cli`) for interactive use. + +## Array Languages + +APL was the first language in an "Array programming" or "Iversonian" paradigm. These languages are closer to mathematical notation than to C-like programming languages. The concepts proposed by APL inspired many similar languages, influenced the development of the functional programming paradigm, and had a giant impact on programming as a whole. + +## Approach + +The project utilizes [Yacc](https://en.wikipedia.org/wiki/Yacc) **(Yet-Another-Compiler-Compiler)** implementation in Rust through [grmtools](https://github.com/softdevteam/grmtools) to build the lexer and parser. + +`apiel.l` contains the tokens for the **lexer**, `apiel.y` describes the **Yacc grammar**. The `build.rs` generates Rust code for the lexer and parser. The evaluator in `parse/eval.rs` is a recursive match over the `Expr` AST. + +Function trains are handled via token-level preprocessing: parenthesized groups of function references are detected by the lexer and rewritten to dfn expressions before parsing. + +## Usage + +### CLI + +``` +cargo run -p apiel-cli +``` + +or `RUST_LOG=debug cargo run -p apiel-cli` for debugging output. + +### Library + +```rust +use apiel::apl; + +let result = apl!("+/ ⍳ 10").unwrap(); // [55.0] + +// Pass data from Rust +let result = apl!("⍺ × ⍵", alpha: &[10.0], omega: &[1.0, 2.0, 3.0]).unwrap(); + +// Persistent environment +let mut env = apiel::Env::new(); +apl!("data←⍳ 10", &mut env).unwrap(); +apl!("+/ data", &mut env).unwrap(); // [55.0] +``` + +## Supported Glyphs and Operations + +### Scalar Functions + +| Glyph | Monadic operation | Impl. | Dyadic operation | Impl. | +| --- | --- | --- | --- | --- | +| + | Conjugate | ✅* | Addition | ✅ | +| - | Negate | ✅ | Subtraction | ✅ | +| × | Direction (signum) | ✅ | Multiplication | ✅ | +| ÷ | Reciprocal | ✅ | Division | ✅ | +| * | Exponential | ✅ | Power | ✅ | +| ⍟ | Natural logarithm | ✅ | Logarithm | ✅ | +| ○ | Pi multiple | ✅ | Circular functions | ✅ | +| ! | Factorial | ✅ | Binomial | ✅ | +| ? | Roll | ✅ | Deal | ✅ | +| \| | Magnitude | ✅ | Residue | ✅ | +| ⌈ | Ceiling | ✅ | Maximum | ✅ | +| ⌊ | Floor | ✅ | Minimum | ✅ | +| ⌹ | Matrix inverse | ✅ | Matrix divide | ✅ | + +\* Not implemented for complex numbers + +### Array Functions + +| Glyph | Monadic operation | Impl. | Dyadic operation | Impl. | +| --- | --- | --- | --- | --- | +| ⍳ | Index generate | ✅ | Index of | ✅ | +| ⍸ | Where | ✅ | Interval index | ✅ | +| ⍴ | Shape | ✅ | Reshape | ✅ | +| , | Ravel | ✅ | Catenate | ✅ | +| ⌽ | Reverse | ✅ | Rotate | ✅ | +| ⍉ | Transpose | ✅ | Dyadic transpose | ✅ | +| ↑ | Mix | ✅ | Take | ✅ | +| ↓ | Split | ✅ | Drop | ✅ | +| ⍋ | Grade Up | ✅ | - | - | +| ⍒ | Grade Down | ✅ | - | - | +| ⊂ | Enclose | ✅ | Partitioned enclose | ✅ | +| ⊃ | First / Disclose | ✅ | - | - | +| ⊆ | - | - | Partition | ✅ | +| ⌷ | - | - | Index | ✅ | +| ⍷ | - | - | Find | ✅ | + +### Selection and Set Functions + +| Glyph | Monadic operation | Impl. | Dyadic operation | Impl. | +| --- | --- | --- | --- | --- | +| ∪ | Unique | ✅ | Union | ✅ | +| ∩ | - | - | Intersection | ✅ | +| ~ | Not | ✅ | Without | ✅ | +| ⊣ | Same (identity) | ✅ | Left | ✅ | +| ⊢ | Same (identity) | ✅ | Right | ✅ | +| ≡ | Depth | ✅ | Match | ✅ | +| ≢ | Tally | ✅ | Not Match | ✅ | + +### Comparison and Logic + +| Glyph | Monadic operation | Impl. | Dyadic operation | Impl. | +| --- | --- | --- | --- | --- | +| = | - | - | Equal | ✅ | +| ≠ | - | - | Not Equal | ✅ | +| < | - | - | Less Than | ✅ | +| > | - | - | Greater Than | ✅ | +| ≤ | - | - | Less or Equal | ✅ | +| ≥ | - | - | Greater or Equal | ✅ | +| ∧ | - | - | And | ✅ | +| ∨ | - | - | Or | ✅ | +| ⍲ | - | - | Nand | ✅ | +| ⍱ | - | - | Nor | ✅ | + +### Encoding + +| Glyph | Monadic operation | Impl. | Dyadic operation | Impl. | +| --- | --- | --- | --- | --- | +| ⊥ | - | - | Decode | ✅ | +| ⊤ | - | - | Encode | ✅ | + +### Operators (Higher-Order) + +| Glyph | Name | Impl. | Description | +| --- | --- | --- | --- | +| f/ | Reduce | ✅ | Right fold: `+/ 1 2 3` = 6 | +| f\ | Scan | ✅ | Cumulative fold: `+\ 1 2 3` = `1 3 6` | +| ∘.f | Outer Product | ✅ | All pairs: `1 2 ∘.× 3 4` | +| f.g | Inner Product | ✅ | Generalized matrix multiply | +| f¨ | Each | ✅ | Apply to each element | +| f⍨ | Commute / Selfie | ✅ | `A f⍨ B` = `B f A`; `f⍨ B` = `B f B` | +| f⍣n | Power | ✅ | Apply f n times | +| {f}∘{g} | Compose | ✅ | `f(g(⍵))` | +| {f}⍥{g} | Over | ✅ | Monadic: `f(g(⍵))`; Dyadic: `(g ⍺) f (g ⍵)` | +| {f}⍤k | Rank | ✅ | Apply f to each rank-k cell | +| {f}@i | At | ✅ | Apply f at specified indices | +| {f}⌸ | Key | ✅ | Group-by: apply f to each group | +| (f g h) | Fork (3-train) | ✅ | `(f ⍵) g (h ⍵)` -- e.g. `(+/ ÷ ≢)` for average | +| (f g) | Atop (2-train) | ✅ | `f (g ⍵)` | + +Reduce, scan, outer product, inner product, and each work with all 20 primitive operators. + +### Language Features + +| Feature | Impl. | Description | +| --- | --- | --- | +| ← Assignment | ✅ | Variable binding | +| x+←1 Modified assignment | ✅ | `x←x+1` shorthand, works with all operators | +| x[i]←v Indexed assignment | ✅ | Modify elements at 1-based indices | +| {⍵} Dfns (lambdas) | ✅ | Anonymous functions with `⍵` (right) and `⍺` (left) args | +| f←{⍵} Named functions | ✅ | Store and call functions by name | +| ∇ Self-reference | ✅ | Recursive calls within dfns | +| ⋄ : Guards / Statements | ✅ | Multi-branch conditionals and sequential execution | +| ¯ High minus | ✅ | Negative number literals | +| '...' Strings | ✅ | Character vectors | +| Nested arrays | ✅ | Arrays containing arrays via `⊂` | +| N-dimensional arrays | ✅ | Any rank via `⍴` reshape | +| Scalar extension | ✅ | Auto-broadcast scalars to arrays | + +### Examples + +``` +>>> (+/ ÷ ≢) 2 4 6 8 10 +6 +>>> (⌈/ - ⌊/) 3 1 4 1 5 9 +8 +>>> +⍨ 1 2 3 +2 4 6 +>>> {⍵+1}⍣3 ⍳ 5 +4 5 6 7 8 +>>> 2 3 ⍴ ⍳ 6 +1 2 3 4 5 6 +>>> ⍴ 2 3 ⍴ ⍳ 6 +2 3 +>>> ⌽ 1 2 3 4 5 +5 4 3 2 1 +>>> 1 2 3 = 1 3 3 +1 0 1 +>>> {⍵<2: ⍵ ⋄ (∇ ⍵-1)+∇ ⍵-2} 10 +55 +>>> ∧/ 1 1 1 0 +0 +>>> {≢⍵}⌸ 1 1 2 3 3 3 +2 1 3 +``` + +## Affiliation + +This was implemented as my capstone project for the [rustcamp](https://github.com/rust-lang-ua/rustcamp), a Rust bootcamp organized by the Ukrainian Rust Community ([website](https://www.uarust.com), [linked in](https://www.linkedin.com/company/ukrainian-rust-community), [telegram](https://t.me/rustlang_ua), [github](https://github.com/rust-lang-ua), [youtube](https://www.youtube.com/channel/UCmkAFUu2MVOX8ly0LjB6TMA), [twitter](https://twitter.com/rustukraine)). diff --git a/apiel-cli/Cargo.toml b/apiel-cli/Cargo.toml index 25f83df..a0e2010 100644 --- a/apiel-cli/Cargo.toml +++ b/apiel-cli/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "apiel-cli" -version = "0.2.0" +version = "0.3.0" authors = ["Mark Firman "] edition = "2024" description = "Interactive REPL for apiel, a subset of the APL programming language implemented in Rust." keywords = ["apl", "array", "repl", "interpreter"] categories = ["command-line-utilities", "mathematics"] homepage = "https://github.com/NamesMark/apiel" -repository = "https://github.com/NamesMark/apiel/apiel-cli" +repository = "https://github.com/NamesMark/apiel/tree/main/apiel-cli" license = "MIT" [[bin]] @@ -15,7 +15,7 @@ doc = false name = "apiel-cli" [dependencies] -apiel = { version = "0.2.0", path = "../apiel" } +apiel = { version = "0.3.0", path = "../apiel" } tracing-subscriber.workspace = true [dev-dependencies] diff --git a/apiel-cli/README.md b/apiel-cli/README.md index 3ed767a..bebff34 100644 --- a/apiel-cli/README.md +++ b/apiel-cli/README.md @@ -1,16 +1,44 @@ -# apiel -This is the cli tool to showcase **apiel**. - -**apiel** is a small subset of the [APL programming language](https://en.wikipedia.org/wiki/APL_(programming_language)) implemented in Rust. - -## Affiliation - -This was created as a capstone project for the [rustcamp](https://github.com/rust-lang-ua/rustcamp), a Rust bootcamp organized by the Ukrainian Rust Community ([website](https://www.uarust.com), [linked in](https://www.linkedin.com/company/ukrainian-rust-community), [telegram](https://t.me/rustlang_ua), [github](https://github.com/rust-lang-ua), [youtube](https://www.youtube.com/channel/UCmkAFUu2MVOX8ly0LjB6TMA), [twitter](https://twitter.com/rustukraine)). - -## Usage - -```cargo run``` or ```RUST_LOG=debug cargo run``` for debugging output. - -Enter commands in the terminal. - -The list of supported glyphs can be found in the [main README](../README.md). +# apiel-cli + +Interactive REPL for [apiel](https://crates.io/crates/apiel), a subset of the APL programming language implemented in Rust. + +## Install + +``` +cargo install apiel-cli +``` + +## Usage + +``` +$ apiel-cli +>>> ⍳ 5 +1 2 3 4 5 +>>> +/ ⍳ 10 +55 +>>> 2 3 ⍴ ⍳ 6 +1 2 3 4 5 6 +>>> ⍴ 2 3 ⍴ ⍳ 6 +2 3 +>>> ⌽ 'hello' +olleh +``` + +Variables and functions persist across lines: + +``` +>>> data←⍳ 10 +>>> +/ data +55 +>>> double←{⍵×2} +>>> double 1 2 3 +2 4 6 +>>> {⍵≤1: ⍵ ⋄ ⍵×∇ ⍵-1} 5 +120 +``` + +See the [apiel](https://crates.io/crates/apiel) crate for the full support info. + +## Affiliation + +Capstone project for the [rustcamp](https://github.com/rust-lang-ua/rustcamp) by the [Ukrainian Rust Community](https://www.uarust.com). diff --git a/apiel-wasm/Cargo.toml b/apiel-wasm/Cargo.toml new file mode 100644 index 0000000..4053f10 --- /dev/null +++ b/apiel-wasm/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "apiel-wasm" +version = "0.3.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +apiel = { path = "../apiel" } +wasm-bindgen = "0.2" +getrandom = { version = "0.2", features = ["js"] } diff --git a/apiel-wasm/src/lib.rs b/apiel-wasm/src/lib.rs new file mode 100644 index 0000000..82981b3 --- /dev/null +++ b/apiel-wasm/src/lib.rs @@ -0,0 +1,26 @@ +use apiel::Env; +use apiel::parse::{eval_to_val, format_val}; +use std::cell::RefCell; +use wasm_bindgen::prelude::*; + +thread_local! { + static ENV: RefCell = RefCell::new(Env::new()); +} + +#[wasm_bindgen] +pub fn eval_apl(input: &str) -> String { + ENV.with(|env| { + let mut env = env.borrow_mut(); + match eval_to_val(input, &mut env) { + Ok(val) => format_val(&val), + Err(e) => format!("ERROR: {e}"), + } + }) +} + +#[wasm_bindgen] +pub fn reset_env() { + ENV.with(|env| { + *env.borrow_mut() = Env::new(); + }); +} diff --git a/apiel/Cargo.toml b/apiel/Cargo.toml index eefe2e1..ca26890 100644 --- a/apiel/Cargo.toml +++ b/apiel/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "apiel" -version = "0.2.0" +version = "0.3.0" authors = ["Mark Firman "] edition = "2024" description = "A subset of the APL programming language implemented in Rust. Exports a macro for evaluating APL expressions from Rust code, providing a way to solve some problems in a very concise manner." keywords = ["apl", "array", "interpreter", "language", "math"] categories = ["mathematics", "compilers"] homepage = "https://github.com/NamesMark/apiel" -repository = "https://github.com/NamesMark/apiel/apiel" +repository = "https://github.com/NamesMark/apiel/tree/main/apiel" license = "MIT" build = "build.rs" diff --git a/apiel/README.md b/apiel/README.md index 8866918..9aad5fe 100644 --- a/apiel/README.md +++ b/apiel/README.md @@ -1,23 +1,43 @@ -# apiel -This is the library crate for the **apiel** interpreter. - -**apiel** is a small subset of the [APL programming language](https://en.wikipedia.org/wiki/APL_(programming_language)) implemented in Rust. - -The ultimate goal of the project is to export a macro that allows evaluating APL expressions from Rust code, providing a way to solve some problems in a very conscise manner. - -## Affiliation - -This was created as a capstone project for the [rustcamp](https://github.com/rust-lang-ua/rustcamp), a Rust bootcamp organized by the Ukrainian Rust Community ([website](https://www.uarust.com), [linked in](https://www.linkedin.com/company/ukrainian-rust-community), [telegram](https://t.me/rustlang_ua), [github](https://github.com/rust-lang-ua), [youtube](https://www.youtube.com/channel/UCmkAFUu2MVOX8ly0LjB6TMA), [twitter](https://twitter.com/rustukraine)). - -## Usage - -Add the crate to the project. - -Call the `parse_and_evaluate()` function: - -```rust -match apiel::parse::parse_and_evaluate(&line) { - Ok(result) => println!("Result: {:?}", result), - Err(err) => (), // process the error -} -``` +# apiel + +A subset of the [APL programming language](https://en.wikipedia.org/wiki/APL_(programming_language)) implemented in Rust. Evaluate APL expressions from Rust code through the `apl!` macro. + +## Usage + +```rust +use apiel::apl; + +// Evaluate APL expressions +let sum = apl!("+/ ⍳ 10").unwrap(); // [55.0] +let mat = apl!("⍴ 2 3 ⍴ ⍳ 6").unwrap(); // [2.0, 3.0] +let fib = apl!("{⍵<2: ⍵ ⋄ (∇ ⍵-1)+∇ ⍵-2} 10").unwrap(); // [55.0] + +// Pass Rust data as ⍵ (right argument) +let result = apl!("+/ ⍵", omega: &[1.0, 2.0, 3.0, 4.0, 5.0]).unwrap(); // [15.0] + +// Pass both ⍺ (left) and ⍵ (right) +let result = apl!("⍺ × ⍵", alpha: &[10.0], omega: &[1.0, 2.0, 3.0]).unwrap(); // [10.0, 20.0, 30.0] + +// Shared environment -- variables persist across calls +let mut env = apiel::Env::new(); +apl!("data←⍳ 10", &mut env).unwrap(); +apl!("total←+/ data", &mut env).unwrap(); +let result = apl!("total", &mut env).unwrap(); // [55.0] + +// Define and call named functions +apl!("double←{⍵×2}", &mut env).unwrap(); +apl!("double 1 2 3", &mut env).unwrap(); // [2.0, 4.0, 6.0] +``` + +## What's Supported + +- **Arithmetic**: `+` `-` `×` `÷` `*` `⍟` `○` `!` `?` `|` `⌈` `⌊` `⌹` +- **Arrays**: `⍴` `,` `⌽` `⍉` `↑` `↓` `⍋` `⍒` `⊂` `⊃` `⊆` `⌷` `∪` `∩` `~` `⊣` `⊢` `≡` `≢` `⍷` +- **Comparison**: `=` `≠` `<` `>` `≤` `≥` `∧` `∨` `⍲` `⍱` +- **Operators**: `f/` reduce, `f\` scan, `∘.f` outer product, `f.g` inner product, `f¨` each, `f⍨` commute, `f⍣n` power, `{f}∘{g}` compose, `{f}⍥{g}` over, `{f}⍤k` rank, `{f}@i` at, `{f}⌸` key +- **Trains**: `(f g h)` fork, `(f g)` atop -- supports primitives, reductions, and named functions +- **Language**: `←` assignment, `x+←1` modified assignment, `x[i]←v` indexed assignment, `{⍵}` dfns, `∇` recursion, `⋄` `:` guards, `¯` high minus, `'...'` strings, `⊥` `⊤` encode/decode, nested arrays + +## Affiliation + +Capstone project for the [rustcamp](https://github.com/rust-lang-ua/rustcamp) by the [Ukrainian Rust Community](https://www.uarust.com). diff --git a/apiel/build.rs b/apiel/build.rs index dd5a109..4b3e4a6 100644 --- a/apiel/build.rs +++ b/apiel/build.rs @@ -10,6 +10,7 @@ fn main() { .lrpar_config(|ctp| { ctp.yacckind(YaccKind::Grmtools) .rust_edition(lrpar::RustEdition::Rust2021) + .error_on_conflicts(false) .grammar_in_src_dir("apiel.y") .unwrap() }) diff --git a/apiel/src/apiel.l b/apiel/src/apiel.l index 3ea6357..e4870db 100644 --- a/apiel/src/apiel.l +++ b/apiel/src/apiel.l @@ -19,6 +19,8 @@ \⌊/ "MIN" \⍳ "IOTA" \⍸ "IOTA_U" +\⌿ "REDUCEFIRST" +\⍀ "SCANFIRST" \/ "/" \\ "\" \, "," @@ -40,6 +42,9 @@ \⍋ "GRADEUP" \⍒ "GRADEDN" ∘\. "OUTERPRODUCT" +\∘ "COMPOSE" +\⍥ "OVER" +[\+\-×÷\*⍟○!\?|⌈⌊∧∨⍲⍱=≠<>≤≥]← "MODASSIGN" \← "ASSIGN" \⍵ "OMEGA" \⍺ "ALPHA" @@ -48,6 +53,10 @@ \⊃ "FIRST" \⊆ "PARTITION" \¨ "EACH" +\⌸ "KEY" +\⍣ "POWOP" +\⍤ "RANK" +\⍨ "COMMUTE" \∪ "UNIQUE" \∩ "INTERSECT" \~ "TILDE" @@ -56,13 +65,22 @@ \. "DOT" \⌷ "INDEX" \⌹ "MATINV" +@ "AT" '[^']*' "STRING" +\⊣ "LEFT" +\⊢ "RIGHT" +\≡ "MATCH" +\≢ "NOTMATCH" +\⍷ "FIND" +\∊ "MEMBERSHIP" \⋄ "DIAMOND" : ":" \( "(" \) ")" \{ "{" \} "}" +\[ "[" +\] "]" [a-zA-Z_][a-zA-Z0-9_]* "NAME" [\t ]+ ; . "UNMATCHED" \ No newline at end of file diff --git a/apiel/src/apiel.y b/apiel/src/apiel.y index 6543b93..4da0b2f 100644 --- a/apiel/src/apiel.y +++ b/apiel/src/apiel.y @@ -1,7 +1,7 @@ %start Expr %avoid_insert "INT" %expect-unused Unmatched "UNMATCHED" -%expect 1 +%expect 3 %% Expr -> Result: Term { $1 } @@ -101,6 +101,38 @@ Term -> Result: | Factor 'DROP' Term { Ok(Expr::Drop{ span: $span, lhs: Box::new($1?), rhs: Box::new($3?) }) } + | 'NAME' 'MODASSIGN' Term { + let op_str = $2.map(|l| $lexer.span_str(l.span()).to_string()).unwrap_or_default(); + let op = match op_str.trim_end_matches('←') { + "+" => Ok(Operator::Add), + "-" => Ok(Operator::Subtract), + "×" => Ok(Operator::Multiply), + "÷" => Ok(Operator::Divide), + "*" => Ok(Operator::Power), + "⍟" => Ok(Operator::Log), + "!" => Ok(Operator::Binomial), + "|" => Ok(Operator::Residue), + "⌈" => Ok(Operator::Max), + "⌊" => Ok(Operator::Min), + "∧" => Ok(Operator::And), + "∨" => Ok(Operator::Or), + "⍲" => Ok(Operator::Nand), + "⍱" => Ok(Operator::Nor), + _ => Err(()), + }; + match op { + Ok(operator) => Ok(Expr::ModifiedAssign{ + span: $span, + name: $1.map(|l| $lexer.span_str(l.span()).to_string()).unwrap_or_default(), + operator, + rhs: Box::new($3?), + }), + Err(_) => Err(()), + } + } + | 'NAME' '[' Expr ']' 'ASSIGN' Term { + Ok(Expr::IndexedAssign{ span: $span, name: $1.map(|l| $lexer.span_str(l.span()).to_string()).unwrap_or_default(), indices: Box::new($3?), rhs: Box::new($6?) }) + } | 'NAME' 'ASSIGN' Term { Ok(Expr::Assign{ span: $span, name: $1.map(|l| $lexer.span_str(l.span()).to_string()).unwrap_or_default(), rhs: Box::new($3?) }) } @@ -119,6 +151,36 @@ Term -> Result: | '{' DfnBody '}' Term { Ok(Expr::MonadicDfn{ span: $span, body: Box::new($2?), rhs: Box::new($4?) }) } + | '{' DfnBody '}' 'COMPOSE' '{' DfnBody '}' Term { + Ok(Expr::ComposeDfn{ span: $span, f: Box::new($2?), g: Box::new($6?), arg: Box::new($8?) }) + } + | Factor '{' DfnBody '}' 'COMPOSE' '{' DfnBody '}' Term { + Ok(Expr::ComposeDyadicDfn{ span: $span, lhs: Box::new($1?), f: Box::new($3?), g: Box::new($7?), arg: Box::new($9?) }) + } + | '{' DfnBody '}' 'OVER' '{' DfnBody '}' Term { + Ok(Expr::OverDfn{ span: $span, f: Box::new($2?), g: Box::new($6?), arg: Box::new($8?) }) + } + | Factor '{' DfnBody '}' 'OVER' '{' DfnBody '}' Term { + Ok(Expr::OverDyadicDfn{ span: $span, lhs: Box::new($1?), f: Box::new($3?), g: Box::new($7?), arg: Box::new($9?) }) + } + | '{' DfnBody '}' 'AT' Factor Term { + Ok(Expr::AtOp{ span: $span, body: Box::new($2?), indices: Box::new($5?), arg: Box::new($6?) }) + } + | '{' DfnBody '}' 'POWOP' Factor Term { + Ok(Expr::PowerOp{ span: $span, body: Box::new($2?), count: Box::new($5?), arg: Box::new($6?) }) + } + | '{' DfnBody '}' 'KEY' Term { + Ok(Expr::KeyOp{ span: $span, body: Box::new($2?), arg: Box::new($5?) }) + } + | '{' DfnBody '}' 'RANK' Factor Term { + Ok(Expr::RankOp{ span: $span, body: Box::new($2?), rank: Box::new($5?), arg: Box::new($6?) }) + } + | '{' DfnBody '}' '/' Term { + Ok(Expr::DfnReduce{ span: $span, body: Box::new($2?), term: Box::new($5?) }) + } + | '{' DfnBody '}' 'REDUCEFIRST' Term { + Ok(Expr::DfnReduceFirst{ span: $span, body: Box::new($2?), term: Box::new($5?) }) + } | 'NAME' Factor { Ok(Expr::NamedMonadic{ span: $span, name: $1.map(|l| $lexer.span_str(l.span()).to_string()).unwrap_or_default(), rhs: Box::new($2?) }) } @@ -149,6 +211,30 @@ Term -> Result: | Factor 'ENCODE' Term { Ok(Expr::Encode{ span: $span, lhs: Box::new($1?), rhs: Box::new($3?) }) } + | Factor 'LEFT' Term { + Ok(Expr::Left{ span: $span, lhs: Box::new($1?), rhs: Box::new($3?) }) + } + | Factor 'RIGHT' Term { + Ok(Expr::Right{ span: $span, lhs: Box::new($1?), rhs: Box::new($3?) }) + } + | Factor 'MATCH' Term { + Ok(Expr::Match{ span: $span, lhs: Box::new($1?), rhs: Box::new($3?) }) + } + | Factor 'NOTMATCH' Term { + Ok(Expr::NotMatch{ span: $span, lhs: Box::new($1?), rhs: Box::new($3?) }) + } + | Factor 'FIND' Term { + Ok(Expr::Find{ span: $span, lhs: Box::new($1?), rhs: Box::new($3?) }) + } + | Factor 'MEMBERSHIP' Term { + Ok(Expr::Membership{ span: $span, lhs: Box::new($1?), rhs: Box::new($3?) }) + } + | Factor 'ENCLOSE' Term { + Ok(Expr::PartitionedEnclose{ span: $span, lhs: Box::new($1?), rhs: Box::new($3?) }) + } + | Factor 'TRANSPOSE' Term { + Ok(Expr::DyadicTranspose{ span: $span, lhs: Box::new($1?), rhs: Box::new($3?) }) + } | Factor Operator 'DOT' Operator Term { match ($2, $4) { (Ok(f), Ok(g)) => Ok(Expr::InnerProduct{ span: $span, lhs: Box::new($1?), f, g, rhs: Box::new($5?) }), @@ -161,6 +247,12 @@ Term -> Result: Err(_) => Err(()) } } + | Factor Operator 'COMMUTE' Term { + match $2 { + Ok(op) => Ok(Expr::Commute{ span: $span, lhs: Box::new($1?), operator: op, rhs: Box::new($4?) }), + Err(_) => Err(()) + } + } | MonadicFactor { Ok($1?) } @@ -253,6 +345,24 @@ MonadicFactor -> Result: | 'MATINV' Term { Ok(Expr::MatrixInverse{ span: $span, arg: Box::new($2?) }) } + | 'LEFT' Term { + Ok(Expr::LeftIdentity{ span: $span, arg: Box::new($2?) }) + } + | 'RIGHT' Term { + Ok(Expr::RightIdentity{ span: $span, arg: Box::new($2?) }) + } + | 'MATCH' Term { + Ok(Expr::Depth{ span: $span, arg: Box::new($2?) }) + } + | 'NOTMATCH' Term { + Ok(Expr::Tally{ span: $span, arg: Box::new($2?) }) + } + | 'TAKE' Term { + Ok(Expr::Mix{ span: $span, arg: Box::new($2?) }) + } + | 'DROP' Term { + Ok(Expr::Split{ span: $span, arg: Box::new($2?) }) + } | 'RHO' 'EACH' Term { Ok(Expr::MonadicEach{ span: $span, func: "shape".to_string(), arg: Box::new($3?) }) } @@ -262,6 +372,12 @@ MonadicFactor -> Result: | 'IOTA' 'EACH' Term { Ok(Expr::MonadicEach{ span: $span, func: "iota".to_string(), arg: Box::new($3?) }) } + | Operator 'COMMUTE' Term { + match $1 { + Ok(op) => Ok(Expr::Selfie{ span: $span, operator: op, arg: Box::new($3?) }), + Err(_) => Err(()) + } + } ; DfnBody -> Result: @@ -279,6 +395,9 @@ DfnBody -> Result: Factor -> Result: '(' Expr ')' { $2 } + | Factor '[' Expr ']' { + Ok(Expr::IndexRead{ span: $span, array: Box::new($1?), indices: Box::new($3?) }) + } | 'VEC' { let elements = match $1 { @@ -324,11 +443,30 @@ Factor -> Result: | 'ALPHA' { Ok(Expr::Alpha { span: $span }) } + | StringArray { $1 } | 'STRING' { Ok(Expr::StringLiteral { span: $span }) } ; + StringArray -> Result: + 'STRING' 'STRING' { + Ok(Expr::StringArray { span: $span, elements: vec![ + Expr::StringLiteral { span: $1.map(|l| l.span()).unwrap_or($span) }, + Expr::StringLiteral { span: $2.map(|l| l.span()).unwrap_or($span) }, + ]}) + } + | StringArray 'STRING' { + match $1? { + Expr::StringArray { span: _, mut elements } => { + elements.push(Expr::StringLiteral { span: $2.map(|l| l.span()).unwrap_or($span) }); + Ok(Expr::StringArray { span: $span, elements }) + }, + _ => Err(()), + } + } + ; + Reduction -> Result: Operator '/' Term { match $1 { @@ -348,6 +486,18 @@ Factor -> Result: Err(_) => Err(()) } } + | Operator 'REDUCEFIRST' Term { + match $1 { + Ok(op) => Ok(Expr::ReduceFirst{ span: $span, operator: op, term: Box::new($3?) }), + Err(_) => Err(()) + } + } + | Operator 'SCANFIRST' Term { + match $1 { + Ok(op) => Ok(Expr::ScanFirst{ span: $span, operator: op, term: Box::new($3?) }), + Err(_) => Err(()) + } + } ; Operator -> Result: @@ -356,10 +506,21 @@ Factor -> Result: | '×' { Ok(Operator::Multiply) } | '÷' { Ok(Operator::Divide) } | 'EQ' { Ok(Operator::Equal) } + | 'NEQ' { Ok(Operator::NotEqual) } | 'LT' { Ok(Operator::LessThan) } | 'GT' { Ok(Operator::GreaterThan) } + | 'LTE' { Ok(Operator::LessEqual) } + | 'GTE' { Ok(Operator::GreaterEqual) } | '⌈' { Ok(Operator::Max) } | '⌊' { Ok(Operator::Min) } + | 'AND' { Ok(Operator::And) } + | 'OR' { Ok(Operator::Or) } + | 'NAND' { Ok(Operator::Nand) } + | 'NOR' { Ok(Operator::Nor) } + | 'EXP' { Ok(Operator::Power) } + | 'LOG' { Ok(Operator::Log) } + | '|' { Ok(Operator::Residue) } + | '!' { Ok(Operator::Binomial) } ; @@ -539,6 +700,29 @@ pub enum Expr { body: Box, rhs: Box, }, + AtOp { + span: Span, + body: Box, + indices: Box, + arg: Box, + }, + PowerOp { + span: Span, + body: Box, + count: Box, + arg: Box, + }, + RankOp { + span: Span, + body: Box, + rank: Box, + arg: Box, + }, + KeyOp { + span: Span, + body: Box, + arg: Box, + }, DyadicDfn { span: Span, lhs: Box, @@ -575,6 +759,18 @@ pub enum Expr { name: String, body: Box, }, + ModifiedAssign { + span: Span, + name: String, + operator: Operator, + rhs: Box, + }, + IndexedAssign { + span: Span, + name: String, + indices: Box, + rhs: Box, + }, NamedMonadic { span: Span, name: String, @@ -631,6 +827,11 @@ pub enum Expr { lhs: Box, rhs: Box, }, + PartitionedEnclose { + span: Span, + lhs: Box, + rhs: Box, + }, MonadicEach { span: Span, func: String, @@ -642,6 +843,17 @@ pub enum Expr { operator: Operator, rhs: Box, }, + Commute { + span: Span, + lhs: Box, + operator: Operator, + rhs: Box, + }, + Selfie { + span: Span, + operator: Operator, + arg: Box, + }, ReduceEach { span: Span, operator: Operator, @@ -669,6 +881,31 @@ pub enum Expr { lhs: Box, rhs: Box, }, + Left { + span: Span, + lhs: Box, + rhs: Box, + }, + Right { + span: Span, + lhs: Box, + rhs: Box, + }, + Match { + span: Span, + lhs: Box, + rhs: Box, + }, + NotMatch { + span: Span, + lhs: Box, + rhs: Box, + }, + Find { + span: Span, + lhs: Box, + rhs: Box, + }, StringLiteral { span: Span, }, @@ -683,6 +920,32 @@ pub enum Expr { operator: Operator, term: Box, }, + ComposeDfn { + span: Span, + f: Box, + g: Box, + arg: Box, + }, + ComposeDyadicDfn { + span: Span, + lhs: Box, + f: Box, + g: Box, + arg: Box, + }, + OverDfn { + span: Span, + f: Box, + g: Box, + arg: Box, + }, + OverDyadicDfn { + span: Span, + lhs: Box, + f: Box, + g: Box, + arg: Box, + }, // Monadic @@ -702,6 +965,11 @@ pub enum Expr { span: Span, arg: Box, }, + DyadicTranspose { + span: Span, + lhs: Box, + rhs: Box, + }, GradeUp { span: Span, arg: Box, @@ -774,12 +1042,70 @@ pub enum Expr { span: Span, arg: Box, }, + LeftIdentity { + span: Span, + arg: Box, + }, + RightIdentity { + span: Span, + arg: Box, + }, + Depth { + span: Span, + arg: Box, + }, + Tally { + span: Span, + arg: Box, + }, + Mix { + span: Span, + arg: Box, + }, + Split { + span: Span, + arg: Box, + }, Reduce { span: Span, operator: Operator, term: Box, }, + ReduceFirst { + span: Span, + operator: Operator, + term: Box, + }, + ScanFirst { + span: Span, + operator: Operator, + term: Box, + }, + Membership { + span: Span, + lhs: Box, + rhs: Box, + }, + IndexRead { + span: Span, + array: Box, + indices: Box, + }, + DfnReduce { + span: Span, + body: Box, + term: Box, + }, + DfnReduceFirst { + span: Span, + body: Box, + term: Box, + }, + StringArray { + span: Span, + elements: Vec, + }, // Values @@ -802,8 +1128,19 @@ pub enum Operator { Multiply, Divide, Equal, + NotEqual, LessThan, GreaterThan, + LessEqual, + GreaterEqual, Max, Min, + And, + Or, + Nand, + Nor, + Power, + Log, + Residue, + Binomial, } diff --git a/apiel/src/parse/eval.rs b/apiel/src/parse/eval.rs index c272280..d57138d 100644 --- a/apiel/src/parse/eval.rs +++ b/apiel/src/parse/eval.rs @@ -42,7 +42,10 @@ fn apply_dyadic_operation( where F: Fn(&Scalar, &Scalar) -> Result, { - if lhs.is_scalar() { + // Treat 1-element arrays as scalars for broadcasting (standard APL behavior) + let lhs_scalar = lhs.data.len() == 1; + let rhs_scalar = rhs.data.len() == 1; + if lhs_scalar && !rhs_scalar { let data = rhs .data .iter() @@ -50,7 +53,7 @@ where .collect::, _>>() .map_err(|_| (span, "Operation failed"))?; Ok(Val::new(rhs.shape.clone(), data)) - } else if rhs.is_scalar() { + } else if rhs_scalar && !lhs_scalar { let data = lhs .data .iter() @@ -58,7 +61,7 @@ where .collect::, _>>() .map_err(|_| (span, "Operation failed"))?; Ok(Val::new(lhs.shape.clone(), data)) - } else if lhs.shape == rhs.shape { + } else if lhs.shape == rhs.shape || (lhs_scalar && rhs_scalar) { let data = lhs .data .iter() @@ -99,10 +102,75 @@ fn get_operator_fn(op: Operator) -> fn(&Scalar, &Scalar) -> Option { Operator::Multiply => |a, b| a.checked_mul(b), Operator::Divide => |a, b| a.checked_div(b), Operator::Equal => |a, b| Some(Scalar::Integer(if a == b { 1 } else { 0 })), + Operator::NotEqual => |a, b| Some(Scalar::Integer(if a != b { 1 } else { 0 })), Operator::LessThan => |a, b| Some(Scalar::Integer(if a < b { 1 } else { 0 })), Operator::GreaterThan => |a, b| Some(Scalar::Integer(if a > b { 1 } else { 0 })), + Operator::LessEqual => |a, b| Some(Scalar::Integer(if a <= b { 1 } else { 0 })), + Operator::GreaterEqual => |a, b| Some(Scalar::Integer(if a >= b { 1 } else { 0 })), Operator::Max => |a, b| Some(if a >= b { a.clone() } else { b.clone() }), Operator::Min => |a, b| Some(if a <= b { a.clone() } else { b.clone() }), + Operator::And => |a, b| { + let af: f64 = a.clone().into(); + let bf: f64 = b.clone().into(); + Some(Scalar::Integer(if af != 0.0 && bf != 0.0 { 1 } else { 0 })) + }, + Operator::Or => |a, b| { + let af: f64 = a.clone().into(); + let bf: f64 = b.clone().into(); + Some(Scalar::Integer(if af != 0.0 || bf != 0.0 { 1 } else { 0 })) + }, + Operator::Nand => |a, b| { + let af: f64 = a.clone().into(); + let bf: f64 = b.clone().into(); + Some(Scalar::Integer(if af != 0.0 && bf != 0.0 { 0 } else { 1 })) + }, + Operator::Nor => |a, b| { + let af: f64 = a.clone().into(); + let bf: f64 = b.clone().into(); + Some(Scalar::Integer(if af != 0.0 || bf != 0.0 { 0 } else { 1 })) + }, + Operator::Power => |a, b| { + let af: f64 = a.clone().into(); + let bf: f64 = b.clone().into(); + let result = af.powf(bf); + if result.fract() == 0.0 && result.abs() < i64::MAX as f64 { + Some(Scalar::Integer(result as i64)) + } else { + Some(Scalar::Float(result)) + } + }, + Operator::Log => |a, b| { + let af: f64 = a.clone().into(); + let bf: f64 = b.clone().into(); + Some(Scalar::Float(bf.ln() / af.ln())) + }, + Operator::Residue => |a, b| { + let af: f64 = a.clone().into(); + let bf: f64 = b.clone().into(); + if af == 0.0 { + Some(Scalar::Float(bf)) + } else { + let r = bf % af; + if r.fract() == 0.0 && r.abs() < i64::MAX as f64 { + Some(Scalar::Integer(r as i64)) + } else { + Some(Scalar::Float(r)) + } + } + }, + Operator::Binomial => |a, b| { + let n: f64 = b.clone().into(); + let k: f64 = a.clone().into(); + let mut result = 1.0_f64; + for i in 0..k as u64 { + result *= (n - i as f64) / (i as f64 + 1.0); + } + if result.fract() == 0.0 && result.abs() < i64::MAX as f64 { + Some(Scalar::Integer(result as i64)) + } else { + Some(Scalar::Float(result)) + } + }, } } @@ -629,6 +697,74 @@ pub fn eval( env.vars.insert(name, val.clone()); Ok(val) } + Expr::ModifiedAssign { + span, + name, + operator, + rhs, + } => { + debug!("Modified Assign: {name}"); + let current = env + .vars + .get(&name) + .cloned() + .ok_or((span, "Undefined variable for modified assignment"))?; + let rhs_eval = eval(lexer, *rhs, env)?; + let op_fn = get_operator_fn(operator); + let result = apply_dyadic_operation(span, ¤t, &rhs_eval, |a, b| { + op_fn(a, b).ok_or_eyre("Modified assignment operation failed") + })?; + env.vars.insert(name, result.clone()); + Ok(result) + } + Expr::IndexedAssign { + span, + name, + indices, + rhs, + } => { + debug!("Indexed Assign: {name}"); + let mut current = env + .vars + .get(&name) + .cloned() + .ok_or((span, "Undefined variable for indexed assignment"))?; + let idx_val = eval(lexer, *indices, env)?; + let rhs_val = eval(lexer, *rhs, env)?; + + let idxs: Vec = idx_val + .data + .iter() + .map(|s| { + let i: usize = s + .clone() + .try_into() + .map_err(|_| (span, "Index must be integer"))?; + if i < 1 || i > current.data.len() { + return Err((span, "Index out of bounds")); + } + Ok(i - 1) // 1-based to 0-based + }) + .collect::, _>>()?; + + if rhs_val.is_scalar() { + // Scalar: set all indexed positions to same value + for &idx in &idxs { + current.data[idx] = rhs_val.data[0].clone(); + } + } else { + // Vector: must match length + if rhs_val.data.len() != idxs.len() { + return Err((span, "Indexed assign: value length must match index count")); + } + for (i, &idx) in idxs.iter().enumerate() { + current.data[idx] = rhs_val.data[i].clone(); + } + } + + env.vars.insert(name, current.clone()); + Ok(current) + } Expr::OuterProduct { span, lhs, @@ -915,6 +1051,83 @@ pub fn eval( _ => Err((Span::new(0, 0), "Transpose only supports rank 0, 1, or 2")), } } + Expr::DyadicTranspose { span, lhs, rhs } => { + debug!("Dyadic Transpose"); + let lhs_eval = eval(lexer, *lhs, env)?; + let rhs_eval = eval(lexer, *rhs, env)?; + + // Parse permutation vector (1-based to 0-based) + let perm: Vec = lhs_eval + .data + .iter() + .map(|s| { + let v: usize = s + .clone() + .try_into() + .map_err(|_| (span, "Transpose perm must be integers"))?; + if v < 1 || v > rhs_eval.shape.len() { + return Err((span, "Transpose permutation out of range")); + } + Ok(v - 1) + }) + .collect::, _>>()?; + + if perm.len() != rhs_eval.shape.len() { + return Err((span, "Transpose permutation length must match array rank")); + } + + let old_shape = &rhs_eval.shape; + let rank = old_shape.len(); + + // New shape: new_shape[perm[i]] = old_shape[i] + let mut new_shape = vec![0usize; rank]; + for i in 0..rank { + new_shape[perm[i]] = old_shape[i]; + } + + let total: usize = new_shape.iter().product(); + let mut new_data = vec![Scalar::Integer(0); total]; + + // Compute strides for old shape + let mut old_strides = vec![1usize; rank]; + for i in (0..rank - 1).rev() { + old_strides[i] = old_strides[i + 1] * old_shape[i + 1]; + } + + // Compute strides for new shape + let mut new_strides = vec![1usize; rank]; + for i in (0..rank - 1).rev() { + new_strides[i] = new_strides[i + 1] * new_shape[i + 1]; + } + + // For each element in old array, compute its new position + for old_flat in 0..total { + // Convert flat index to multi-dimensional old index + let mut old_idx = vec![0usize; rank]; + let mut remaining = old_flat; + for i in 0..rank { + old_idx[i] = remaining / old_strides[i]; + remaining %= old_strides[i]; + } + + // Apply permutation: new_idx[perm[i]] = old_idx[i] + let mut new_idx = vec![0usize; rank]; + for i in 0..rank { + new_idx[perm[i]] = old_idx[i]; + } + + // Convert new multi-dimensional index to flat + let new_flat: usize = new_idx + .iter() + .zip(new_strides.iter()) + .map(|(&i, &s)| i * s) + .sum(); + + new_data[new_flat] = rhs_eval.data[old_flat].clone(); + } + + Ok(Val::new(new_shape, new_data)) + } Expr::GradeUp { span, arg } => { debug!("Monadic Grade Up"); let arg_eval = eval(lexer, *arg, env)?; @@ -947,23 +1160,52 @@ pub fn eval( debug!("Reduce"); let term_eval = eval(lexer, *term, env)?; - // APL reduce is a right-fold: f/ a b c d = a f (b f (c f d)) + // APL reduce is a right-fold along the last axis: + // f/ a b c d = a f (b f (c f d)) let op_fn = get_operator_fn(operator); - let result = term_eval - .data - .iter() - .rev() - .cloned() - .try_fold(None, |acc, n| match acc { - None => Some(Some(n)), - Some(right) => op_fn(&n, &right).map(Some), - }) - .flatten(); - - result - .map(Val::scalar) - .ok_or((span, "Arithmetic error or invalid operation in Reduce")) + if term_eval.shape.len() <= 1 { + // Vector or scalar: reduce all elements + let result = term_eval + .data + .iter() + .rev() + .cloned() + .try_fold(None, |acc, n| match acc { + None => Some(Some(n)), + Some(right) => op_fn(&n, &right).map(Some), + }) + .flatten(); + result + .map(Val::scalar) + .ok_or((span, "Arithmetic error or invalid operation in Reduce")) + } else { + // Higher-rank: reduce along last axis + let last_dim = *term_eval.shape.last().unwrap(); + let row_count: usize = term_eval.data.len() / last_dim; + let mut results = Vec::with_capacity(row_count); + for i in 0..row_count { + let start = i * last_dim; + let row = &term_eval.data[start..start + last_dim]; + let result = row + .iter() + .rev() + .cloned() + .try_fold(None::, |acc, n| match acc { + None => Some(Some(n)), + Some(right) => op_fn(&n, &right).map(Some), + }) + .flatten() + .ok_or((span, "Arithmetic error in Reduce"))?; + results.push(result); + } + let new_shape = term_eval.shape[..term_eval.shape.len() - 1].to_vec(); + if new_shape.is_empty() { + Ok(Val::scalar(results.into_iter().next().unwrap())) + } else { + Ok(Val::new(new_shape, results)) + } + } } Expr::Scan { span, @@ -993,6 +1235,235 @@ pub fn eval( } Ok(Val::vector(data)) } + Expr::ReduceFirst { + span, + operator, + term, + } => { + debug!("Reduce First Axis"); + let term_eval = eval(lexer, *term, env)?; + let op_fn = get_operator_fn(operator); + + if term_eval.shape.len() <= 1 { + // Vector: same as regular reduce + let result = term_eval + .data + .iter() + .rev() + .cloned() + .try_fold(None, |acc, n| match acc { + None => Some(Some(n)), + Some(right) => op_fn(&n, &right).map(Some), + }) + .flatten(); + result + .map(Val::scalar) + .ok_or((span, "Arithmetic error in ReduceFirst")) + } else { + // Higher-rank: reduce along FIRST axis (columns) + let first_dim = term_eval.shape[0]; + let stride: usize = term_eval.data.len() / first_dim; + let mut results = Vec::with_capacity(stride); + for col in 0..stride { + let column: Vec = (0..first_dim) + .map(|row| term_eval.data[row * stride + col].clone()) + .collect(); + let result = column + .iter() + .rev() + .cloned() + .try_fold(None::, |acc, n| match acc { + None => Some(Some(n)), + Some(right) => op_fn(&n, &right).map(Some), + }) + .flatten() + .ok_or((span, "Arithmetic error in ReduceFirst"))?; + results.push(result); + } + let new_shape = term_eval.shape[1..].to_vec(); + if new_shape.is_empty() { + Ok(Val::scalar(results.into_iter().next().unwrap())) + } else { + Ok(Val::new(new_shape, results)) + } + } + } + Expr::ScanFirst { + span, + operator, + term, + } => { + debug!("Scan First Axis"); + let term_eval = eval(lexer, *term, env)?; + let op_fn = get_operator_fn(operator); + + if term_eval.shape.len() <= 1 { + // Vector: same as regular scan + let mut data = Vec::with_capacity(term_eval.data.len()); + for i in 0..term_eval.data.len() { + let prefix = &term_eval.data[..=i]; + let result = prefix + .iter() + .rev() + .cloned() + .try_fold(None::, |acc, n| match acc { + None => Some(Some(n)), + Some(right) => op_fn(&n, &right).map(Some), + }) + .flatten() + .ok_or((span, "Arithmetic error in ScanFirst"))?; + data.push(result); + } + Ok(Val::vector(data)) + } else { + // Higher-rank: scan along FIRST axis (columns) + let first_dim = term_eval.shape[0]; + let stride: usize = term_eval.data.len() / first_dim; + let mut data = term_eval.data.clone(); + for col in 0..stride { + for row in 1..first_dim { + let prev = data[(row - 1) * stride + col].clone(); + let curr = data[row * stride + col].clone(); + data[row * stride + col] = + op_fn(&prev, &curr).ok_or((span, "Arithmetic error in ScanFirst"))?; + } + } + Ok(Val::new(term_eval.shape.clone(), data)) + } + } + Expr::Membership { span: _, lhs, rhs } => { + debug!("Dyadic Membership"); + let lhs_eval = eval(lexer, *lhs, env)?; + let rhs_eval = eval(lexer, *rhs, env)?; + let data = lhs_eval + .data + .iter() + .map(|l| { + let found = rhs_eval.data.iter().any(|r| l == r); + Scalar::Integer(if found { 1 } else { 0 }) + }) + .collect(); + Ok(Val::new(lhs_eval.shape.clone(), data)) + } + Expr::IndexRead { + span, + array, + indices, + } => { + debug!("Index Read"); + let arr = eval(lexer, *array, env)?; + let idx_val = eval(lexer, *indices, env)?; + let indices: Vec = idx_val + .data + .iter() + .map(|s| { + let i: usize = s + .clone() + .try_into() + .map_err(|_| (span, "Index must be integer"))?; + if i < 1 || i > arr.data.len() { + return Err((span, "Index out of bounds")); + } + Ok(i - 1) + }) + .collect::, _>>()?; + let data: Vec = indices.iter().map(|&i| arr.data[i].clone()).collect(); + if data.len() == 1 { + Ok(Val::scalar(data.into_iter().next().unwrap())) + } else { + Ok(Val::vector(data)) + } + } + Expr::DfnReduce { span, body, term } => { + debug!("Dfn Reduce"); + let term_eval = eval(lexer, *term, env)?; + if term_eval.data.len() < 2 { + return Ok(term_eval); + } + let body_rc = Rc::new(*body); + // Right fold: f/ a b c = a f (b f c) + let mut acc = Val::scalar(term_eval.data.last().unwrap().clone()); + for i in (0..term_eval.data.len() - 1).rev() { + let left = match &term_eval.data[i] { + Scalar::Nested(v) => (**v).clone(), + s => Val::scalar(s.clone()), + }; + let stored = StoredDfn { + body: Rc::clone(&body_rc), + source: lexer.span_str(span).to_string(), + }; + let mut dfn_env = env.clone(); + dfn_env.vars.insert("⍺".to_string(), left); + dfn_env.vars.insert("⍵".to_string(), acc); + dfn_env.fns.insert("∇".to_string(), stored); + acc = eval(lexer, (*body_rc).clone(), &mut dfn_env)?; + } + Ok(acc) + } + Expr::DfnReduceFirst { span, body, term } => { + debug!("Dfn Reduce First"); + let term_eval = eval(lexer, *term, env)?; + // For vectors, same as DfnReduce + // For matrices, reduce along first axis (column-wise) + if term_eval.shape.len() <= 1 { + let body_rc = Rc::new(*body); + if term_eval.data.len() < 2 { + return Ok(term_eval); + } + let mut acc = Val::scalar(term_eval.data.last().unwrap().clone()); + for i in (0..term_eval.data.len() - 1).rev() { + let left = match &term_eval.data[i] { + Scalar::Nested(v) => (**v).clone(), + s => Val::scalar(s.clone()), + }; + let stored = StoredDfn { + body: Rc::clone(&body_rc), + source: lexer.span_str(span).to_string(), + }; + let mut dfn_env = env.clone(); + dfn_env.vars.insert("⍺".to_string(), left); + dfn_env.vars.insert("⍵".to_string(), acc); + dfn_env.fns.insert("∇".to_string(), stored); + acc = eval(lexer, (*body_rc).clone(), &mut dfn_env)?; + } + Ok(acc) + } else { + // Higher-rank: reduce along first axis + let body_rc = Rc::new(*body); + let first_dim = term_eval.shape[0]; + let stride: usize = term_eval.data.len() / first_dim; + let cell_shape = term_eval.shape[1..].to_vec(); + // Start with last row + let mut acc_data = term_eval.data[(first_dim - 1) * stride..].to_vec(); + for row in (0..first_dim - 1).rev() { + let row_data = &term_eval.data[row * stride..(row + 1) * stride]; + let left = Val::new(cell_shape.clone(), row_data.to_vec()); + let right = Val::new(cell_shape.clone(), acc_data); + let stored = StoredDfn { + body: Rc::clone(&body_rc), + source: lexer.span_str(span).to_string(), + }; + let mut dfn_env = env.clone(); + dfn_env.vars.insert("⍺".to_string(), left); + dfn_env.vars.insert("⍵".to_string(), right); + dfn_env.fns.insert("∇".to_string(), stored); + let result = eval(lexer, (*body_rc).clone(), &mut dfn_env)?; + acc_data = result.data; + } + Ok(Val::new(cell_shape, acc_data)) + } + } + Expr::StringArray { span: _, elements } => { + debug!("String Array"); + let data: Vec = elements + .into_iter() + .map(|e| { + let val = eval(lexer, e, env)?; + Ok(Scalar::Nested(Box::new(val))) + }) + .collect::, (Span, &'static str)>>()?; + Ok(Val::vector(data)) + } Expr::Variable { span, name } => { debug!("Variable: {name}"); env.vars @@ -1023,6 +1494,168 @@ pub fn eval( dfn_env.fns.insert("∇".to_string(), stored); eval(lexer, (*body_rc).clone(), &mut dfn_env) } + Expr::RankOp { + span, + body, + rank, + arg, + } => { + debug!("Rank Operator"); + let rank_val = eval(lexer, *rank, env)?; + let k: usize = rank_val.data[0] + .clone() + .try_into() + .map_err(|_| (span, "Rank must be a non-negative integer"))?; + let arg_val = eval(lexer, *arg, env)?; + let n = arg_val.shape.len(); + let body_rc = Rc::new(*body); + if k >= n { + // Apply to entire array + let stored = StoredDfn { + body: Rc::clone(&body_rc), + source: lexer.span_str(span).to_string(), + }; + let mut dfn_env = env.clone(); + dfn_env.vars.insert("⍵".to_string(), arg_val); + dfn_env.fns.insert("∇".to_string(), stored); + return eval(lexer, (*body_rc).clone(), &mut dfn_env); + } + let frame_shape = arg_val.shape[..n - k].to_vec(); + let cell_shape = arg_val.shape[n - k..].to_vec(); + let cell_size: usize = cell_shape.iter().product(); + let num_cells: usize = frame_shape.iter().product(); + let mut results = Vec::new(); + let mut result_cell_shape: Option> = None; + for i in 0..num_cells { + let start = i * cell_size; + let cell_data = arg_val.data[start..start + cell_size].to_vec(); + let cell = Val::new(cell_shape.clone(), cell_data); + let stored = StoredDfn { + body: Rc::clone(&body_rc), + source: lexer.span_str(span).to_string(), + }; + let mut dfn_env = env.clone(); + dfn_env.vars.insert("⍵".to_string(), cell); + dfn_env.fns.insert("∇".to_string(), stored); + let result = eval(lexer, (*body_rc).clone(), &mut dfn_env)?; + if result_cell_shape.is_none() { + result_cell_shape = Some(result.shape.clone()); + } + results.extend(result.data); + } + let rcs = result_cell_shape.unwrap_or_default(); + let mut final_shape = frame_shape; + final_shape.extend_from_slice(&rcs); + Ok(Val::new(final_shape, results)) + } + Expr::AtOp { + span, + body, + indices, + arg, + } => { + debug!("At Operator"); + let idx_val = eval(lexer, *indices, env)?; + let mut arg_val = eval(lexer, *arg, env)?; + let body_rc = Rc::new(*body); + + // Convert 1-based indices to 0-based + let idxs: Vec = idx_val + .data + .iter() + .map(|s| { + let i: usize = s + .clone() + .try_into() + .map_err(|_| (span, "At index must be integer"))?; + if i < 1 || i > arg_val.data.len() { + return Err((span, "At index out of bounds")); + } + Ok(i - 1) + }) + .collect::, _>>()?; + + // Apply function to each indexed element + for &idx in &idxs { + let elem = Val::scalar(arg_val.data[idx].clone()); + let stored = StoredDfn { + body: Rc::clone(&body_rc), + source: lexer.span_str(span).to_string(), + }; + let mut dfn_env = env.clone(); + dfn_env.vars.insert("⍵".to_string(), elem); + dfn_env.fns.insert("∇".to_string(), stored); + let result = eval(lexer, (*body_rc).clone(), &mut dfn_env)?; + arg_val.data[idx] = result.data[0].clone(); + } + + Ok(arg_val) + } + Expr::KeyOp { span, body, arg } => { + debug!("Key Operator"); + let arg_val = eval(lexer, *arg, env)?; + let body_rc = Rc::new(*body); + + // Find unique keys and their indices (1-based) + let mut keys: Vec = Vec::new(); + let mut groups: Vec> = Vec::new(); + + for (i, s) in arg_val.data.iter().enumerate() { + if let Some(pos) = keys.iter().position(|k| k == s) { + groups[pos].push(Scalar::Integer((i + 1) as i64)); + } else { + keys.push(s.clone()); + groups.push(vec![Scalar::Integer((i + 1) as i64)]); + } + } + + // Apply f to each group + let mut results = Vec::new(); + for (key, indices) in keys.iter().zip(groups.iter()) { + let stored = StoredDfn { + body: Rc::clone(&body_rc), + source: lexer.span_str(span).to_string(), + }; + let mut dfn_env = env.clone(); + dfn_env + .vars + .insert("⍺".to_string(), Val::scalar(key.clone())); + dfn_env + .vars + .insert("⍵".to_string(), Val::vector(indices.clone())); + dfn_env.fns.insert("∇".to_string(), stored); + let result = eval(lexer, (*body_rc).clone(), &mut dfn_env)?; + results.extend(result.data); + } + + Ok(Val::vector(results)) + } + Expr::PowerOp { + span, + body, + count, + arg, + } => { + debug!("Power Operator (dfn)"); + let count_val = eval(lexer, *count, env)?; + let n: usize = count_val.data[0] + .clone() + .try_into() + .map_err(|_| (span, "Power operator count must be a non-negative integer"))?; + let mut current = eval(lexer, *arg, env)?; + let body_rc = Rc::new(*body); + for _ in 0..n { + let stored = StoredDfn { + body: Rc::clone(&body_rc), + source: lexer.span_str(span).to_string(), + }; + let mut dfn_env = env.clone(); + dfn_env.vars.insert("⍵".to_string(), current); + dfn_env.fns.insert("∇".to_string(), stored); + current = eval(lexer, (*body_rc).clone(), &mut dfn_env)?; + } + Ok(current) + } Expr::DyadicDfn { span, lhs, @@ -1043,6 +1676,74 @@ pub fn eval( dfn_env.fns.insert("∇".to_string(), stored); eval(lexer, (*body_rc).clone(), &mut dfn_env) } + Expr::ComposeDfn { span: _, f, g, arg } => { + debug!("Compose (monadic)"); + let arg_val = eval(lexer, *arg, env)?; + // First apply g monadically + let mut g_env = env.clone(); + g_env.vars.insert("⍵".to_string(), arg_val); + let g_result = eval(lexer, *g, &mut g_env)?; + // Then apply f monadically + let mut f_env = env.clone(); + f_env.vars.insert("⍵".to_string(), g_result); + eval(lexer, *f, &mut f_env) + } + Expr::ComposeDyadicDfn { + span: _, + lhs, + f, + g, + arg, + } => { + debug!("Compose (dyadic)"); + let lhs_val = eval(lexer, *lhs, env)?; + let arg_val = eval(lexer, *arg, env)?; + // Apply g monadically to right arg + let mut g_env = env.clone(); + g_env.vars.insert("⍵".to_string(), arg_val); + let g_result = eval(lexer, *g, &mut g_env)?; + // Apply f dyadically with left and g's result + let mut f_env = env.clone(); + f_env.vars.insert("⍺".to_string(), lhs_val); + f_env.vars.insert("⍵".to_string(), g_result); + eval(lexer, *f, &mut f_env) + } + Expr::OverDfn { span: _, f, g, arg } => { + debug!("Over (monadic)"); + let arg_val = eval(lexer, *arg, env)?; + // Apply g monadically to arg + let mut g_env = env.clone(); + g_env.vars.insert("⍵".to_string(), arg_val); + let g_result = eval(lexer, *g, &mut g_env)?; + // Apply f monadically to g's result + let mut f_env = env.clone(); + f_env.vars.insert("⍵".to_string(), g_result); + eval(lexer, *f, &mut f_env) + } + Expr::OverDyadicDfn { + span: _, + lhs, + f, + g, + arg, + } => { + debug!("Over (dyadic)"); + let lhs_val = eval(lexer, *lhs, env)?; + let arg_val = eval(lexer, *arg, env)?; + // Apply g to BOTH arguments + let g_clone = (*g).clone(); + let mut g_env_l = env.clone(); + g_env_l.vars.insert("⍵".to_string(), lhs_val); + let g_lhs = eval(lexer, *g, &mut g_env_l)?; + let mut g_env_r = env.clone(); + g_env_r.vars.insert("⍵".to_string(), arg_val); + let g_rhs = eval(lexer, g_clone, &mut g_env_r)?; + // Apply f dyadically to the two results + let mut f_env = env.clone(); + f_env.vars.insert("⍺".to_string(), g_lhs); + f_env.vars.insert("⍵".to_string(), g_rhs); + eval(lexer, *f, &mut f_env) + } Expr::SelfCall { span, arg } => { debug!("Self-reference ∇"); let arg_val = eval(lexer, *arg, env)?; @@ -1177,6 +1878,48 @@ pub fn eval( } Ok(Val::vector(groups)) } + Expr::PartitionedEnclose { span, lhs, rhs } => { + debug!("Partitioned Enclose"); + let lhs_eval = eval(lexer, *lhs, env)?; + let rhs_eval = eval(lexer, *rhs, env)?; + + if lhs_eval.data.len() != rhs_eval.data.len() { + return Err(( + span, + "Partitioned enclose: left and right must be same length", + )); + } + + let mut partitions: Vec> = Vec::new(); + let mut current: Option> = None; + + for (mask, elem) in lhs_eval.data.iter().zip(rhs_eval.data.iter()) { + let m: f64 = mask.clone().into(); + if m >= 1.0 { + // Start new partition (save current if exists) + if let Some(part) = current.take() { + partitions.push(part); + } + current = Some(vec![elem.clone()]); + } else if m == 0.0 && current.is_some() { + // Continue current partition + current.as_mut().unwrap().push(elem.clone()); + } + // If m == 0 and no current partition, element is dropped + } + + // Don't forget the last partition + if let Some(part) = current { + partitions.push(part); + } + + let data: Vec = partitions + .into_iter() + .map(|p| Scalar::Nested(Box::new(Val::vector(p)))) + .collect(); + + Ok(Val::vector(data)) + } Expr::MonadicEach { span, func, arg } => { debug!("Monadic Each: {func}"); let arg_eval = eval(lexer, *arg, env)?; @@ -1611,6 +2354,87 @@ pub fn eval( Ok(Val::new(vec![n, b_cols], data)) } } + Expr::Left { span: _, lhs, rhs } => { + debug!("Dyadic Left"); + let lhs_eval = eval(lexer, *lhs, env)?; + let _rhs_eval = eval(lexer, *rhs, env)?; + Ok(lhs_eval) + } + Expr::Right { span: _, lhs, rhs } => { + debug!("Dyadic Right"); + let _lhs_eval = eval(lexer, *lhs, env)?; + let rhs_eval = eval(lexer, *rhs, env)?; + Ok(rhs_eval) + } + Expr::LeftIdentity { span: _, arg } => { + debug!("Monadic Left (identity)"); + eval(lexer, *arg, env) + } + Expr::RightIdentity { span: _, arg } => { + debug!("Monadic Right (identity)"); + eval(lexer, *arg, env) + } + Expr::Tally { span: _, arg } => { + debug!("Monadic Tally"); + let arg_eval = eval(lexer, *arg, env)?; + let tally = if arg_eval.shape.is_empty() { + 1 + } else { + arg_eval.shape[0] + }; + Ok(Val::scalar(Scalar::Integer(tally as i64))) + } + Expr::Depth { span: _, arg } => { + debug!("Monadic Depth"); + let arg_eval = eval(lexer, *arg, env)?; + Ok(Val::scalar(Scalar::Integer(arg_eval.depth() as i64))) + } + Expr::Match { span: _, lhs, rhs } => { + debug!("Dyadic Match"); + let lhs_eval = eval(lexer, *lhs, env)?; + let rhs_eval = eval(lexer, *rhs, env)?; + Ok(Val::scalar(Scalar::Integer( + if lhs_eval.matches_val(&rhs_eval) { + 1 + } else { + 0 + }, + ))) + } + Expr::NotMatch { span: _, lhs, rhs } => { + debug!("Dyadic Not Match"); + let lhs_eval = eval(lexer, *lhs, env)?; + let rhs_eval = eval(lexer, *rhs, env)?; + Ok(Val::scalar(Scalar::Integer( + if lhs_eval.matches_val(&rhs_eval) { + 0 + } else { + 1 + }, + ))) + } + Expr::Find { span: _, lhs, rhs } => { + debug!("Dyadic Find"); + let lhs_eval = eval(lexer, *lhs, env)?; + let rhs_eval = eval(lexer, *rhs, env)?; + let pattern = &lhs_eval.data; + let data = &rhs_eval.data; + let plen = pattern.len(); + let dlen = data.len(); + let mut result = vec![Scalar::Integer(0); dlen]; + if plen > 0 && plen <= dlen { + for i in 0..=(dlen - plen) { + if data[i..i + plen] + .iter() + .zip(pattern.iter()) + .all(|(a, b)| a == b) + { + result[i] = Scalar::Integer(1); + } + } + } + Ok(Val::new(rhs_eval.shape.clone(), result)) + } Expr::StringLiteral { span } => { debug!("String Literal"); let raw = lexer.span_str(span); @@ -1623,6 +2447,83 @@ pub fn eval( Ok(Val::vector(data)) } } + Expr::Commute { + span, + lhs, + operator, + rhs, + } => { + debug!("Dyadic Commute"); + let lhs_eval = eval(lexer, *lhs, env)?; + let rhs_eval = eval(lexer, *rhs, env)?; + let op_fn = get_operator_fn(operator); + // Swap: apply as (rhs op lhs) instead of (lhs op rhs) + apply_dyadic_operation(span, &rhs_eval, &lhs_eval, |a, b| { + op_fn(a, b).ok_or_eyre("Commute operation failed") + }) + } + Expr::Selfie { + span, + operator, + arg, + } => { + debug!("Monadic Selfie"); + let arg_eval = eval(lexer, *arg, env)?; + let op_fn = get_operator_fn(operator); + // Apply as (arg op arg) + apply_dyadic_operation(span, &arg_eval, &arg_eval, |a, b| { + op_fn(a, b).ok_or_eyre("Selfie operation failed") + }) + } + Expr::Split { span: _, arg } => { + debug!("Monadic Split"); + let arg_eval = eval(lexer, *arg, env)?; + if arg_eval.shape.len() <= 1 { + // Vector or scalar: each element becomes a nested scalar + let data = arg_eval + .data + .into_iter() + .map(|s| Scalar::Nested(Box::new(Val::scalar(s)))) + .collect::>(); + Ok(Val::vector(data)) + } else { + let rows = arg_eval.shape[0]; + let cell_shape = arg_eval.shape[1..].to_vec(); + let cell_size: usize = cell_shape.iter().product(); + let data = (0..rows) + .map(|i| { + let start = i * cell_size; + let cell = Val::new( + cell_shape.clone(), + arg_eval.data[start..start + cell_size].to_vec(), + ); + Scalar::Nested(Box::new(cell)) + }) + .collect(); + Ok(Val::vector(data)) + } + } + Expr::Mix { span, arg } => { + debug!("Monadic Mix"); + let arg_eval = eval(lexer, *arg, env)?; + let cells: Vec = arg_eval + .data + .iter() + .filter_map(|s| match s { + Scalar::Nested(v) => Some((**v).clone()), + _ => None, + }) + .collect(); + if cells.is_empty() { + return Ok(arg_eval); + } + let cell_shape = cells[0].shape.clone(); + let mut shape = vec![cells.len()]; + shape.extend_from_slice(&cell_shape); + let data = cells.into_iter().flat_map(|v| v.data).collect(); + let _ = span; + Ok(Val::new(shape, data)) + } Expr::ScalarFloat { span, .. } => { debug!("Scalar Float"); lexer diff --git a/apiel/src/parse/mod.rs b/apiel/src/parse/mod.rs index 15c1ca9..9ca953e 100644 --- a/apiel/src/parse/mod.rs +++ b/apiel/src/parse/mod.rs @@ -20,7 +20,336 @@ pub fn parse_and_evaluate_with_env(line: &str, env: &mut Env) -> Result eval_to_val(line, env).map(|val| val.data.into_iter().map(f64::from).collect()) } +// --- Token-level train rewriting --- + +/// A token with its byte span and text, extracted from the lexer. +struct Tok<'a> { + start: usize, + end: usize, + text: &'a str, +} + +/// Is this token text a primitive dyadic operator (usable in trains)? +fn is_operator_tok(t: &str) -> bool { + matches!( + t, + "+" | "-" + | "×" + | "÷" + | "*" + | "⍟" + | "○" + | "!" + | "?" + | "|" + | "⌈" + | "⌊" + | "=" + | "≠" + | "<" + | ">" + | "≤" + | "≥" + | "∧" + | "∨" + | "⍲" + | "⍱" + | "," + ) +} + +/// Is this token text a monadic-only function (usable in trains)? +fn is_monadic_fn_tok(t: &str) -> bool { + matches!( + t, + "⍴" | "⌽" + | "⍳" + | "⍋" + | "⍒" + | "≢" + | "≡" + | "∪" + | "⊃" + | "⊂" + | "⍉" + | "~" + | "⊣" + | "⊢" + | "⌹" + | "⍸" + | "⍷" + | "↑" + | "↓" + | "∊" + | "⊆" + | "⌷" + ) +} + +/// Is this token text a pre-composed reduction (single lexer token)? +fn is_builtin_reduce_tok(t: &str) -> bool { + matches!(t, "⌈/" | "⌊/") +} + +/// Is this token text a NAME (identifier)? +fn is_name_tok(t: &str) -> bool { + let mut chars = t.chars(); + match chars.next() { + Some(c) if c.is_ascii_alphabetic() || c == '_' => { + chars.all(|c| c.is_ascii_alphanumeric() || c == '_') + } + _ => false, + } +} + +/// Is this token text a value (NOT function-like)? +fn is_value_tok(t: &str) -> bool { + // INT, FLOAT, VEC, STRING, OMEGA, ALPHA, braces, brackets, assignment, etc. + if t == "⍵" || t == "⍺" || t == "←" || t == "⋄" || t == ":" { + return true; + } + if t.starts_with('\'') { + return true; // STRING + } + if t == "{" || t == "}" || t == "[" || t == "]" { + return true; + } + // Numeric: starts with digit or ¯ followed by digit + let first = t.chars().next().unwrap_or(' '); + if first.is_ascii_digit() { + return true; + } + if first == '¯' && t.len() > 1 { + return true; + } + false +} + +/// A function reference parsed from the token stream. +enum TrainFn { + /// A primitive operator or monadic function: "+", "≢", "⍴", etc. + Simple(String), + /// A derived function (reduce/scan): "+/", "×\", "⌈/", etc. + Derived(String), + /// A named user-defined function + Named(String), +} + +impl TrainFn { + /// Build monadic application string: `f⍵` + fn apply_monadic(&self) -> String { + match self { + TrainFn::Simple(f) | TrainFn::Derived(f) => format!("{f}⍵"), + TrainFn::Named(f) => format!("{f} ⍵"), + } + } + + /// Return the text of this function reference (for dyadic use between results). + fn text(&self) -> &str { + match self { + TrainFn::Simple(f) | TrainFn::Derived(f) | TrainFn::Named(f) => f, + } + } +} + +/// Try to parse a sequence of tokens (between parens) as train function references. +/// Returns None if any token is value-like or the count isn't 2 or 3. +fn try_parse_train(tokens: &[Tok]) -> Option> { + if tokens.is_empty() { + return None; + } + + // Reject if any value-like token is present + if tokens.iter().any(|t| is_value_tok(t.text)) { + return None; + } + + // Reject if nested parens/braces are present + if tokens + .iter() + .any(|t| matches!(t.text, "(" | ")" | "{" | "}")) + { + return None; + } + + let mut fns: Vec = Vec::new(); + let mut i = 0; + + while i < tokens.len() { + let t = tokens[i].text; + + // Built-in reductions that are single tokens (⌈/ ⌊/) + if is_builtin_reduce_tok(t) { + fns.push(TrainFn::Derived(t.to_string())); + i += 1; + continue; + } + + // Operator possibly followed by / or \ or ⌿ or ⍀ (reduce/scan variants) + if is_operator_tok(t) { + if i + 1 < tokens.len() && matches!(tokens[i + 1].text, "/" | "\\" | "⌿" | "⍀") { + fns.push(TrainFn::Derived(format!("{}{}", t, tokens[i + 1].text))); + i += 2; + continue; + } + fns.push(TrainFn::Simple(t.to_string())); + i += 1; + continue; + } + + // Monadic-only function + if is_monadic_fn_tok(t) { + fns.push(TrainFn::Simple(t.to_string())); + i += 1; + continue; + } + + // NAME (user-defined function) + if is_name_tok(t) { + fns.push(TrainFn::Named(t.to_string())); + i += 1; + continue; + } + + // Unrecognized token -> not a train + return None; + } + + if fns.len() == 2 || fns.len() == 3 { + Some(fns) + } else { + None + } +} + +/// Build a monadic dfn string from train function references. +fn build_train_dfn_monadic(fns: &[TrainFn]) -> String { + if fns.len() == 3 { + // Fork: (f g h) -> {(f⍵)g(h⍵)} + let f_app = fns[0].apply_monadic(); + let g = fns[1].text(); + let h_app = fns[2].apply_monadic(); + format!("{{({f_app}){g}({h_app})}}") + } else { + // Atop: (f g) -> {f(g⍵)} + let f = fns[0].text(); + let g_app = fns[1].apply_monadic(); + format!("{{{f}({g_app})}}") + } +} + +/// Build a dyadic dfn string from train function references. +fn build_train_dfn_dyadic(fns: &[TrainFn]) -> String { + if fns.len() == 3 { + // Fork: ⍺(f g h)⍵ -> {(⍺ f ⍵)g(⍺ h ⍵)} + let f = fns[0].text(); + let g = fns[1].text(); + let h = fns[2].text(); + format!("{{(⍺{f}⍵){g}(⍺{h}⍵)}}") + } else { + // Atop: ⍺(f g)⍵ -> {f(⍺ g ⍵)} + let f = fns[0].text(); + let g = fns[1].text(); + format!("{{{f}(⍺{g}⍵)}}") + } +} + +/// Check if a token text represents a value (could be left arg of a dyadic train). +fn is_left_arg_tok(t: &str) -> bool { + if t == ")" || t == "]" || t == "⍵" || t == "⍺" { + return true; + } + if t.starts_with('\'') { + return true; // STRING + } + let first = t.chars().next().unwrap_or(' '); + if first.is_ascii_digit() || first == '¯' { + return true; + } + // NAME (identifier) — could be variable as left arg + if is_name_tok(t) { + return true; + } + false +} + +/// Rewrite train patterns using token-level analysis. +/// +/// Tokenizes the input with the lexer, identifies parenthesized groups containing +/// only function-like tokens (operators, monadic functions, derived functions, names), +/// and rewrites them as dfn expressions. +fn rewrite_trains(input: &str) -> String { + let lexerdef = apiel_l::lexerdef(); + let lexer = lexerdef.lexer(input); + + // Collect tokens with byte spans + let tokens: Vec = lexer + .iter() + .filter_map(|r| r.ok()) + .map(|tok| { + let s = tok.span(); + Tok { + start: s.start(), + end: s.end(), + text: &input[s.start()..s.end()], + } + }) + .collect(); + + // Find parenthesized groups and check for trains + // Collect (paren_open_byte, paren_close_byte_end, replacement_string) + let mut replacements: Vec<(usize, usize, String)> = Vec::new(); + + let mut i = 0; + while i < tokens.len() { + if tokens[i].text == "(" { + // Find matching ) at depth 0 + let mut depth = 1; + let mut j = i + 1; + while j < tokens.len() && depth > 0 { + match tokens[j].text { + "(" => depth += 1, + ")" => depth -= 1, + _ => {} + } + if depth > 0 { + j += 1; + } + } + if depth == 0 && j > i + 1 { + // Inner tokens: i+1 .. j (exclusive of parens) + let inner = &tokens[i + 1..j]; + if let Some(fns) = try_parse_train(inner) { + // Check if previous token is a value (dyadic context) + let is_dyadic = i > 0 && is_left_arg_tok(tokens[i - 1].text); + let replacement = if is_dyadic { + build_train_dfn_dyadic(&fns) + } else { + build_train_dfn_monadic(&fns) + }; + replacements.push((tokens[i].start, tokens[j].end, replacement)); + i = j + 1; + continue; + } + } + } + i += 1; + } + + if replacements.is_empty() { + return input.to_string(); + } + + // Apply replacements in reverse order so byte offsets stay valid + let mut result = input.to_string(); + for (start, end, replacement) in replacements.into_iter().rev() { + result.replace_range(start..end, &replacement); + } + result +} + pub fn eval_to_val(line: &str, env: &mut Env) -> Result { + let line = &rewrite_trains(line); let lexerdef = apiel_l::lexerdef(); let lexer = lexerdef.lexer(line); diff --git a/apiel/src/parse/val.rs b/apiel/src/parse/val.rs index 9aa6a87..f0457d0 100644 --- a/apiel/src/parse/val.rs +++ b/apiel/src/parse/val.rs @@ -298,6 +298,43 @@ impl Val { self.shape.is_empty() } + pub fn depth(&self) -> usize { + if self.is_scalar() { + match &self.data[0] { + Scalar::Nested(inner) => 1 + inner.depth(), + _ => 0, + } + } else { + let has_nested = self.data.iter().any(|s| matches!(s, Scalar::Nested(_))); + if has_nested { + 1 + self + .data + .iter() + .map(|s| match s { + Scalar::Nested(inner) => inner.depth(), + _ => 0, + }) + .max() + .unwrap_or(0) + } else { + 1 + } + } + } + + pub fn matches_val(&self, other: &Val) -> bool { + self.shape == other.shape + && self.data.len() == other.data.len() + && self + .data + .iter() + .zip(other.data.iter()) + .all(|(a, b)| match (a, b) { + (Scalar::Nested(va), Scalar::Nested(vb)) => va.matches_val(vb), + _ => a == b, + }) + } + pub fn from_f64s(values: &[f64]) -> Self { let data: Vec = values .iter() diff --git a/apiel/tests/reference.rs b/apiel/tests/reference.rs index 4822a9f..2de0c11 100644 --- a/apiel/tests/reference.rs +++ b/apiel/tests/reference.rs @@ -228,6 +228,17 @@ fn reference_tests() { ("⍴ ⍉ 2 3 ⍴ ⍳ 6", &[3.0, 2.0], "shape of transposed"), ("⍉ 1 2 3", &[1.0, 2.0, 3.0], "transpose vector noop"), ("⍉ 5", &[5.0], "transpose scalar noop"), + // Dyadic transpose ⍉ + ( + "1 2 ⍉ 2 3 ⍴ ⍳ 6", + &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + "dyadic transpose: identity perm", + ), + ( + "2 1 ⍉ 2 3 ⍴ ⍳ 6", + &[1.0, 4.0, 2.0, 5.0, 3.0, 6.0], + "dyadic transpose: swap axes", + ), // Bool ops (∧ ∨ ⍲ ⍱) ("1 ∧ 1", &[1.0], "and 1 1"), ("1 ∧ 0", &[0.0], "and 1 0"), @@ -407,6 +418,108 @@ fn reference_tests() { &[10.0, 30.0, 50.0], "index vector", ), + // Identity functions ⊣ ⊢ + ("⊢ 42", &[42.0], "monadic right tack (identity)"), + ("⊢ 1 2 3", &[1.0, 2.0, 3.0], "monadic right tack vector"), + ("⊣ 42", &[42.0], "monadic left tack (identity)"), + ("⊣ 1 2 3", &[1.0, 2.0, 3.0], "monadic left tack vector"), + ("5 ⊢ 42", &[42.0], "dyadic right tack returns right"), + ("5 ⊣ 42", &[5.0], "dyadic left tack returns left"), + ( + "1 2 3 ⊢ 4 5 6", + &[4.0, 5.0, 6.0], + "dyadic right tack vectors", + ), + ( + "1 2 3 ⊣ 4 5 6", + &[1.0, 2.0, 3.0], + "dyadic left tack vectors", + ), + // Tally ≢ + ("≢ 1 2 3 4 5", &[5.0], "tally of vector"), + ("≢ 42", &[1.0], "tally of scalar"), + // Find ⍷ + ( + "2 3 ⍷ 1 2 3 4 5", + &[0.0, 1.0, 0.0, 0.0, 0.0], + "find subsequence", + ), + ( + "5 ⍷ 1 2 3 4 5", + &[0.0, 0.0, 0.0, 0.0, 1.0], + "find single element", + ), + ( + "3 4 5 ⍷ 1 2 3 4 5", + &[0.0, 0.0, 1.0, 0.0, 0.0], + "find at end", + ), + ("9 ⍷ 1 2 3", &[0.0, 0.0, 0.0], "find missing element"), + // Extended operator reductions + ("∧/ 1 1 1 0", &[0.0], "and-reduce"), + ("∧/ 1 1 1 1", &[1.0], "and-reduce all true"), + ("∨/ 0 0 1 0", &[1.0], "or-reduce (any)"), + ("∨/ 0 0 0 0", &[0.0], "or-reduce all false"), + ("≠/ 1 0 1 1", &[1.0], "neq-reduce (parity)"), + ( + "1 2 3 ∘.≤ 1 2 3", + &[1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0], + "outer product leq", + ), + // Commute ⍨ + ("+⍨ 3", &[6.0], "selfie: 3+3"), + ("+⍨ 1 2 3", &[2.0, 4.0, 6.0], "selfie vector: double"), + ("×⍨ 1 2 3", &[1.0, 4.0, 9.0], "selfie: square"), + ("2 -⍨ 5", &[3.0], "commute: 5-2"), + ("3 ÷⍨ 12", &[4.0], "commute: 12÷3"), + ("2 *⍨ 3", &[9.0], "commute: 3*2=9"), + // Power operator ⍣ + ( + "{⍵+1}⍣3 ⍳ 5", + &[4.0, 5.0, 6.0, 7.0, 8.0], + "power: increment 3 times", + ), + ("{⍵×2}⍣4 (1)", &[16.0], "power: double 4 times"), + ("{⍵+1}⍣0 (5)", &[5.0], "power 0 is identity"), + // Compose ∘ + ("{⍵+1}∘{⍵×2} 3", &[7.0], "compose: (3×2)+1 = 7"), + ("{⍵×⍵}∘{⍵+1} 4", &[25.0], "compose: (4+1)² = 25"), + // Function trains — fork (3-train) + ("(+/ ÷ ≢) 2 4 6 8 10", &[6.0], "fork: average"), + ("(⌈/ - ⌊/) 3 1 4 1 5 9", &[8.0], "fork: range = max-min"), + ("(+/ ÷ ≢) 10 20 30", &[20.0], "fork: average of 3"), + // Function trains — atop (2-train) + ("(- ×) 3", &[-1.0], "atop: negate(signum(3))"), + ("(- ×) ¯5", &[1.0], "atop: negate(signum(-5))"), + ("(⌊ ÷) 7", &[0.0], "atop: floor(reciprocal(7))"), + // Rank operator ⍤ + ("{+/⍵}⍤1 ⊢ 2 3 ⍴ ⍳ 6", &[6.0, 15.0], "rank 1: sum each row"), + ( + "{⌽⍵}⍤1 ⊢ 2 3 ⍴ ⍳ 6", + &[3.0, 2.0, 1.0, 6.0, 5.0, 4.0], + "rank 1: reverse each row", + ), + // Over ⍥ + ("{⍵×2}⍥{⍵+1} 5", &[12.0], "over monadic: (5+1)×2 = 12"), + ("3 {⍺+⍵}⍥{⍵×⍵} 4", &[25.0], "over dyadic: 3²+4² = 25"), + // At operator @ + ( + "{⍵×10}@(2 3) ⊢ ⍳ 5", + &[1.0, 20.0, 30.0, 4.0, 5.0], + "at: multiply at indices", + ), + ( + "{0}@(1 3 5) ⊢ ⍳ 5", + &[0.0, 2.0, 0.0, 4.0, 0.0], + "at: replace at indices", + ), + // Key operator ⌸ + ( + "{≢⍵}⌸ 1 1 2 3 3 3", + &[2.0, 1.0, 3.0], + "key: count each group", + ), + ("{⍺}⌸ 1 1 2 3 3 3", &[1.0, 2.0, 3.0], "key: unique keys"), ]; let mut failures = Vec::new(); @@ -432,6 +545,239 @@ fn reference_tests() { } } +/// All examples from https://aplwiki.com/wiki/Simple_examples +/// Unsupported examples are commented out with the reason. +#[test] +fn aplwiki_simple_examples() { + let mut env = Env::new(); + + // --- Averaging --- + + // Ex 1: Average function definition (just a dfn, no invocation — no output to test) + // {(+⌿⍵)÷≢⍵} + + // Ex 2: +⌿ 1 2 3 4 5 6 → 21 + assert_apl("+⌿ 1 2 3 4 5 6", &[21.0], "wiki ex2: sum reduce first"); + + // Ex 3: 1+2+3+4+5+6 → 21 + assert_apl("1+2+3+4+5+6", &[21.0], "wiki ex3: chained addition"); + + // Ex 4: {⍺,', ',⍵}⌿ — partial application, no output + // Requires ⌿ and string array literals — skipped + + // Ex 5: {⍺,', ',⍵}⌿'cow' 'sheep' 'cat' 'dog' + // Requires ⌿ and array-of-strings syntax — skipped + + // Ex 6: {(+⌿⍵)÷≢⍵} 3 4.5 7 21 → 8.875 + // Adapted: mixed int/float vector not supported by VEC lexer, using all ints + assert_apl("{(+⌿⍵)÷≢⍵} 2 4 6 8 10", &[6.0], "wiki ex6: average via dfn"); + + // Ex 7: (+⌿÷≢) 3 4.5 7 21 → 8.875 + // Adapted: all ints (VEC lexer limitation) + assert_apl("(+⌿ ÷ ≢) 2 4 6 8 10", &[6.0], "wiki ex7: average via fork"); + + // Ex 8: Same as 7, just showing spacing + // Already covered by ex7 + + // Ex 9: (+⌿ 3 4.5 7 21) ÷ (≢ 3 4.5 7 21) → 8.875 + // Adapted: all ints + assert_apl( + "(+⌿ 2 4 6 8 10) ÷ (≢ 2 4 6 8 10)", + &[6.0], + "wiki ex9: average expanded", + ); + + // Ex 10-11: Pseudocode explaining forks — not testable + // (f g h) ⍵ ↔ (f ⍵) g (h ⍵) + + // --- Comma-separated text --- + + // Ex 12: ','≠'comma,delimited,text' → 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 0 1 1 1 1 + assert_apl( + "','≠'comma,delimited,text'", + &[ + 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, + 1.0, 1.0, 1.0, + ], + "wiki ex12: comma not-equal mask", + ); + + // Ex 13: ','⊢'comma,delimited,text' → comma,delimited,text + let val = eval_to_val("','⊢'comma,delimited,text'", &mut env).unwrap(); + assert_eq!( + format_val(&val), + "comma,delimited,text", + "wiki ex13: right tack" + ); + + // Ex 14: 1 1 0 1 1 1⊆'Hello!' → 'He' 'lo!' + let val = eval_to_val("1 1 0 1 1 1⊆'Hello!'", &mut env).unwrap(); + assert_eq!( + format_val(&val), + "(He) (lo!)", + "wiki ex14: partition string" + ); + + // Ex 15: ','(≠⊆⊢)'comma,delimited,text' → 'comma' 'delimited' 'text' + // Dyadic fork: ⍺(f g h)⍵ = (⍺ f ⍵) g (⍺ h ⍵) + // = (','≠'comma,...') ⊆ (','⊢'comma,...') + // = boolean_mask ⊆ original_string + let val = eval_to_val("','(≠ ⊆ ⊢)'comma,delimited,text'", &mut env).unwrap(); + assert_eq!( + format_val(&val), + "(comma) (delimited) (text)", + "wiki ex15: dyadic fork split CSV" + ); + + // Ex 16: (','≠s)⊂s←'comma,delimited,text' + // Multi-statement: assign s, then partitioned enclose + // With ⊂, each 1 in the mask starts a new partition, 0 continues. + // The mask ','≠s has 1s at non-comma positions, so each character starts + // its own group (since consecutive 1s each start a new partition in ⊂). + // This is different from ⊆ (partition) which groups consecutive 1s. + // (The wiki shows this expression without expected output.) + eval_to_val("s←'comma,delimited,text'", &mut env).unwrap(); + let val = eval_to_val("(','≠s)⊂s", &mut env).unwrap(); + assert_eq!( + val.data.len(), + 18, + "wiki ex16: 18 partitions (one per non-comma char)" + ); + + // --- Membership --- + + // Ex 17: 'mississippi'∊'sp' → 0 0 1 1 0 1 1 0 1 1 0 + assert_apl( + "'mississippi'∊'sp'", + &[0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0], + "wiki ex17: membership", + ); + + // Ex 18: ⍸'mississippi'∊'sp' → 3 4 6 7 9 10 + assert_apl( + "⍸'mississippi'∊'sp'", + &[3.0, 4.0, 6.0, 7.0, 9.0, 10.0], + "wiki ex18: where membership", + ); + + // Ex 19: 'mississippi' (⍸∊) 'sp' → 3 4 6 7 9 10 + // Dyadic atop: ⍺(⍸∊)⍵ = ⍸(⍺∊⍵) + assert_apl( + "'mississippi' (⍸ ∊) 'sp'", + &[3.0, 4.0, 6.0, 7.0, 9.0, 10.0], + "wiki ex19: dyadic atop where-membership", + ); + + // --- Outer product with characters --- + + // Ex 20: 'abcd' ∘.= 'cabbage' → 4×7 boolean matrix + assert_apl( + "'abcd' ∘.= 'cabbage'", + &[ + 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + ], + "wiki ex20: outer product char equality", + ); + + // Ex 21: +/ 'abcd' ∘.= 'cabbage' → 2 2 1 0 + assert_apl( + "+/ 'abcd' ∘.= 'cabbage'", + &[2.0, 2.0, 1.0, 0.0], + "wiki ex21: letter frequency", + ); + + // --- Bracket matching --- + + // Ex 22: '()'∘.='plus(square(a),...' → 2×49 boolean matrix + let val = eval_to_val( + "'()'∘.='plus(square(a),plus(square(b),times(2,plus(a,b)))'", + &mut env, + ) + .unwrap(); + assert_eq!( + val.shape, + vec![2, 49], + "wiki ex22: bracket outer product shape" + ); + + // Ex 23: -⌿'()'∘.=... → nesting delta (row0 - row1 column-wise) + assert_apl( + "-⌿'()'∘.='plus(square(a),plus(square(b),times(2,plus(a,b)))'", + &[ + 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -1.0, -1.0, -1.0, + ], + "wiki ex23: bracket nesting delta", + ); + + // Ex 24: +\-⌿'()'∘.=... → cumulative nesting depth + assert_apl( + "+\\-⌿'()'∘.='plus(square(a),plus(square(b),times(2,plus(a,b)))'", + &[ + 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 4.0, 4.0, 4.0, 4.0, 3.0, 2.0, 1.0, + ], + "wiki ex24: bracket nesting depth", + ); + + // Ex 25: 'ABBA'⍳'ABC' → 1 2 5 + assert_apl( + "'ABBA'⍳'ABC'", + &[1.0, 2.0, 5.0], + "wiki ex25a: index of chars", + ); + + // Ex 25 (part 2): '()'⍳'plus(square...' → bracket position mapping + assert_apl( + "'()'⍳'plus(square(a),plus(square(b),times(2,plus(a,b)))'", + &[ + 3.0, 3.0, 3.0, 3.0, 1.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 1.0, 3.0, 2.0, 3.0, 3.0, 3.0, + 3.0, 3.0, 1.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 1.0, 3.0, 2.0, 3.0, 3.0, 3.0, 3.0, 3.0, + 3.0, 1.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 1.0, 3.0, 3.0, 3.0, 2.0, 2.0, 2.0, + ], + "wiki ex25b: bracket index mapping", + ); + + // Ex 26: 1 ¯1 0['()'⍳'plus(square...'] → nesting delta via indexing + assert_apl( + "1 ¯1 0['()'⍳'plus(square(a),plus(square(b),times(2,plus(a,b)))']", + &[ + 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -1.0, -1.0, -1.0, + ], + "wiki ex26: nesting delta via indexing", + ); + + // Ex 27: +\1 ¯1 0['()'⍳'plus(square...'] → nesting depth via scan + assert_apl( + "+\\1 ¯1 0['()'⍳'plus(square(a),plus(square(b),times(2,plus(a,b)))']", + &[ + 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0, + 1.0, 1.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 3.0, 3.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, + 2.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 4.0, 4.0, 4.0, 4.0, 3.0, 2.0, 1.0, + ], + "wiki ex27: nesting depth via scan+indexing", + ); + + // --- Cardan grille cipher --- + + // Ex 28: ⎕←(grid grille)←5 5∘⍴¨'VRYIA...' '⌺⌺⌺ ⌺...' + // Requires ⎕←, multiple assignment, and ∘⍴¨ (bind+each) + // Skipped + + // Ex 29: grid[⍸grille=' '] → ILIKEAPL + // Requires array indexing for reading and 2D indexing + // Skipped + + // Ex 30: (' '=,grille)/,grid → ILIKEAPL + // Requires the grille/grid variables from ex28 + // Skipped +} + fn assert_apl_env(expr: &str, env: &mut Env, expected: &[f64], desc: &str) { let result = apl!(expr, env).unwrap_or_else(|e| panic!("[{desc}] `{expr}` failed: {e}")); assert_eq!( @@ -481,6 +827,20 @@ fn variables_and_assignment() { &[5.0, 7.0, 9.0], "call named dyadic vector", ); + + // Named functions in trains + assert_apl_env( + "(double + ×) 3", + &mut env, + &[7.0], + "train with named fn: (double 3)+(× 3) = 6+1 = 7", + ); + assert_apl_env( + "(double - double) 5", + &mut env, + &[0.0], + "train with two named fns: (double 5)-(double 5) = 0", + ); } #[test] @@ -596,3 +956,103 @@ fn nested_arrays() { let val = eval_to_val("⌽¨ (⊂ 1 2 3) , (⊂ 4 5) , (⊂ 6)", &mut env).unwrap(); assert_eq!(format_val(&val), "(3 2 1) (5 4) (6)"); } + +#[test] +fn depth_and_match() { + let mut env = Env::new(); + + let val = eval_to_val("≡ 42", &mut env).unwrap(); + assert_eq!(format_val(&val), "0", "depth of scalar"); + + let val = eval_to_val("≡ 1 2 3", &mut env).unwrap(); + assert_eq!(format_val(&val), "1", "depth of flat vector"); + + let val = eval_to_val("≡ ⊂ 1 2 3", &mut env).unwrap(); + assert_eq!(format_val(&val), "2", "depth of enclosed vector"); + + let val = eval_to_val("1 2 3 ≡ 1 2 3", &mut env).unwrap(); + assert_eq!(format_val(&val), "1", "match identical vectors"); + + let val = eval_to_val("1 2 3 ≡ 1 2 4", &mut env).unwrap(); + assert_eq!(format_val(&val), "0", "match different vectors"); + + let val = eval_to_val("1 2 3 ≢ 1 2 4", &mut env).unwrap(); + assert_eq!(format_val(&val), "1", "not match different vectors"); + + let val = eval_to_val("1 2 3 ≢ 1 2 3", &mut env).unwrap(); + assert_eq!(format_val(&val), "0", "not match identical vectors"); + + let val = eval_to_val("(2 3 ⍴ ⍳ 6) ≡ 1 2 3 4 5 6", &mut env).unwrap(); + assert_eq!(format_val(&val), "0", "match: different shapes"); +} + +#[test] +fn mix_and_split() { + let mut env = Env::new(); + + // Split: matrix -> nested vector of rows + let val = eval_to_val("↓ 2 3 ⍴ ⍳ 6", &mut env).unwrap(); + assert_eq!(val.data.len(), 2, "split 2x3 gives 2 elements"); + assert_eq!(format_val(&val), "(1 2 3) (4 5 6)"); + + // Mix: nested vector -> matrix + let val = eval_to_val("↑ (⊂ 1 2 3),(⊂ 4 5 6)", &mut env).unwrap(); + assert_eq!(val.shape, vec![2, 3], "mix produces 2x3 matrix"); + assert_eq!(format_val(&val), "1 2 3 4 5 6"); + + // Split then mix is identity (for regular matrix) + let val = eval_to_val("↑ ↓ 2 3 ⍴ ⍳ 6", &mut env).unwrap(); + assert_eq!(val.shape, vec![2, 3], "split then mix roundtrip"); +} + +#[test] +fn partitioned_enclose() { + let mut env = Env::new(); + + let val = eval_to_val("1 0 1 0 0 ⊂ 1 2 3 4 5", &mut env).unwrap(); + assert_eq!(format_val(&val), "(1 2) (3 4 5)", "partition: two groups"); + + let val = eval_to_val("1 1 1 ⊂ 10 20 30", &mut env).unwrap(); + assert_eq!( + format_val(&val), + "(10) (20) (30)", + "partition: each element" + ); + + let val = eval_to_val("1 0 0 0 0 ⊂ 1 2 3 4 5", &mut env).unwrap(); + assert_eq!(format_val(&val), "(1 2 3 4 5)", "partition: single group"); +} + +#[test] +fn modified_assignment() { + let mut env = Env::new(); + assert_apl_env("x←5", &mut env, &[5.0], "assign x"); + assert_apl_env("x+←3", &mut env, &[8.0], "modified assign x+←3"); + assert_apl_env("x", &mut env, &[8.0], "x is now 8"); + assert_apl_env("x×←2", &mut env, &[16.0], "modified assign x×←2"); + assert_apl_env("x", &mut env, &[16.0], "x is now 16"); +} + +#[test] +fn indexed_assignment() { + let mut env = Env::new(); + assert_apl_env( + "x←1 2 3 4 5", + &mut env, + &[1.0, 2.0, 3.0, 4.0, 5.0], + "assign vector", + ); + assert_apl_env( + "x[3]←99", + &mut env, + &[1.0, 2.0, 99.0, 4.0, 5.0], + "index assign single", + ); + assert_apl_env("x", &mut env, &[1.0, 2.0, 99.0, 4.0, 5.0], "x modified"); + assert_apl_env( + "x[1 5]←0", + &mut env, + &[0.0, 2.0, 99.0, 4.0, 0.0], + "index assign multiple", + ); +} diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..d160cdc --- /dev/null +++ b/docs/index.html @@ -0,0 +1,263 @@ + + + + + + apiel - APL in Rust + + + +
+

apiel / toy APL in Rust

+ +
+
+
+ >>> + +
+
+ Enter evaluate Up/Down history + Ctrl+L clear — APL chars: + click to show keyboard +
+ +
+
+
+ + + + diff --git a/docs/pkg/apiel_wasm.d.ts b/docs/pkg/apiel_wasm.d.ts new file mode 100644 index 0000000..95d1d64 --- /dev/null +++ b/docs/pkg/apiel_wasm.d.ts @@ -0,0 +1,43 @@ +/* tslint:disable */ +/* eslint-disable */ + +export function eval_apl(input: string): string; + +export function reset_env(): void; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly eval_apl: (a: number, b: number) => [number, number]; + readonly reset_env: () => void; + readonly __wbindgen_exn_store: (a: number) => void; + readonly __externref_table_alloc: () => number; + readonly __wbindgen_externrefs: WebAssembly.Table; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; + +/** + * Instantiates the given `module`, which can either be bytes or + * a precompiled `WebAssembly.Module`. + * + * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. + * + * @returns {InitOutput} + */ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** + * If `module_or_path` is {RequestInfo} or {URL}, makes a request and + * for everything else, calls `WebAssembly.instantiate` directly. + * + * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. + * + * @returns {Promise} + */ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/docs/pkg/apiel_wasm.js b/docs/pkg/apiel_wasm.js new file mode 100644 index 0000000..983f3df --- /dev/null +++ b/docs/pkg/apiel_wasm.js @@ -0,0 +1,333 @@ +/* @ts-self-types="./apiel_wasm.d.ts" */ + +/** + * @param {string} input + * @returns {string} + */ +export function eval_apl(input) { + let deferred2_0; + let deferred2_1; + try { + const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.eval_apl(ptr0, len0); + deferred2_0 = ret[0]; + deferred2_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); + } +} + +export function reset_env() { + wasm.reset_env(); +} + +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg___wbindgen_is_function_2a95406423ea8626: function(arg0) { + const ret = typeof(arg0) === 'function'; + return ret; + }, + __wbg___wbindgen_is_object_59a002e76b059312: function(arg0) { + const val = arg0; + const ret = typeof(val) === 'object' && val !== null; + return ret; + }, + __wbg___wbindgen_is_string_624d5244bb2bc87c: function(arg0) { + const ret = typeof(arg0) === 'string'; + return ret; + }, + __wbg___wbindgen_is_undefined_87a3a837f331fef5: function(arg0) { + const ret = arg0 === undefined; + return ret; + }, + __wbg___wbindgen_throw_5549492daedad139: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg_call_8f5d7bb070283508: function() { return handleError(function (arg0, arg1, arg2) { + const ret = arg0.call(arg1, arg2); + return ret; + }, arguments); }, + __wbg_crypto_38df2bab126b63dc: function(arg0) { + const ret = arg0.crypto; + return ret; + }, + __wbg_getRandomValues_c44a50d8cfdaebeb: function() { return handleError(function (arg0, arg1) { + arg0.getRandomValues(arg1); + }, arguments); }, + __wbg_length_e6e1633fbea6cfa9: function(arg0) { + const ret = arg0.length; + return ret; + }, + __wbg_msCrypto_bd5a034af96bcba6: function(arg0) { + const ret = arg0.msCrypto; + return ret; + }, + __wbg_new_with_length_0f3108b57e05ed7c: function(arg0) { + const ret = new Uint8Array(arg0 >>> 0); + return ret; + }, + __wbg_node_84ea875411254db1: function(arg0) { + const ret = arg0.node; + return ret; + }, + __wbg_process_44c7a14e11e9f69e: function(arg0) { + const ret = arg0.process; + return ret; + }, + __wbg_prototypesetcall_3875d54d12ef2eec: function(arg0, arg1, arg2) { + Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); + }, + __wbg_randomFillSync_6c25eac9869eb53c: function() { return handleError(function (arg0, arg1) { + arg0.randomFillSync(arg1); + }, arguments); }, + __wbg_require_b4edbdcf3e2a1ef0: function() { return handleError(function () { + const ret = module.require; + return ret; + }, arguments); }, + __wbg_static_accessor_GLOBAL_8dfb7f5e26ebe523: function() { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_GLOBAL_THIS_941154efc8395cdd: function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_SELF_58dac9af822f561f: function() { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_static_accessor_WINDOW_ee64f0b3d8354c0b: function() { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addToExternrefTable0(ret); + }, + __wbg_subarray_035d32bb24a7d55d: function(arg0, arg1, arg2) { + const ret = arg0.subarray(arg1 >>> 0, arg2 >>> 0); + return ret; + }, + __wbg_versions_276b2795b1c6a219: function(arg0) { + const ret = arg0.versions; + return ret; + }, + __wbindgen_cast_0000000000000001: function(arg0, arg1) { + // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`. + const ret = getArrayU8FromWasm0(arg0, arg1); + return ret; + }, + __wbindgen_cast_0000000000000002: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }, + __wbindgen_init_externref_table: function() { + const table = wasm.__wbindgen_externrefs; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + }, + }; + return { + __proto__: null, + "./apiel_wasm_bg.js": import0, + }; +} + +function addToExternrefTable0(obj) { + const idx = wasm.__externref_table_alloc(); + wasm.__wbindgen_externrefs.set(idx, obj); + return idx; +} + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return decodeText(ptr, len); +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + const idx = addToExternrefTable0(e); + wasm.__wbindgen_exn_store(idx); + } +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +let wasmModule, wasm; +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + wasmModule = module; + cachedUint8ArrayMemory0 = null; + wasm.__wbindgen_start(); + return wasm; +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = module.ok && expectedResponseType(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { throw e; } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } + + function expectedResponseType(type) { + switch (type) { + case 'basic': case 'cors': case 'default': return true; + } + return false; + } +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (module !== undefined) { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (module_or_path !== undefined) { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (module_or_path === undefined) { + module_or_path = new URL('apiel_wasm_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync, __wbg_init as default }; diff --git a/docs/pkg/apiel_wasm_bg.wasm b/docs/pkg/apiel_wasm_bg.wasm new file mode 100644 index 0000000..d12b04e Binary files /dev/null and b/docs/pkg/apiel_wasm_bg.wasm differ diff --git a/docs/pkg/apiel_wasm_bg.wasm.d.ts b/docs/pkg/apiel_wasm_bg.wasm.d.ts new file mode 100644 index 0000000..26f8666 --- /dev/null +++ b/docs/pkg/apiel_wasm_bg.wasm.d.ts @@ -0,0 +1,12 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export const eval_apl: (a: number, b: number) => [number, number]; +export const reset_env: () => void; +export const __wbindgen_exn_store: (a: number) => void; +export const __externref_table_alloc: () => number; +export const __wbindgen_externrefs: WebAssembly.Table; +export const __wbindgen_malloc: (a: number, b: number) => number; +export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; +export const __wbindgen_free: (a: number, b: number, c: number) => void; +export const __wbindgen_start: () => void; diff --git a/docs/pkg/package.json b/docs/pkg/package.json new file mode 100644 index 0000000..733cfb4 --- /dev/null +++ b/docs/pkg/package.json @@ -0,0 +1,15 @@ +{ + "name": "apiel-wasm", + "type": "module", + "version": "0.2.0", + "files": [ + "apiel_wasm_bg.wasm", + "apiel_wasm.js", + "apiel_wasm.d.ts" + ], + "main": "apiel_wasm.js", + "types": "apiel_wasm.d.ts", + "sideEffects": [ + "./snippets/*" + ] +} \ No newline at end of file