Crate reefer

Crate reefer 

Source
Expand description

§Overview

This crate behaves like a staged compiler that turns Clifford algebra declarations into generated Rust. Macros parse input (spec), build an intermediate multivector representation, run optimisation passes, then emit TokenStreams.

Two entry points exist:

  • The module attribute macro #[reefer::algebra] (attach it to an empty module)
  • The generated expr! macro that expands symbolic expressions within that module

Both flow through the same pipeline described in “Optimization Pipeline”.

§Quickstart

This five‑minute guide gets you from zero to a working algebra and a compiled expression.

§Installation

Add Reefer to your manifest (not on crates.io yet):

[dependencies]
reefer = { git = "https://github.com/kgullion/reefer", branch = "main" }

Heads up: building the default CAS backend compiles GMP/MPFR through rug, which requires the m4 macro processor to be available during the build. Install it via your system package manager (for example, sudo apt install m4 on Debian/Ubuntu or brew install m4 on macOS) before running cargo build.

§Example CGA 2D

#[reefer::algebra(f32, 3, 1)]
mod cga2d {
    basis! { e0 = P0 + N0 }
    basis! { eI = P0 - N0 }
    basis! { e1 = P1 }
    basis! { e2 = P2 }

    #[derive(Debug, Clone, Copy, PartialEq)]
    shape!(Vector { e0, eI, e1, e2 });
}

// Use the generated `expr!` macro to build expressions:
let mv: cga2d::Vector = cga2d::expr!(|| e1 + 2.0 * e2)();

// Methods are available directly on symbolic values:
let norm = cga2d::expr!(|| (e1 + e2).norm_squared())();
let rotor = cga2d::expr!(|| ((-0.5 * (e1 ^ e2)).exp()))();

// Closures can take typed arguments too
let reflect = cga2d::expr!(|v: cga2d::Vector| {
    let r = (-0.5 * (e1 ^ e2)).exp();
    r.sandwich(v)
});
let result = reflect(mv);
  • Operators: * (geometric), ^ (wedge), | (inner), & (regressive/meet)
  • Methods: .reverse(), .dual(), .undual(), .left_contract(rhs), .right_contract(rhs), .complement(), .conjugate(), .automorphism(), .sandwich(x), .exp(), .sqrt(), .norm_squared(), .scalar()
  • Shapes returned from expr! are concrete structs. Inspect the generated module to see fields.

See the “Expressions & Operators” chapter for details.

§Using the Macros

§Module setup

#[reefer::algebra(f32, 3, 1)]
mod cga2d {
    basis! { e0 = P0 + N0 }
    basis! { eI = P0 - N0 }
    basis! { e1 = P1 }
    basis! { e2 = P2 }

    #[derive(Debug, Clone, Copy, PartialEq)]
    shape!(Vector { e0, eI, e1, e2 });
}

§Basis and shape declarations

  • Alias grammar matches [a-z]+[0-9A-Z]? and must remain prefix-free (e1, e12, rotorA).
  • Frame slots on the right-hand side stick to the uppercase [PN]\d+ (shorthand [PQ]\d+]) pattern such as P0, N1, so the parser cleanly separates them from alias names. These frame slots are the basis of the compile time mother algebra.
  • Basis aliases may include scalar coefficients: basis! { idem = 0.5 + 0.5 * P0 }.
  • Compound aliases such as e12 or t0x1 are expanded automatically using the alias map.
  • Shapes enumerate chosen blades; at expansion time each shape! becomes a public struct populated with scalar fields.

§Expressions

Use the generated expr! macro to define typed closures:

let f = cga2d::expr!(|v: cga2d::Vector| (v ^ e1).reverse());
let result = f(sample_vector);

Supported operators: *, ^, |, &.

Supported methods:

  • reverse, conjugate, automorphism (involutions)
  • dual, complement, scalar, norm_squared
  • left_contract, right_contract
  • sandwich, exp, sqrt

Unsupported today: arbitrary function calls, method chains with arguments other than the documented list, or pattern let bindings inside the closure. The optimizer will report unsupported syntax at compile time.

For use in the expr! macro of your algebra.

§Expressions & Operators

  • * Geometric product
  • ^ Exterior (wedge) product
  • | Inner product (contraction)
  • & Regressive product (meet)

§Methods

  • .reverse() – reversion involution
  • .conjugate() – grade involution (negates odd grades)
  • .automorphism() – main involution (negates grades ≡ 2,3 mod 4)
  • .dual() – right dual with respect to pseudoscalar
  • .complement() – orthogonal complement
  • .sandwich(target)a * target * a.reverse() shortcut
  • .exp() – multivector exponential (sums by grades when applicable)
  • .sqrt() – principal square root where defined
  • .norm_squared() – returns scalar squared norm
  • .left_contract(rhs), .right_contract(rhs) – contractions

See tests in tests/smoke.rs for end‑to‑end examples of each.

