diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000..bc8609f
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,77 @@
+name: test
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+on:
+ pull_request:
+ push:
+ branches: [main]
+
+ workflow_dispatch:
+
+defaults:
+ run:
+ shell: bash
+
+env:
+ CARGO_TERM_COLOR: always
+ CLICOLOR: 1
+
+jobs:
+ unit_test:
+ name: Unit test [${{ matrix.mode }}-rust-${{ matrix.rust }}-${{ matrix.os }}]
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-24.04
+ - macos-14
+ rust:
+ - 1.81.0
+ mode:
+ - debug
+
+ include:
+ - os: ubuntu-24.04
+ rust: 1.81.0
+ mode: release
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+ - name: Update rust
+ run: rustup install ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }}
+
+ - name: Check rust installation
+ run: rustc -vV
+ - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
+ with:
+ path: |
+ ~/.cargo/registry/index/
+ ~/.cargo/registry/cache/
+ ~/.cargo/git/db/
+ target/
+ key: ${{ runner.os }}-rust-${{ matrix.rust }}-cargo-unit-test-${{ matrix.mode }}-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Build
+ run: cargo build ${{ matrix.mode == 'release' && '--release' || '' }} --verbose
+ - name: Run tests
+ run: cargo test ${{ matrix.mode == 'release' && '--release' || '' }} --verbose
+ - name: Build API documentation
+ run: cargo doc --no-deps
+
+
+ tests_complete:
+ name: All tests
+ if: always()
+ # needs: [unit_test, execute_tutorials, build_documentation]
+ needs: [unit_test]
+ runs-on: ubuntu-latest
+
+ steps:
+ - run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'
+ - name: Done
+ run: exit 0
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..5155ca0
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,39 @@
+ci:
+ autoupdate_schedule: quarterly
+ autoupdate_branch: 'trunk'
+ autofix_prs: true
+
+default_language_version:
+ rust: 1.81.0
+
+repos:
+- repo: https://github.com/backplane/pre-commit-rust-hooks
+ rev: v1.1.0
+ hooks:
+ - id: fmt
+ - id: check
+ - id: clippy
+ args:
+ - --all-targets
+ - --all-features
+ - --
+ - -Dwarnings
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v5.0.0
+ hooks:
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - id: check-json
+ - id: check-toml
+ - id: check-yaml
+ - id: check-case-conflict
+ - id: mixed-line-ending
+- repo: https://github.com/codespell-project/codespell
+ rev: v2.4.0
+ hooks:
+ - id: codespell
+ args: ["--ignore-words-list=crate"]
+- repo: https://github.com/rhysd/actionlint
+ rev: v1.7.7
+ hooks:
+ - id: actionlint
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..33520b6
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,279 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "arrcomp"
+version = "0.1.0"
+dependencies = [
+ "rstest",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-timer"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+
+[[package]]
+name = "indexmap"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "relative-path"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
+
+[[package]]
+name = "rstest"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89"
+dependencies = [
+ "futures-timer",
+ "futures-util",
+ "rstest_macros",
+ "rustc_version",
+]
+
+[[package]]
+name = "rstest_macros"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b"
+dependencies = [
+ "cfg-if",
+ "glob",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "relative-path",
+ "rustc_version",
+ "syn",
+ "unicode-ident",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.96"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+
+[[package]]
+name = "toml_edit"
+version = "0.22.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
+
+[[package]]
+name = "winnow"
+version = "0.6.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
+dependencies = [
+ "memchr",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..9455a9d
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "arrcomp"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+
+[dev-dependencies]
+rstest = "0.24.0"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b467bf1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2025 Jenna Bradley
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index 89d18d8..d4f993e 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,81 @@
-# arrcomp
\ No newline at end of file
+
+
+List comprehension-style syntax for creating Rust array using declarative macros.
+
+In contrast to most Rust packages of this sort, arrcomp exclusively creates fixed size
+arrays without an intermediate heap allocation. This is more performant than standard
+Vector-based approaches, but places a few additional restrictions on the allowed syntax.
+
+`arrcomp` Syntax
+----------------
+
+```rust
+use arrcomp::arr;
+
+let incremented = arr![x + 1, for x in 0..10; len 10];
+// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+
+let incremented_if_odd = arr![x + 1, for x in 0..10, if x % 2 == 1; len 10];
+// [None, Some(2), None, Some(4), None, Some(6), None, Some(8), None, Some(10)]
+```
+
+This Rust adaption provides a familiar and performant interface for creating and
+modifying fixed-size arrays. `Option` types allow the use of filters even in cases where
+the number of unfiltered outputs is unknown at compile time -- without any dynamic
+allocations!
+
+The `arr!` pattern is generally expressed as `f(x), for x in iterable, if condition; len N`,
+where `f(x)` and `iterable` are any [statement](https://doc.rust-lang.org/reference/statements.html),
+`condition` is any statement that evaluates to a `bool`, and and `x` is any [pattern](https://doc.rust-lang.org/reference/patterns.html). Unlike Python, we must also provide a const `N` matching
+the length of the provided iterable in order to ensure the output can be sized at compile time.
+
+Why this crate?
+===============
+
+When working with vectors, list comprehensions are most naturally expressed as
+`filter/flat_map`s in Rust. Consider the following example, which uses `filter` and
+`map` for clarity:
+
+```rust
+use arrcomp::arr;
+let incremented_vec: Vec<_> = (0..10).filter(|x| x % 2 == 1).map(|x| x + 1).collect();
+
+// Converting to an array is simple. Note we have to provide the correct array length
+let incremented_arr: [i32; 5] = incremented_vec.clone().try_into().unwrap();
+assert_eq!(incremented_arr.to_vec(), incremented_vec);
+```
+
+Note that, in the example above, we dynamically allocate a vector that gets converted to
+an array. In performance-critical contexts this is undesirable. Fortunately, there is
+another way:
+
+```rust
+# use arrcomp::arr;
+let mut incremented_iter = (0..10)
+ .into_iter()
+ .map(|x| if x % 2 == 1 { Some(x+1) } else { None });
+
+let iter_copy = incremented_iter.clone();
+
+// std::array::from_fn lets us generate the array without collecting into a vector
+let arr_without_allocation: [Option; 10] = std::array::from_fn(
+ |_| incremented_iter.next().unwrap()
+);
+
+assert_eq!(arr_without_allocation.to_vec(), iter_copy.collect::>());
+```
+
+The default Rust syntax is a little clunky, and the inputs to `std::array::from_fn` are
+limited. Furthermore, an additional variable must be created outside the function call,
+as cloning the iterator inside `from_fn` resets the iterator to the beginning.
+
+An array comprehension provides an attractive alternative to this pattern, with a
+simplified syntax that allows for arbitrary expressions for our input iterable.
+
+```rust
+# use arrcomp::arr;
+let incremented_vec = (0..10).map(|x| if x % 2 == 1 { Some(x+1) } else { None });
+
+let arr_comprehension = arr![x+1, for x in 0..10, if x % 2 == 1; len 10];
+assert_eq!(arr_comprehension.to_vec(), incremented_vec.collect::>());
+```
diff --git a/src/ims/arrcomp.svg b/src/ims/arrcomp.svg
new file mode 100644
index 0000000..692fadb
--- /dev/null
+++ b/src/ims/arrcomp.svg
@@ -0,0 +1,33 @@
+
+
+
diff --git a/src/ims/arrcomp_anim.svg b/src/ims/arrcomp_anim.svg
new file mode 100644
index 0000000..b62b2a4
--- /dev/null
+++ b/src/ims/arrcomp_anim.svg
@@ -0,0 +1,169 @@
+
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..89c1add
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,399 @@
+#![doc(html_logo_url = "https://github.com/janbridley/arrcomp/blob/main/src/ims/arrcomp.svg")]
+
+/*!
+List comprehension-style syntax for creating Rust array using declarative macros.
+
+In contrast to most Rust packages of this sort, arrcomp exclusively creates fixed size
+arrays without an intermediate heap allocation. This is more performant than standard
+Vector-based approaches, but places a few additional restrictions on the allowed syntax.
+
+`arrcomp` Syntax
+----------------
+
+```rust
+use arrcomp::arr;
+
+# fn main() {
+let incremented = arr![x + 1, for x in 0..10; len 10];
+// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+
+let incremented_if_odd = arr![x + 1, for x in 0..10, if x % 2 == 1; len 10];
+// [None, Some(2), None, Some(4), None, Some(6), None, Some(8), None, Some(10)]
+# }
+```
+This Rust adaption provides a familiar and performant interface for creating and
+modifying fixed-size arrays. `Option` types allow the use of filters even in cases where
+the number of unfiltered outputs is unknown at compile time -- without any dynamic
+allocations!
+
+The `arr!` pattern is generally expressed as `f(x), for x in iterable, if condition; len N`,
+where `f(x)` and `iterable` are any [statement](https://doc.rust-lang.org/reference/statements.html),
+`condition` is any statement that evaluates to a `bool`, and and `x` is any [pattern](https://doc.rust-lang.org/reference/patterns.html). Unlike Python, we must also provide a const `N` matching
+the length of the provided iterable in order to ensure the output can be sized at compile time.
+
+
+Note that the extended arr![f(x), for x in a, for a in b, ... for c in iterable]
+syntax that Python supports is not yet supported. Nested comprehensions like
+arr![arr![f(x) for x in outer] for outer in iterable] work as expected.
+
+
+Why this crate?
+===============
+
+When working with vectors, list comprehensions are most naturally expressed as
+`filter/flat_map`s in Rust. Consider the following example, which uses `filter` and
+`map` for clarity:
+
+```rust
+use arrcomp::arr;
+# fn main() {
+let incremented_vec: Vec<_> = (0..10).filter(|x| x % 2 == 1).map(|x| x + 1).collect();
+
+// Converting to an array is simple. Note we have to provide the correct array length
+let incremented_arr: [i32; 5] = incremented_vec.clone().try_into().unwrap();
+assert_eq!(incremented_arr.to_vec(), incremented_vec);
+# }
+```
+
+Note that, in the example above, we dynamically allocate a vector that gets converted to
+an array. In performance-critical contexts this is undesirable. Fortunately, there is
+another way:
+
+```rust
+# use arrcomp::arr;
+# fn main() {
+let mut incremented_iter = (0..10)
+ .into_iter()
+ .map(|x| if x % 2 == 1 { Some(x+1) } else { None });
+
+let iter_copy = incremented_iter.clone();
+
+// std::array::from_fn lets us generate the array without collecting into a vector
+let arr_without_allocation: [Option; 10] = std::array::from_fn(
+ |_| incremented_iter.next().unwrap()
+);
+
+assert_eq!(arr_without_allocation.to_vec(), iter_copy.collect::>());
+# }
+```
+
+The default Rust syntax is a little clunky, and the inputs to `std::array::from_fn` are
+limited. Furthermore, an additional variable must be created outside the function call,
+as cloning the iterator inside `from_fn` resets the iterator to the beginning.
+
+An array comprehension provides an attractive alternative to this pattern, with a
+simplified syntax that allows for arbitrary expressions for our input iterable.
+
+```rust
+# use arrcomp::arr;
+# fn main() {
+let incremented_vec = (0..10).map(|x| if x % 2 == 1 { Some(x+1) } else { None });
+
+let arr_comprehension = arr![x+1, for x in 0..10, if x % 2 == 1; len 10];
+
+assert_eq!(arr_comprehension.to_vec(), incremented_vec.collect::>());
+# }
+```
+
+*/
+
+/**
+The `arr!` macro has two branches - one for filtered comprehensions and one for
+unfiltered. This distinction allows unfiltered comprehensions to return arrays of a base
+type rather `Option`, simplifying cases where all values would be `Some`
+
+`arr![$ex, for $x in $input, if $cond0, if $cond1; len $len]`
+**/
+#[macro_export]
+macro_rules! arr {
+ ($ex:stmt, for $x:pat in $input:expr $(, if $cond:expr)+; len $len:expr) => {{
+ let mut iter = $input.into_iter();
+
+ if $input.len() != $len {
+ let msg = &format!("Expected {} elements, got {}.", $len, $input.len());
+ panic!("{}", msg);
+ }
+ std::array::from_fn::<_, $len, _>(|_| {
+ let $x = iter.next().unwrap_or_default();
+ (true $(&& $cond)*).then(|| {$ex})
+ })
+ }};
+
+
+ ($ex:stmt, for $x:pat in $input:expr; len $len:expr) => {{
+ let mut iter = $input.into_iter();
+
+ if $input.len() != $len {
+ let msg = &format!("Expected {} elements, got {}.", $len, $input.len());
+ panic!("{}", msg);
+ }
+
+ std::array::from_fn::<_, $len, _>(|_| {
+ let $x = iter.next().unwrap_or_default();
+ $ex
+ })
+ }};
+
+}
+
+#[cfg(test)]
+mod tests {
+ /*
+ The tests here aim to cover a wide range of possible syntax statements that
+ users may wish to include in their list comprehensions. Our comprehension syntax
+ is tested against map/filter statements as one would normally use, with the
+ additional understanding that such patterns do not translate easily to arrays.
+ */
+ use super::*;
+ use rstest::fixture;
+ use rstest::rstest;
+
+ #[fixture]
+ fn nums() -> [i32; 5] {
+ [0, 99, -2, 5, 9]
+ }
+
+ #[fixture]
+ fn nums_plus_one() -> [i32; 5] {
+ [1, 100, -1, 6, 10]
+ }
+
+ #[fixture]
+ fn pairs() -> [(i32, f64); 5] {
+ [(0, 0.0), (99, 9900.0), (-2, 2.0), (5, 30.0), (9, 90.0)]
+ }
+
+ #[rstest]
+ fn test_nums_identity(nums: [i32; 5]) {
+ assert_eq!(arr![x, for x in nums; len 5], nums);
+ }
+
+ #[rstest]
+ fn test_nums_statement(nums: [i32; 5]) {
+ assert_eq!(arr![{let _ = x + 1;}, for x in nums; len 5], [(); 5]);
+ }
+
+ #[rstest]
+ fn test_nums_incremented(nums: [i32; 5], nums_plus_one: [i32; 5]) {
+ assert_eq!(arr![x + 1, for x in nums; len 5], nums_plus_one);
+ }
+
+ #[rstest]
+ fn test_nums_with_fn(nums: [i32; 5]) {
+ assert_eq!(
+ vec![arr![x.abs(), for x in nums; len 5]],
+ vec![nums.map(|x| x.abs())]
+ );
+ }
+
+ #[rstest]
+ fn test_nums_constant_value(nums: [i32; 5]) {
+ assert_eq!(arr![12.3, for _ in nums; len 5], [12.3; 5]);
+ }
+
+ #[rstest]
+ fn test_conditional_expressions(nums: [i32; 5]) {
+ assert_eq!(
+ vec![arr![if x > 0 { 1 } else { 0 }, for x in nums; len 5]],
+ vec![nums.map(|x| if x > 0 { 1 } else { 0 })]
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_first_element(pairs: [(i32, f64); 5]) {
+ assert_eq!(arr![x, for (x, _) in pairs; len 5], [0, 99, -2, 5, 9]);
+ }
+
+ #[rstest]
+ fn test_pairs_nested(pairs: [(i32, f64); 5]) {
+ assert_eq!(
+ vec![arr![arr![y, for (_, _, y) in [(1, 33, x)]; len 1], for (x, _) in pairs; len 5]],
+ vec![pairs.map(|p| [p.0])]
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_second_element_zeroed(pairs: [(i32, f64); 5]) {
+ assert_eq!(arr![y * 0.0, for (_, y) in pairs; len 5], [0.0; 5]);
+ }
+
+ #[rstest]
+ fn test_pairs_constant_tuple(pairs: [(i32, f64); 5]) {
+ assert_eq!(arr![(), for _ in pairs; len 5], [(); 5]);
+ }
+
+ #[rstest]
+ fn test_pairs_swapped_elements(pairs: [(i32, f64); 5]) {
+ assert_eq!(
+ arr![(y, x), for (x, y) in pairs; len 5],
+ arr![(pair.1, pair.0), for pair in pairs; len 5]
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_swapped_and_scaled(pairs: [(i32, f64); 5]) {
+ assert_eq!(
+ vec![arr![(y * 2.0, x + 10), for (x, y) in pairs; len 5]],
+ vec![pairs.map(|(x, y)| (y * 2.0, x + 10))]
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_to_arr(pairs: [(i32, f64); 5]) {
+ let other_variable = 43;
+ assert_eq!(
+ vec![arr![[x - other_variable, y as i32], for (x, y) in pairs; len 5]],
+ vec![pairs.map(|(x, y)| [x - other_variable, y as i32])]
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_zipped_product(nums_plus_one: [i32; 5], pairs: [(i32, f64); 5]) {
+ assert_eq!(
+ arr![(x * z) as f64, for ((x, _), z) in pairs.into_iter().zip(nums_plus_one); len 5],
+ arr![y, for (_, y) in pairs; len 5]
+ );
+ }
+
+ #[rstest]
+ fn test_nums_identity_with_cond(nums: [i32; 5]) {
+ assert_eq!(
+ arr![x, for x in nums, if x % 2 == 0; len 5],
+ nums.map(|x| if x % 2 == 0 { Some(x) } else { None })
+ );
+ }
+
+ #[rstest]
+ fn test_nums_statement_with_cond(nums: [i32; 5]) {
+ assert_eq!(
+ arr![{let _ = x + 1;}, for x in nums, if x > 0; len 5],
+ nums.map(|x| if x > 0 { Some(()) } else { None })
+ );
+ }
+
+ #[rstest]
+ fn test_nums_incremented_with_cond(nums: [i32; 5]) {
+ assert_eq!(
+ arr![x + 1, for x in nums, if x >= 0; len 5],
+ nums.map(|x| if x >= 0 { Some(x + 1) } else { None })
+ );
+ }
+
+ #[rstest]
+ fn test_nums_with_fn_with_cond(nums: [i32; 5]) {
+ assert_eq!(
+ vec![arr![x.abs(), for x in nums, if x != 0; len 5]],
+ vec![nums.map(|x| if x != 0 { Some(x.abs()) } else { None })]
+ );
+ }
+
+ #[rstest]
+ #[case::is_odd(|x: i32| x % 2 == 1)]
+ #[case::greater_than_5(|x: i32| x > 5)]
+ #[case::is_negative(|x: i32| x < 0)]
+ #[case::is_zero(|x: i32| x == 0)]
+ fn test_nums_constant_value_with_cond(nums: [i32; 5], #[case] cond: fn(i32) -> bool) {
+ assert_eq!(
+ vec![arr![12.3, for x in nums, if cond(x); len 5]],
+ vec![nums.map(|x| if cond(x) { Some(12.3) } else { None })]
+ );
+ }
+
+ #[rstest]
+ #[case::is_odd(|x: i32| x % 2 == 1)]
+ #[case::greater_than_5(|x: i32| x > 5)]
+ #[case::is_negative(|x: i32| x < 0)]
+ #[case::is_zero(|x: i32| x == 0)]
+ fn test_pairs_first_element_with_cond(pairs: [(i32, f64); 5], #[case] cond: fn(i32) -> bool) {
+ assert_eq!(
+ arr![x, for (x, _) in pairs, if cond(x); len 5],
+ pairs.map(|(x, _)| if cond(x) { Some(x) } else { None })
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_nested_with_cond(pairs: [(i32, f64); 5]) {
+ assert_eq!(
+ vec![arr![arr![y, for (_, _, y) in [(1, 33, x)]; len 1],
+ for (x, _) in pairs, if x % 2 == 0; len 5]],
+ vec![pairs.map(|p| if p.0 % 2 == 0 { Some([p.0]) } else { None })]
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_nested_with_nested_cond(pairs: [(i32, f64); 5]) {
+ assert_eq!(
+ vec![
+ arr![arr![y, for (_, _, y) in [(1, 33, x)], if y > 0; len 1],
+ for (x, _) in pairs, if x % 2 == 0; len 5]
+ ],
+ vec![pairs.map(|p| if p.0 % 2 == 0 {
+ Some([if p.0 > 0 { Some(p.0) } else { None }])
+ } else {
+ None
+ })]
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_second_element_zeroed_with_cond(pairs: [(i32, f64); 5]) {
+ assert_eq!(
+ arr![y * 0.0, for (_, y) in pairs, if true; len 5],
+ [Some(0.0); 5]
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_constant_tuple_with_cond(pairs: [(i32, f64); 5]) {
+ assert_eq!(arr![(), for _ in pairs, if true; len 5], [Some(()); 5]);
+ }
+
+ #[rstest]
+ fn test_pairs_swapped_elements_with_cond(pairs: [(i32, f64); 5]) {
+ assert_eq!(
+ arr![(y, x), for (x, y) in pairs, if x > 0; len 5],
+ arr![(pair.1, pair.0), for pair in pairs, if pair.0 > 0; len 5]
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_swapped_and_scaled_with_cond(pairs: [(i32, f64); 5]) {
+ assert_eq!(
+ vec![arr![(y * 2.0, x + 10), for (x, y) in pairs, if x as f64 + y > 10.0; len 5]],
+ vec![pairs.map(|(x, y)| if x as f64 + y > 10.0 {
+ Some((y * 2.0, x + 10))
+ } else {
+ None
+ })]
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_to_arr_with_cond(pairs: [(i32, f64); 5]) {
+ let other_variable = 43;
+ assert_eq!(
+ vec![arr![
+ [x - other_variable, y as i32],
+ for (x, y) in pairs,
+ if x > y as i32; len 5
+ ]],
+ vec![pairs.map(|(x, y)| if x > y as i32 {
+ Some([x - other_variable, y as i32])
+ } else {
+ None
+ })]
+ );
+ }
+
+ #[rstest]
+ fn test_pairs_zipped_product_with_cond(nums_plus_one: [i32; 5], pairs: [(i32, f64); 5]) {
+ assert_eq!(
+ arr![
+ (x * z) as f64,
+ for ((x, _), z) in pairs.into_iter().zip(nums_plus_one),
+ if x > z; len 5
+ ],
+ arr![y, for (x, y) in pairs, if x as f64 > y + 1.0; len 5]
+ );
+ }
+}