hetero-cartesian 0.1.0

A procedural macro to flatten nested heterogeneous callback-based control flows into a clean cartesian product.
Documentation

hetero-cartesian

hetero-cartesian is a Rust procedural macro designed to elegantly flatten nested, callback-based heterogeneous control flows (like deeply nested visitors or inverted iterations) into a clean, unified cartesian product block with absolute zero overhead.

Why this crate?

Iterators are great, but they require all yielded elements to be of the exact same type. When dealing with control flows that yield highly heterogeneous data (different types, varying const bounds) via callback traits, you can't just collect them into a Vec or use itertools.

Imagine two disjoint systems. One yields arrays of varying lengths (const generic N), and another yields tuples of completely different types (type generics T, U):

trait ArrayHandler {
    fn process_array<const N: usize>(&mut self, arr: [i32; N]);
}

trait TupleHandler {
    // Both variables require `ToString` dynamically parameterized
    fn process_tuple<T: Display, U: Display>(&mut self, val: (T, U));
}

// System A feeds arrays of DIFFERENT lengths
fn drive_arrays<H: ArrayHandler>(h: &mut H) {
    h.process_array([1, 2]); // N = 2
    h.process_array([3, 4, 5]); // N = 3
}

// System B feeds tuples of DIFFERENT types
fn drive_tuples<H: TupleHandler>(h: &mut H) {
    h.process_tuple((42u32, true)); // T = u32, U = bool
    h.process_tuple(("hello", 'x')); // T = &str, U = char
}

If you want to iterate over the cartesian product of these two systems, standard for loops or iterators completely fail here. Cleanly connecting these requires you to write complex, deeply nested overlapping struct implementations manually.

cartesian! elegantly flattens this with absolute zero overhead:

  • 0 Dynamic Dispatch: It generates strictly typed, nested generic struct implementations on the fly. No Box<dyn Trait> or runtime type checks.
  • 0 Memory Allocations: Your local environment is captured and passed seamlessly down the call stack. No heap allocation is required.
  • 100% Static Context: Bounds are propagated precisely. The control flow is flattened into a unified block where the innermost execution has full access to exact compile-time types, trait bounds, and const generics.

Usage

Define your environments, map your driver iterations to their struct boundaries, and let the macro do the heavy lifting:

use hetero_cartesian::cartesian;

fn main() {
    let mut total_array_sum: i32 = 0;
    let mut total_length: usize = 0;
    let mut total_dimensions: usize = 0;
    let mut combinations_count: usize = 0;
    let mut strings = Vec::new();

    cartesian! {
        // 1. Capture local environment explicitly
        let (sum, length, dim, comb, s): (&mut i32, &mut usize, &mut usize, &mut usize, &mut Vec<String>)
            = (&mut total_array_sum, &mut total_length, &mut total_dimensions, &mut combinations_count, &mut strings);

        // 2. Define the layers: function(args) => Trait::method<Generics>(param: Type);
        // `_` is replaced by the generated struct handler.
        drive_arrays(_) => ArrayHandler::process_array<const N: usize>(arr: [i32; N]);
        drive_tuples(_) => TupleHandler::process_tuple<T: Display, U: Display>(val: (T, U));

        // 3. The innermost execution block
        {
            let (first, second) = val;
            s.push(format!("arr: {:?}, val: ({}, {})", arr, first, second));

            // 1. Real array iterators and trait operations work perfectly using exact memory shapes
            *sum += arr.iter().sum::<i32>();
            *length += arr.len();

            // 2. Trait bounds bound to `T` and `U` inside the macro are fully operational
            *length += first.to_string().len() + second.to_string().len();

            // 3. `N` is an actual compile-time `const` available as a local variable!
            *dim += N;
            *comb += 1;
        }
    }

    // The cartesian product seamlessly executed distinct trait boundaries, gathering
    // deeply aggregated computation data strictly via compiler safe types!
    assert_eq!(total_array_sum, 30); // (1+2)*2 + (3+4+5)*2 = 6 + 24 = 30
    assert_eq!(total_length, 34); // (2 + 3 + ("hello".len() + 'x'.len() + "42".len() + "true".len())) * 2
    assert_eq!(total_dimensions, 10); // N: 2 + 2 + 3 + 3 = 10
    assert_eq!(combinations_count, 4); // 2 array calls x 2 tuple calls
    assert_eq!(
        strings,
        [
            "arr: [1, 2], val: (42, true)",
            "arr: [1, 2], val: (hello, x)",
            "arr: [3, 4, 5], val: (42, true)",
            "arr: [3, 4, 5], val: (hello, x)",
        ]
    );
}