⚠️ Heads up: The remaining chapters dive into the compiler internals that power Reefer. If you’re here for day-to-day algebra usage, you can safely skip what’s ahead or come back later when you’re ready for the gory details.

§Optimization Pipeline

Reefer behaves like a tiny compiler that runs entirely during macro expansion. The input is the Rust module annotated with #[reefer::algebra] plus any later calls to the generated expr! macro; the output is more Rust code that Rustc can type-check and inline. This chapter walks through the pipeline end to end so you can reason about where behaviours or diagnostics originate.

§Stage 1 - Attribute parsing (front end)

  • Responsibilities: Decode the attribute payload and gather basis! / shape! invocations.
  • Key modules: src/spec.rs, src/algebra.rs.
  • Inputs & outputs: The procedural macro receives a module annotated with #[reefer::algebra(scalar, p, q)]. It lowers the arguments into AlgebraAttr, then aggregates basis and shape declarations into an AlgebraSpec.
  • Failure surface: Any syntactic issue (unknown argument, missing module body, malformed basis!) is surfaced immediately via syn::Error tied to the offending span.

At the end of this stage we have a structured description of the algebra, no code has been emitted yet, but the spec contains everything downstream passes need.

§Stage 2 - Module elaboration

  • Responsibilities: Turn the declarative spec into concrete Rust items.
  • Key modules: src/algebra.rs (build_algebra), src/spec.rs (ShapeItem).
  • What happens:
    • Every basis! block is recorded and removed from the expanded module.
    • Each shape! block desugars into a public struct with scalar fields, constructors, and basic trait impls.
    • The module receives an expr! helper backed by reefer::build_expr! so user crates can author symbolic closures.

If anything goes wrong here, such as the module being empty, the macro emits a compile-time error and stops.

§Stage 3 - Alias rewrite

  • Responsibilities: Normalise user-friendly aliases (e12, rotorA) to canonical basis masks.
  • Key modules: src/build_expr/rewrite.rs, src/optimizer.rs (Optimizer::alias_mapping).
  • Inputs & outputs: When a user invokes ga::expr!(...), the macro captures the closure and builds a BuildExpr record. Alias mapping produces a deterministic table from strings to basis masks; the rewrite pass walks the syntax tree, replacing identifiers so the rest of the pipeline only sees canonical names.
  • Failure surface: Rewriting is designed to be total. If an identifier cannot be mapped, the optimiser records a diagnostic that materialises as a compile-time error attached to the original token.

This stage keeps the symbolic IR free from any naming quirks and enables deterministic simplifications.

§Stage 4 - IR construction

  • Responsibilities: Interpret the rewritten closure into a Clifford-aware intermediate representation.
  • Key modules: src/optimizer/parser.rs, src/clifford/multivector.rs, src/clifford/mod.rs.
  • Inputs & outputs: The parser walks the syn::Expr tree, calling algebraic helpers to produce a SymbolMultivector (backed by SynField coefficients). Each node records the blade mask and the coefficient expression, preserving structure like wedges, inner products, involutions, and exponentials.
  • Failure surface: If the expression uses unsupported syntax (for example a method call the parser does not recognise), analysis_error is set on BuildExpr. Expansion continues with a fallback closure so compilation succeeds, but later optimisation steps are skipped.

Think of this stage as turning syntax into a typed, algebra-specific IR; much like translating Rust HIR into MIR.

§Stage 5 - Optimisation passes

  • Responsibilities: Simplify the multivector and prepare literals.
  • Key modules: src/build_expr.rs (BuildExpr::optimise, constant_shape_literal), src/clifford/multivector.rs (algebraic helpers).
  • Pass highlights:
    • Zero pruning: Any blade whose coefficient evaluates to zero is dropped immediately, keeping the representation sparse.
    • Term merging: Like-grade blades with compatible masks are merged via accumulate_term, reducing redundant arithmetic.

Feature flags (backend_cas, backend_egg) control which scalar backend drives simplification. The default CAS stack works quite well. Egg is an alternative that is being explored but is not yet functional and thus is behind a feature flag.

§Stage 6 - Backend emission

  • Responsibilities: Convert the final IR back into Rust tokens.
  • Key modules: src/build_expr.rs (BuildExpr::into_stream), src/algebra.rs (module generation).
  • What happens:
    • Successful constant folding swaps the closure body for a literal shape struct value.
    • The closure is rehydrated into tokens using quote!, referencing the generated shape/basis types.
    • The resulting TokenStream is emitted to Rustc, which then performs its normal type checking and optimisation pipeline.

§Cross-cutting behaviours

  • Diagnostics: Every stage propagates syn::Error or human-friendly messages. Front-end issues halt expansion; middle-end failures record context on BuildExpr so users receive actionable compiler notes. There are still some todo!()s sprinkled in (eg div still needs implemented) but every other path should not panic.
  • Testing hooks: Unit tests in src/algebra.rs validate attribute handling, optimizer::parser::tests cover IR construction, and integration suites in tests/smoke.rs exercise the complete pipeline.
  • Extensibility: New passes should slot naturally into Stage 5. They operate in place on SymbolMultivector.

Understanding these stages makes it easier to diagnose compile-time errors and to extend Reefer with new algebraic tricks. When you add a feature, ask which stage owns the logic, wire a couple tests into the matching module, and verify that the emitted tokens still type-check under both backends.

§Backends & Features

Reefer ships with a minimal default configuration width of 32. When you need different performance or algebra sizes, toggle Cargo features to swap backends or widen the internal bitset type. This chapter summarises what each flag does, when to use it, and the chores that come with it.

§Default configuration

  • Scalar backend: backend_cas – enables the CAS-based coefficient field (cas-rs, cas-compute, and rug). This gives the optimiser access to symbolic evaluation and constant folding out of the box.
  • Blade mask width: Falls back to u8 (BladeBits = u8), supporting algebras with up to 8 basis vectors (n <= 8).
  • System requirement: The rug dependency pulls in GMP/MPFR and needs the m4 macro processor during compilation. Install it via your package manager (e.g. sudo apt install m4 or brew install m4) before building.

If the defaults match your algebra (most 2D/3D examples do), you can stay here and treat Reefer like any other dependency: cargo add reefer and you are done.

§Selecting a scalar backend

Exactly one backend should be active at a time. The backend_cas feature is enabled by default; switch to backend_egg by disabling default features. Enabling both simultaneously will produce duplicate type definitions and break the build.

§backend_cas (default)

  • What you get: A symbolic algebra field powered by CAS-RS. It performs aggressive constant folding and can represent rational coefficients exactly.
  • Trade-offs: Missing some nice optimazations available to egg, like folding to mul_add expressions. There’s also the external m4 requirement mentioned above.

§backend_egg (experimental)

  • What you get: An e-graph-based field that relies on the egg crate. The optimiser rewrites coefficient expressions using equality saturation.
  • Trade-offs: Currently offers a smaller rule set than the CAS backend and is marked experimental in the source tree. Will likely refactor in the future to find certain optimizations that cas-rs misses. The feature is still exploratory though, expect missing rewrites.

§Controlling blade mask width

The generated algebra stores basis combinations inside a bitset type BladeBits. Choose the width that matches the largest blade mask you plan to represent (number of basis vectors n must satisfy n <= bit_width). Only enable one of these flags at a time; otherwise the compiler will complain about conflicting type aliases.

| Feature | BladeBits | Max basis vectors | | — | — | — | — | | bits_u8 | u8 | 8 | | bits_u16 | u16 | 16 | | bits_u32 | u32 | 32 | | bits_u64 | u64 | 64 | | bits_u128 | u128 | 128 |

Switching width is as simple as: reefer = { features = ["bits_u64"], .. }

§Troubleshooting

This chapter collects the failure modes run into most often while building or using Reefer. Start with the scenario that matches your compiler output, then follow the checklist to unblock yourself quickly.

§Build failures & prerequisites

  • cc / link errors mentioning GMP/MPFR or missing m4: The default backend_cas feature pulls in the rug crate, which relies on the m4 macro processor during its native build. Install it (sudo apt install m4, brew install m4, etc.), then retry cargo build.
  • Feature conflicts: Only enable one blade-width flag (bits_u8, bits_u16, …) and one backend (backend_cas or backend_egg). Cargo will emit duplicate type alias errors if multiple options are toggled simultaneously.
  • no module items after applying #[reefer::algebra]: Ensure the annotated module isn’t empty. At minimum, include one basis! declaration; an empty module triggers a syn::Error with this wording.
  • Proc-macro panics: These are considered bugs; macros should return structured diagnostics. If you hit one, re-run with RUST_BACKTRACE=1 and capture the output when filing an issue (if this is a todo! panic, please check for existing issue first).

§Macro expansion & alias issues

  • Unknown identifier diagnostics: The optimiser could not map a symbol to a declared basis alias. Confirm the alias is listed in your basis! blocks.
  • expr! rejects pattern bindings or method chains: The current parser accepts simple let bindings and the supported method list (reverse, dual, sandwich, contractions, etc.). Remove unsupported syntax or break expressions into smaller helpers.

§Debugging techniques

  • Inspect emitted code: Use cargo expand -p your_crate --test smoke (or whichever target invokes Reefer) to dump the generated tokens. For quick peeks inside this repository, run the relevant tests with -- --nocapture to print the emitted closure bodies.

Keep this page handy while iterating on new algebra specs or optimiser passes. When you discover a new failure pattern, add it here so the next contributor benefits from the breadcrumb trail.

Macros§

build_expr
Proc-macro helper that builds optimized Clifford algebra expressions at compile time.

Attribute Macros§

algebra
Attribute macro that expands a module definition into a full Clifford algebra implementation.