Skip to main content

symplex_macros/
lib.rs

1//! Proc macros for the symplex symbolic mathematics library.
2//!
3//! This crate provides two proc macros:
4//!
5//! - [`expr!`] — build symbolic expressions with natural math syntax.
6//! - [`rule!`] — define rewrite rules with pattern/template syntax.
7//!
8//! This crate should not be used directly.  Instead, depend on `symplex`
9//! which re-exports these macros.
10
11mod parse;
12
13use parse::{
14    BinOp, DimMacroInput, EqMacroInput, ExprMacroInput, MathExpr, MatrixMacroInput, RuleMacroInput,
15};
16use parse::{KNOWN_FUNCTIONS, is_known_constant, is_known_function};
17
18use proc_macro::TokenStream;
19use proc_macro2::Span;
20use proc_macro2::TokenStream as TokenStream2;
21use quote::{format_ident, quote};
22use syn::Ident;
23
24// ═══════════════════════════════════════════════════════════════════════════
25// expr! macro
26// ═══════════════════════════════════════════════════════════════════════════
27
28/// Build a symbolic expression using natural math syntax.
29///
30/// All identifiers are treated as existing Rust variables of type `Ex`
31/// (or `&Ex`).  They are automatically borrowed with `&`.
32///
33/// # Syntax
34///
35/// - Operators: `+`, `-`, `*`, `/`, `^` (power)
36/// - Functions: `sin(x)`, `cos(x)`, `tan(x)`, `asin(x)`, `acos(x)`, `atan(x)`,
37///   `sinh(x)`, `cosh(x)`, `tanh(x)`, `exp(x)`, `ln(x)`, `sqrt(x)`, `abs(x)`
38/// - Parentheses for grouping
39/// - Integer literals
40/// - Unary minus: `-x`
41///
42/// # Constants
43///
44/// The following identifiers are recognized as mathematical constants:
45/// - `pi`, `Pi`, `PI` → π
46/// - `E` → Euler's number e
47/// - `I` → imaginary unit i
48/// - `oo`, `inf` → positive infinity
49///
50/// # Rationals
51///
52/// `1/2`, `3/4`, etc. produce exact rational numbers (not Rust integer division).
53///
54/// # Multi-argument functions
55///
56/// - `log(x, base)` → logarithm of x with given base
57/// - `diff(f, x)` → formal derivative of f with respect to x (unevaluated)
58/// - `factorial(n)` → n!
59/// - `binomial(n, k)` or `C(n, k)` → binomial coefficient C(n,k)
60///
61/// # Examples
62///
63/// ```ignore
64/// use symplex::prelude::*;
65/// use symplex::expr;
66///
67/// let ctx = Context::new();
68/// syms!(ctx; x, y);
69/// let e = expr!(x^2 + 2*x + 1);
70/// let f = expr!(sin(x)^2 + cos(x)^2);
71/// ```
72///
73/// # Limitations
74///
75/// - Implicit multiplication (`2x`) is not supported.  Write `2*x`.
76/// - Only the listed built-in functions are recognised.
77#[proc_macro]
78pub fn expr(input: TokenStream) -> TokenStream {
79    let input = syn::parse_macro_input!(input as ExprMacroInput);
80    // A purely numeric expression (`2^10`, `-3`, `2 + 3`) must still be an
81    // `Ex`, not an `i64`.
82    let result = if input.expr.is_numeric_only() {
83        generate_expr_as_ex(&input.ctx, &input.expr)
84    } else {
85        generate_expr(&input.ctx, &input.expr)
86    };
87    match result {
88        Ok(tokens) => tokens.into(),
89        Err(e) => e.to_compile_error().into(),
90    }
91}
92
93/// Generate Rust code for an `expr!` invocation.
94///
95/// Every identifier is emitted as `(&ident)`.  Integer literals stay
96/// as `i64` (the `Ex` operator impls accept them), except that in a
97/// binary operation whose operands are *both* numeric-only the left
98/// operand is promoted to an `Ex` so that `2^10` or `2 + 3` build an
99/// expression instead of `i64` arithmetic (`i64` has no `powi`).
100/// `^` becomes `.powi(n)` for integer RHS or `.pow(&rhs)` for expression
101/// RHS.  Known function names become method calls.
102fn generate_expr(ctx: &Ident, expr: &MathExpr) -> syn::Result<TokenStream2> {
103    match expr {
104        MathExpr::Int(n, _span) => Ok(quote! { #n }),
105
106        MathExpr::Ident(id) => {
107            let name = id.to_string();
108            match name.as_str() {
109                "pi" | "Pi" | "PI" => Ok(quote! { #ctx.pi() }),
110                "E" => Ok(quote! { #ctx.e() }),
111                "I" => Ok(quote! { #ctx.i_unit() }),
112                "oo" | "inf" => Ok(quote! { #ctx.infinity() }),
113                _ => Ok(quote! { (&#id) }),
114            }
115        }
116
117        MathExpr::Neg(inner) => {
118            let inner_code = generate_expr(ctx, inner)?;
119            Ok(quote! { (-(#inner_code)) })
120        }
121
122        MathExpr::LogicalNot(inner) => {
123            let inner_code = generate_expr(ctx, inner)?;
124            Ok(quote! { (#inner_code).not() })
125        }
126
127        MathExpr::BinOp { op, lhs, rhs } => {
128            // `2 ^ 10`, `2 + 3`, `(2*3) - 1`: lift the left operand to an
129            // `Ex` so the arithmetic is symbolic (exact `i64` folding would
130            // otherwise happen in Rust, or fail to compile for `^`).
131            let numeric_binop = lhs.is_numeric_only()
132                && rhs.is_numeric_only()
133                && matches!(
134                    op,
135                    BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Pow
136                );
137            let gen_lhs = |ctx: &Ident, lhs: &MathExpr| -> syn::Result<TokenStream2> {
138                if numeric_binop {
139                    generate_expr_as_ex(ctx, lhs)
140                } else {
141                    generate_expr(ctx, lhs)
142                }
143            };
144            match op {
145                BinOp::Pow => {
146                    let lhs_code = gen_lhs(ctx, lhs)?;
147                    // If RHS is an integer literal, use .powi(n).
148                    // If RHS is Neg(Int), use .powi(-n).
149                    if let Some(n) = rhs.as_int() {
150                        Ok(quote! { (#lhs_code).powi(#n) })
151                    } else if let MathExpr::Neg(inner) = rhs.as_ref() {
152                        if let Some(n) = inner.as_int() {
153                            let neg_n = -n;
154                            Ok(quote! { (#lhs_code).powi(#neg_n) })
155                        } else {
156                            let rhs_code = generate_expr(ctx, rhs)?;
157                            Ok(quote! { (#lhs_code).pow(&(#rhs_code)) })
158                        }
159                    } else {
160                        let rhs_code = generate_expr(ctx, rhs)?;
161                        Ok(quote! { (#lhs_code).pow(&(#rhs_code)) })
162                    }
163                }
164                BinOp::Div => {
165                    if let (Some(p), Some(q)) = (lhs.as_int(), rhs.as_int()) {
166                        if q == 0 {
167                            return Err(syn::Error::new(
168                                Span::call_site(),
169                                "division by zero in expr!()",
170                            ));
171                        }
172                        return Ok(quote! { #ctx.rational(#p, #q) });
173                    }
174                    // Handle -Int / Int → rational(-n, q).
175                    // Due to precedence, `-1/2` parses as `Neg(1) / 2`.
176                    // Without this, the codegen emits `(-(1)) / (2)` which
177                    // is Rust integer division yielding 0.
178                    if let MathExpr::Neg(inner_lhs) = lhs.as_ref()
179                        && let (Some(p), Some(q)) = (inner_lhs.as_int(), rhs.as_int())
180                    {
181                        if q == 0 {
182                            return Err(syn::Error::new(
183                                Span::call_site(),
184                                "division by zero in expr!()",
185                            ));
186                        }
187                        let neg_p = -p;
188                        return Ok(quote! { #ctx.rational(#neg_p, #q) });
189                    }
190                    let lhs_code = gen_lhs(ctx, lhs)?;
191                    let rhs_code = generate_expr(ctx, rhs)?;
192                    Ok(quote! { ((#lhs_code) / (#rhs_code)) })
193                }
194                BinOp::Gt => {
195                    let lhs_code = generate_expr_as_ex(ctx, lhs)?;
196                    let rhs_code = generate_expr_as_ex(ctx, rhs)?;
197                    Ok(quote! { (#lhs_code).gt(&(#rhs_code)) })
198                }
199                BinOp::Lt => {
200                    let lhs_code = generate_expr_as_ex(ctx, lhs)?;
201                    let rhs_code = generate_expr_as_ex(ctx, rhs)?;
202                    Ok(quote! { (#lhs_code).lt(&(#rhs_code)) })
203                }
204                BinOp::Ge => {
205                    let lhs_code = generate_expr_as_ex(ctx, lhs)?;
206                    let rhs_code = generate_expr_as_ex(ctx, rhs)?;
207                    Ok(quote! { (#lhs_code).ge(&(#rhs_code)) })
208                }
209                BinOp::Le => {
210                    let lhs_code = generate_expr_as_ex(ctx, lhs)?;
211                    let rhs_code = generate_expr_as_ex(ctx, rhs)?;
212                    Ok(quote! { (#lhs_code).le(&(#rhs_code)) })
213                }
214                BinOp::EqEq => {
215                    let lhs_code = generate_expr_as_ex(ctx, lhs)?;
216                    let rhs_code = generate_expr_as_ex(ctx, rhs)?;
217                    Ok(quote! { (#lhs_code).eq_expr(&(#rhs_code)) })
218                }
219                BinOp::Ne => {
220                    let lhs_code = generate_expr_as_ex(ctx, lhs)?;
221                    let rhs_code = generate_expr_as_ex(ctx, rhs)?;
222                    Ok(quote! { (#lhs_code).ne_expr(&(#rhs_code)) })
223                }
224                BinOp::AndAnd => {
225                    let lhs_code = generate_expr(ctx, lhs)?;
226                    let rhs_code = generate_expr(ctx, rhs)?;
227                    Ok(quote! { (#lhs_code).and(&(#rhs_code)) })
228                }
229                BinOp::OrOr => {
230                    let lhs_code = generate_expr(ctx, lhs)?;
231                    let rhs_code = generate_expr(ctx, rhs)?;
232                    Ok(quote! { (#lhs_code).or(&(#rhs_code)) })
233                }
234                _ => {
235                    let lhs_code = gen_lhs(ctx, lhs)?;
236                    let rhs_code = generate_expr(ctx, rhs)?;
237                    let op_token = match op {
238                        BinOp::Add => quote! { + },
239                        BinOp::Sub => quote! { - },
240                        BinOp::Mul => quote! { * },
241                        _ => unreachable!(),
242                    };
243                    Ok(quote! { ((#lhs_code) #op_token (#rhs_code)) })
244                }
245            }
246        }
247
248        MathExpr::Func { name, span, args } => {
249            // Multi-argument functions
250            if name == "log" && args.len() == 2 {
251                let arg_code = generate_expr(ctx, &args[0])?;
252                // The base must be an Ex; bare integer literals from
253                // generate_expr would be i64, so promote them.
254                let base_code = generate_expr_as_ex(ctx, &args[1])?;
255                return Ok(quote! { (#arg_code).log(&(#base_code)) });
256            }
257
258            // diff(f, x) → formal derivative node (unevaluated)
259            if name == "diff" && args.len() == 2 {
260                let f_code = generate_expr(ctx, &args[0])?;
261                let var_code = generate_expr(ctx, &args[1])?;
262                return Ok(quote! { (#f_code).formal_diff(&(#var_code)) });
263            }
264
265            // factorial(n) → n.factorial()
266            if name == "factorial" && args.len() == 1 {
267                let arg_code = generate_expr_as_ex(ctx, &args[0])?;
268                return Ok(quote! { (#arg_code).factorial() });
269            }
270
271            // binomial(n, k) or C(n, k) → n.binomial(&k)
272            if (name == "binomial" || name == "C") && args.len() == 2 {
273                let n_code = generate_expr_as_ex(ctx, &args[0])?;
274                let k_code = generate_expr_as_ex(ctx, &args[1])?;
275                return Ok(quote! { (#n_code).binomial(&(#k_code)) });
276            }
277
278            // atan2(y, x) → y.atan2(&x)
279            if name == "atan2" && args.len() == 2 {
280                let y_code = generate_expr_as_ex(ctx, &args[0])?;
281                let x_code = generate_expr_as_ex(ctx, &args[1])?;
282                return Ok(quote! { (#y_code).atan2(&(#x_code)) });
283            }
284
285            // rising_factorial(x, n) → x.rising_factorial(&n)
286            if name == "rising_factorial" && args.len() == 2 {
287                let x_code = generate_expr_as_ex(ctx, &args[0])?;
288                let n_code = generate_expr_as_ex(ctx, &args[1])?;
289                return Ok(quote! { (#x_code).rising_factorial(&(#n_code)) });
290            }
291
292            // falling_factorial(x, n) → x.falling_factorial(&n)
293            if name == "falling_factorial" && args.len() == 2 {
294                let x_code = generate_expr_as_ex(ctx, &args[0])?;
295                let n_code = generate_expr_as_ex(ctx, &args[1])?;
296                return Ok(quote! { (#x_code).falling_factorial(&(#n_code)) });
297            }
298
299            // beta(a, b) → a.beta(&b)
300            if name == "beta" && args.len() == 2 {
301                let a_code = generate_expr_as_ex(ctx, &args[0])?;
302                let b_code = generate_expr_as_ex(ctx, &args[1])?;
303                return Ok(quote! { (#a_code).beta(&(#b_code)) });
304            }
305
306            // min(a, b) → a.min_with(&b)
307            if name == "min" && args.len() == 2 {
308                let a = generate_expr_as_ex(ctx, &args[0])?;
309                let b = generate_expr_as_ex(ctx, &args[1])?;
310                return Ok(quote! { (#a).min_with(&(#b)) });
311            }
312
313            // max(a, b) → a.max_with(&b)
314            if name == "max" && args.len() == 2 {
315                let a = generate_expr_as_ex(ctx, &args[0])?;
316                let b = generate_expr_as_ex(ctx, &args[1])?;
317                return Ok(quote! { (#a).max_with(&(#b)) });
318            }
319
320            if !is_known_function(name)
321                && ![
322                    "log",
323                    "diff",
324                    "factorial",
325                    "binomial",
326                    "C",
327                    "atan2",
328                    "rising_factorial",
329                    "falling_factorial",
330                    "beta",
331                    "min",
332                    "max",
333                ]
334                .contains(&name.as_str())
335            {
336                return Err(syn::Error::new(
337                    *span,
338                    format!(
339                        "unknown function '{}' in expr!(). Supported: {}, log, diff, factorial, binomial, C, atan2, rising_factorial, falling_factorial, beta, min, max",
340                        name,
341                        KNOWN_FUNCTIONS.join(", ")
342                    ),
343                ));
344            }
345            if args.len() != 1 {
346                return Err(syn::Error::new(
347                    *span,
348                    format!("{}() takes exactly 1 argument in expr!()", name),
349                ));
350            }
351            let arg_code = generate_expr_as_ex(ctx, &args[0])?;
352            let method = match name.as_str() {
353                "sin" => quote! { sin },
354                "cos" => quote! { cos },
355                "tan" => quote! { tan },
356                "asin" => quote! { asin },
357                "acos" => quote! { acos },
358                "atan" => quote! { atan },
359                "sinh" => quote! { sinh },
360                "cosh" => quote! { cosh },
361                "tanh" => quote! { tanh },
362                "asinh" => quote! { asinh },
363                "acosh" => quote! { acosh },
364                "atanh" => quote! { atanh },
365                "exp" => quote! { exp },
366                "ln" => quote! { ln },
367                "sqrt" => quote! { sqrt },
368                "cbrt" => quote! { cbrt },
369                "abs" => quote! { abs },
370                "sign" => quote! { sign },
371                "floor" => quote! { floor },
372                "ceiling" => quote! { ceiling },
373                // Wave A: reciprocal trig/hyp
374                "sec" => quote! { sec },
375                "csc" => quote! { csc },
376                "cot" => quote! { cot },
377                "acot" => quote! { acot },
378                "asec" => quote! { asec },
379                "acsc" => quote! { acsc },
380                "coth" => quote! { coth },
381                "sech" => quote! { sech },
382                "csch" => quote! { csch },
383                "acoth" => quote! { acoth },
384                "asech" => quote! { asech },
385                "acsch" => quote! { acsch },
386                "sinc" => quote! { sinc },
387                // Wave O: complex
388                "arg" => quote! { arg },
389                "conjugate" => quote! { conjugate },
390                // Wave R: combinatorial (1-arg)
391                "fibonacci" => quote! { fibonacci },
392                "lucas" => quote! { lucas },
393                "catalan_number" => quote! { catalan_number },
394                "bell" => quote! { bell },
395                "euler_number" => quote! { euler_number },
396                "harmonic" => quote! { harmonic },
397                "subfactorial" => quote! { subfactorial },
398                "factorial2" => quote! { factorial2 },
399                "bernoulli_number" => quote! { bernoulli_number },
400                // Wave S: special elementary
401                "heaviside" => quote! { heaviside },
402                "dirac_delta" => quote! { dirac_delta },
403                "lambertw" => quote! { lambertw },
404                // Wave J: special functions (1-arg)
405                "gamma" => quote! { gamma },
406                "log_gamma" => quote! { log_gamma },
407                "digamma" => quote! { digamma },
408                "erf" => quote! { erf },
409                "erfc" => quote! { erfc },
410                _ => unreachable!(),
411            };
412            Ok(quote! { (#arg_code).#method() })
413        }
414    }
415}
416
417// ═══════════════════════════════════════════════════════════════════════════
418// dim! macro
419// ═══════════════════════════════════════════════════════════════════════════
420
421/// Build a dimension-checked physical quantity using natural math syntax.
422///
423/// # Syntax
424///
425/// ```ignore
426/// dim!(OutputType: math_expression)
427/// ```
428///
429/// The macro parses the math expression (same syntax as [`expr!`]), generates
430/// code that operates on `Qty<D>` values (preserving compile-time dimension
431/// tracking), and converts the result to `OutputType` via [`FromDimExpr`].
432///
433/// If the computed dimension doesn't match `OutputType`, the compiler emits
434/// a clear error message.
435///
436/// # How it works
437///
438/// - Identifiers refer to named quantity variables (e.g. `Mass`, `Length`).
439///   They are cloned and converted to `Qty<D>` via `.as_qty()`.
440/// - Integer literals become `Dimensionless::constant(n).as_qty()`.
441/// - `+`, `-`, `*`, `/` use the `Qty` operator impls which track dimensions
442///   at the type level.
443/// - `x^n` for small integer `n` (0–8) expands to repeated multiplication,
444///   preserving type-level dimension tracking. For larger or non-literal
445///   exponents, the macro falls back to extracting the inner `Ex` and using
446///   `.powi()` / `.pow()`, which loses dimension tracking (treats result as
447///   dimensionless).
448/// - Functions like `sin`, `cos`, `exp`, `ln` extract the inner `Ex`, call
449///   the method, and wrap the result as `Dimensionless`.
450///
451/// # Examples
452///
453/// ```ignore
454/// use symplex::prelude::*;
455/// use symplex::units::*;
456///
457/// let m = Mass::symbol("m");
458/// let g = Acceleration::symbol("g");
459/// let h = Length::symbol("h");
460/// let pe = symplex::dim!(Energy: m * g * h);
461/// ```
462///
463/// [`FromDimExpr`]: ::symplex::units::qty::FromDimExpr
464#[proc_macro]
465pub fn dim(input: TokenStream) -> TokenStream {
466    let input = syn::parse_macro_input!(input as DimMacroInput);
467    let output_type = &input.output_type;
468    match generate_dim_expr(&input.ctx, &input.expr) {
469        Ok(expr_tokens) => quote! {
470            <#output_type as ::symplex::units::qty::FromDimExpr<_>>::from_dim_expr(#expr_tokens)
471        }
472        .into(),
473        Err(e) => e.to_compile_error().into(),
474    }
475}
476
477/// Generate Rust code for a `dim!` invocation.
478///
479/// Each `MathExpr` node is translated to code producing a `Qty<D>`,
480/// where the dimension `D` is computed at the type level by Rust's
481/// type system via the `Qty` arithmetic operator impls.
482fn generate_dim_expr(ctx: &Ident, expr: &MathExpr) -> syn::Result<TokenStream2> {
483    match expr {
484        MathExpr::Int(n, _span) => Ok(quote! {
485            ::symplex::units::Dimensionless::from_ex(#ctx.int(#n)).as_qty()
486        }),
487
488        MathExpr::Ident(id) => {
489            let name = id.to_string();
490            match name.as_str() {
491                "pi" | "Pi" | "PI" => Ok(quote! {
492                    ::symplex::units::Dimensionless::from_ex(#ctx.pi()).as_qty()
493                }),
494                "E" => Ok(quote! {
495                    ::symplex::units::Dimensionless::from_ex(#ctx.e()).as_qty()
496                }),
497                _ => Ok(quote! { (#id).clone().as_qty() }),
498            }
499        }
500
501        MathExpr::Neg(inner) => {
502            let inner_code = generate_dim_expr(ctx, inner)?;
503            Ok(quote! { (-(#inner_code)) })
504        }
505
506        MathExpr::LogicalNot(_) => Err(syn::Error::new(
507            Span::call_site(),
508            "logical NOT (!) is not supported in dim!()",
509        )),
510
511        MathExpr::BinOp { op, lhs, rhs } => match op {
512            BinOp::Add => {
513                let l = generate_dim_expr(ctx, lhs)?;
514                let r = generate_dim_expr(ctx, rhs)?;
515                Ok(quote! { ((#l) + (#r)) })
516            }
517            BinOp::Sub => {
518                let l = generate_dim_expr(ctx, lhs)?;
519                let r = generate_dim_expr(ctx, rhs)?;
520                Ok(quote! { ((#l) - (#r)) })
521            }
522            BinOp::Mul => {
523                let l = generate_dim_expr(ctx, lhs)?;
524                let r = generate_dim_expr(ctx, rhs)?;
525                Ok(quote! { ((#l) * (#r)) })
526            }
527            BinOp::Div => {
528                // int / int → exact rational (dimensionless)
529                if let (Some(p), Some(q)) = (lhs.as_int(), rhs.as_int()) {
530                    if q == 0 {
531                        return Err(syn::Error::new(
532                            Span::call_site(),
533                            "division by zero in dim!()",
534                        ));
535                    }
536                    return Ok(quote! {
537                        ::symplex::units::Dimensionless::from_ex(#ctx.rational(#p, #q)).as_qty()
538                    });
539                }
540                // -int / int → rational(-n, q)
541                if let MathExpr::Neg(inner_lhs) = lhs.as_ref()
542                    && let (Some(p), Some(q)) = (inner_lhs.as_int(), rhs.as_int())
543                {
544                    if q == 0 {
545                        return Err(syn::Error::new(
546                            Span::call_site(),
547                            "division by zero in dim!()",
548                        ));
549                    }
550                    let neg_p = -p;
551                    return Ok(quote! {
552                        ::symplex::units::Dimensionless::from_ex(#ctx.rational(#neg_p, #q)).as_qty()
553                    });
554                }
555                let l = generate_dim_expr(ctx, lhs)?;
556                let r = generate_dim_expr(ctx, rhs)?;
557                Ok(quote! { ((#l) / (#r)) })
558            }
559            BinOp::Pow => {
560                // Integer exponents: expand to repeated multiplication for
561                // type-level dimension tracking.
562                if let Some(n) = rhs.as_int() {
563                    return generate_dim_pow(ctx, lhs, n);
564                }
565                // Negative integer exponent: x^(-n) = 1 / x^n
566                if let MathExpr::Neg(inner_rhs) = rhs.as_ref()
567                    && let Some(n) = inner_rhs.as_int()
568                {
569                    let pow_code = generate_dim_pow(ctx, lhs, n)?;
570                    return Ok(quote! {
571                        (::symplex::units::Dimensionless::from_ex(#ctx.int(1)).as_qty() / (#pow_code))
572                    });
573                }
574                // Non-integer exponent: fall back to inner Ex operations.
575                // This loses dimension tracking — result is Dimensionless.
576                let b = generate_dim_expr(ctx, lhs)?;
577                let e = generate_dim_expr(ctx, rhs)?;
578                Ok(quote! {
579                    ::symplex::units::Dimensionless::from_ex(
580                        (#b).into_inner().pow(&#e.into_inner())
581                    ).as_qty()
582                })
583            }
584            _ => Err(syn::Error::new(
585                Span::call_site(),
586                format!("operator {:?} is not supported in dim!()", op),
587            )),
588        },
589
590        MathExpr::Func { name, span, args } => {
591            // Transcendental functions produce dimensionless results.
592            // Extract inner Ex, call the method, wrap as Dimensionless.
593            let func_str = name.as_str();
594            if args.len() == 1 {
595                let arg = generate_dim_expr(ctx, &args[0])?;
596                let method = match func_str {
597                    "sin" => quote! { sin },
598                    "cos" => quote! { cos },
599                    "tan" => quote! { tan },
600                    "asin" => quote! { asin },
601                    "acos" => quote! { acos },
602                    "atan" => quote! { atan },
603                    "sinh" => quote! { sinh },
604                    "cosh" => quote! { cosh },
605                    "tanh" => quote! { tanh },
606                    "exp" => quote! { exp },
607                    "ln" => quote! { ln },
608                    "sqrt" => quote! { sqrt },
609                    "abs" => quote! { abs },
610                    _ => {
611                        return Err(syn::Error::new(
612                            *span,
613                            format!("dim!: unsupported function '{}'", func_str),
614                        ));
615                    }
616                };
617                Ok(quote! {
618                    ::symplex::units::Dimensionless::from_ex(
619                        (#arg).into_inner().#method()
620                    ).as_qty()
621                })
622            } else {
623                Err(syn::Error::new(
624                    *span,
625                    format!(
626                        "dim!: function '{}' with {} args is not supported",
627                        func_str,
628                        args.len()
629                    ),
630                ))
631            }
632        }
633    }
634}
635
636/// Generate code for `base^n` where `n` is a known integer literal.
637///
638/// For small `n` (0–8), this expands to repeated multiplication so the
639/// type system tracks the resulting dimension.  For larger `n`, it falls
640/// back to `.powi()` on the inner `Ex` (losing dimension tracking).
641fn generate_dim_pow(ctx: &Ident, base: &MathExpr, n: i64) -> syn::Result<TokenStream2> {
642    if n == 0 {
643        return Ok(quote! { ::symplex::units::Dimensionless::from_ex(#ctx.int(1)).as_qty() });
644    }
645    if n == 1 {
646        return generate_dim_expr(ctx, base);
647    }
648    if (2..=8).contains(&n) {
649        // Expand x^n = x * x * ... * x  (n factors).
650        // Each factor is an independent evaluation of `base` so the
651        // type-level dimension products compose correctly.
652        let mut factors = Vec::new();
653        for _ in 0..n {
654            factors.push(generate_dim_expr(ctx, base)?);
655        }
656        let mut result = factors.remove(0);
657        for factor in factors {
658            result = quote! { ((#result) * (#factor)) };
659        }
660        return Ok(result);
661    }
662    // n > 8: fall back to extracting inner Ex and using powi.
663    // Dimension tracking is lost — the result is treated as Dimensionless.
664    let base_code = generate_dim_expr(ctx, base)?;
665    Ok(quote! {
666        ::symplex::units::Dimensionless::from_ex(
667            (#base_code).into_inner().powi(#n)
668        ).as_qty()
669    })
670}
671
672// ═══════════════════════════════════════════════════════════════════════════
673// rule! macro
674// ═══════════════════════════════════════════════════════════════════════════
675
676/// Define a rewrite rule with pattern/template syntax.
677///
678/// # Syntax
679///
680/// ```ignore
681/// rule!(arena, "rule_name", LHS_PATTERN => RHS_TEMPLATE)
682/// rule!(arena, "rule_name", LHS_PATTERN => RHS_TEMPLATE if condition_expr)
683/// ```
684///
685/// - `arena` — an expression of type `&mut Arena`.
686/// - `"rule_name"` — a string literal used for tracing.
687/// - Identifiers ending in `_` are **wilds** (pattern variables):
688///   they match any sub-expression and bind it.
689/// - Known constants: `pi`, `E`, `I`, `oo`, `nan`, `zoo`.
690/// - Integer literals: `0`, `1`, `2`, `-3`, etc.
691/// - Functions: `sin`, `cos`, `tan`, `exp`, `ln`, `sqrt`, `abs`.
692/// - `=>` separates the pattern (LHS) from the template (RHS).
693/// - An optional `if <expr>` after the RHS specifies a condition
694///   function: `fn(&Arena, &Substitution) -> bool`.
695///
696/// # Examples
697///
698/// ```ignore
699/// use symplex::prelude::*;
700/// use symplex::rule;
701///
702/// let rules = vec![
703///     rule!(arena, "pythagorean", sin(w_)^2 + cos(w_)^2 => 1),
704///     rule!(arena, "exp_ln", exp(ln(w_)) => w_),
705///     rule!(arena, "ln_exp", ln(exp(w_)) => w_),
706/// ];
707/// ```
708///
709/// # Limitations
710///
711/// - Only wilds (`w_`), integers, known constants, and known functions
712///   are allowed.  Bare identifiers that are not wilds or constants
713///   produce a compile error.
714#[proc_macro]
715pub fn rule(input: TokenStream) -> TokenStream {
716    let input = syn::parse_macro_input!(input as RuleMacroInput);
717    match generate_rule(&input) {
718        Ok(tokens) => tokens.into(),
719        Err(e) => e.to_compile_error().into(),
720    }
721}
722
723/// Generate Rust code for a `rule!` invocation.
724///
725/// All sub-expressions are bound to flat `let __tN = arena.method(...)`
726/// temporaries to avoid double-mutable-borrow errors.
727fn generate_rule(input: &RuleMacroInput) -> syn::Result<TokenStream2> {
728    let arena = &input.arena;
729    let name = &input.name;
730
731    // Collect wilds from LHS.
732    let lhs_wilds = input.lhs.collect_wilds();
733
734    // Validate that all RHS wilds also appear in LHS.
735    for w in input.rhs.collect_wilds() {
736        if !lhs_wilds.iter().any(|existing| existing == &w) {
737            return Err(syn::Error::new_spanned(
738                &input.name,
739                format!(
740                    "wild '{}' appears in RHS but not in LHS — it will never be bound by matching",
741                    w
742                ),
743            ));
744        }
745    }
746
747    let all_wilds = lhs_wilds;
748
749    let mut codegen = RuleCodeGen {
750        arena: arena.clone(),
751        temp_counter: 0,
752        bindings: Vec::new(),
753        wild_expr_idents: Vec::new(),
754        wild_wid_idents: Vec::new(),
755        wild_names: Vec::new(),
756    };
757
758    // Generate wild declarations.
759    for wild in &all_wilds {
760        let wild_name = wild.to_string();
761        let expr_ident = format_ident!("__wild_expr_{}", wild_name);
762        let wid_ident = format_ident!("__wild_wid_{}", wild_name);
763        codegen.bindings.push(quote! {
764            let (#expr_ident, #wid_ident) = #arena.wild();
765        });
766        codegen.wild_expr_idents.push(expr_ident);
767        codegen.wild_wid_idents.push(wid_ident);
768        codegen.wild_names.push(wild.clone());
769    }
770
771    // Generate LHS expression tree.
772    let lhs_temp = codegen.generate_arena_expr(&input.lhs)?;
773
774    // Generate RHS expression tree.
775    let rhs_temp = codegen.generate_arena_expr(&input.rhs)?;
776
777    // Build the wilds map.
778    let wild_inserts: Vec<TokenStream2> = codegen
779        .wild_names
780        .iter()
781        .zip(
782            codegen
783                .wild_expr_idents
784                .iter()
785                .zip(codegen.wild_wid_idents.iter()),
786        )
787        .map(|(_, (expr_id, wid_id))| {
788            quote! { __wilds.insert(#expr_id, #wid_id); }
789        })
790        .collect();
791
792    let bindings = &codegen.bindings;
793
794    let condition_code = if let Some(cond) = &input.condition {
795        quote! { Some(#cond) }
796    } else {
797        quote! { None }
798    };
799
800    Ok(quote! {
801        {
802            #(#bindings)*
803
804            let mut __wilds = ::symplex::__macro_support::FxHashMap::default();
805            #(#wild_inserts)*
806
807            ::symplex::__macro_support::Rule {
808                name: #name,
809                pattern: ::symplex::__macro_support::Pattern {
810                    root: #lhs_temp,
811                    wilds: __wilds,
812                },
813                template: #rhs_temp,
814                condition: #condition_code,
815            }
816        }
817    })
818}
819
820/// Code generator for `rule!` that emits flat let-bindings for every
821/// sub-expression, avoiding double-mutable-borrow issues.
822struct RuleCodeGen {
823    arena: Ident,
824    temp_counter: usize,
825    bindings: Vec<TokenStream2>,
826    wild_expr_idents: Vec<Ident>,
827    wild_wid_idents: Vec<Ident>,
828    wild_names: Vec<Ident>,
829}
830
831impl RuleCodeGen {
832    /// Allocate a fresh temporary name.
833    fn fresh_temp(&mut self) -> Ident {
834        let id = format_ident!("__t{}", self.temp_counter);
835        self.temp_counter += 1;
836        id
837    }
838
839    /// Generate arena method calls for a MathExpr, returning the
840    /// identifier of the temporary holding the final ExprId.
841    fn generate_arena_expr(&mut self, expr: &MathExpr) -> syn::Result<Ident> {
842        let arena = self.arena.clone();
843
844        match expr {
845            MathExpr::Int(n, _) => {
846                let temp = self.fresh_temp();
847                match *n {
848                    0 => {
849                        self.bindings.push(quote! { let #temp = #arena.zero(); });
850                    }
851                    1 => {
852                        self.bindings.push(quote! { let #temp = #arena.one(); });
853                    }
854                    -1 => {
855                        self.bindings.push(quote! { let #temp = #arena.neg_one(); });
856                    }
857                    _ => {
858                        self.bindings.push(quote! { let #temp = #arena.int(#n); });
859                    }
860                }
861                Ok(temp)
862            }
863
864            MathExpr::Ident(id) => {
865                let name = id.to_string();
866
867                // Wild: return the pre-declared wild expression id.
868                if name.ends_with('_') {
869                    for (i, wn) in self.wild_names.iter().enumerate() {
870                        if wn == id {
871                            return Ok(self.wild_expr_idents[i].clone());
872                        }
873                    }
874                    return Err(syn::Error::new(id.span(), format!("unknown wild '{name}'")));
875                }
876
877                // Known constant.
878                if is_known_constant(&name) {
879                    let temp = self.fresh_temp();
880                    let access = match name.as_str() {
881                        "pi" => quote! { #arena.pi() },
882                        "E" => quote! { #arena.e_const() },
883                        "I" => quote! { #arena.i_unit() },
884                        "oo" => quote! { #arena.infinity() },
885                        "nan" => quote! { #arena.nan() },
886                        "zoo" => quote! { #arena.complex_infinity() },
887                        _ => unreachable!(),
888                    };
889                    self.bindings.push(quote! { let #temp = #access; });
890                    return Ok(temp);
891                }
892
893                // Unknown identifier — error.
894                Err(syn::Error::new(
895                    id.span(),
896                    format!(
897                        "unknown identifier '{name}' in rule!(). \
898                         Use '{name}_' for a wild, or a known constant (pi, E, I, oo, nan, zoo), \
899                         or an integer literal."
900                    ),
901                ))
902            }
903
904            MathExpr::Neg(inner) => {
905                let inner_temp = self.generate_arena_expr(inner)?;
906                let temp = self.fresh_temp();
907                self.bindings
908                    .push(quote! { let #temp = #arena.neg(#inner_temp); });
909                Ok(temp)
910            }
911
912            MathExpr::LogicalNot(inner) => {
913                let inner_temp = self.generate_arena_expr(inner)?;
914                let temp = self.fresh_temp();
915                self.bindings
916                    .push(quote! { let #temp = #arena.not(#inner_temp); });
917                Ok(temp)
918            }
919
920            MathExpr::BinOp { op, lhs, rhs } => {
921                let lhs_temp = self.generate_arena_expr(lhs)?;
922                let rhs_temp = self.generate_arena_expr(rhs)?;
923                let temp = self.fresh_temp();
924
925                let call = match op {
926                    BinOp::Add => quote! { #arena.add(&[#lhs_temp, #rhs_temp]) },
927                    BinOp::Sub => quote! { #arena.sub(#lhs_temp, #rhs_temp) },
928                    BinOp::Mul => quote! { #arena.mul(&[#lhs_temp, #rhs_temp]) },
929                    BinOp::Div => quote! { #arena.div(#lhs_temp, #rhs_temp) },
930                    BinOp::Pow => quote! { #arena.pow(#lhs_temp, #rhs_temp) },
931                    BinOp::Gt => quote! { #arena.gt(#lhs_temp, #rhs_temp) },
932                    BinOp::Lt => quote! { #arena.gt(#rhs_temp, #lhs_temp) },
933                    BinOp::Ge => quote! { #arena.ge(#lhs_temp, #rhs_temp) },
934                    BinOp::Le => quote! { #arena.ge(#rhs_temp, #lhs_temp) },
935                    BinOp::EqEq => quote! { #arena.eq_(#lhs_temp, #rhs_temp) },
936                    BinOp::Ne => quote! { #arena.ne_(#lhs_temp, #rhs_temp) },
937                    BinOp::AndAnd => quote! { #arena.and(&[#lhs_temp, #rhs_temp]) },
938                    BinOp::OrOr => quote! { #arena.or(&[#lhs_temp, #rhs_temp]) },
939                };
940
941                self.bindings.push(quote! { let #temp = #call; });
942                Ok(temp)
943            }
944
945            MathExpr::Func { name, span, args } => {
946                if !is_known_function(name) {
947                    return Err(syn::Error::new(
948                        *span,
949                        format!(
950                            "unknown function '{}' in rule!(). Supported: {}",
951                            name,
952                            KNOWN_FUNCTIONS.join(", ")
953                        ),
954                    ));
955                }
956
957                // Binary functions: beta(a, b), atan2(y, x)
958                if name == "beta" && args.len() == 2 {
959                    let a_temp = self.generate_arena_expr(&args[0])?;
960                    let b_temp = self.generate_arena_expr(&args[1])?;
961                    let temp = self.fresh_temp();
962                    self.bindings
963                        .push(quote! { let #temp = #arena.beta(#a_temp, #b_temp); });
964                    return Ok(temp);
965                }
966                if name == "atan2" && args.len() == 2 {
967                    let y_temp = self.generate_arena_expr(&args[0])?;
968                    let x_temp = self.generate_arena_expr(&args[1])?;
969                    let temp = self.fresh_temp();
970                    self.bindings
971                        .push(quote! { let #temp = #arena.atan2(#y_temp, #x_temp); });
972                    return Ok(temp);
973                }
974
975                if args.len() != 1 {
976                    return Err(syn::Error::new(
977                        *span,
978                        format!("{}() takes exactly 1 argument in rule!()", name),
979                    ));
980                }
981
982                let arg_temp = self.generate_arena_expr(&args[0])?;
983                let temp = self.fresh_temp();
984
985                let call = match name.as_str() {
986                    // Core trig
987                    "sin" => quote! { #arena.sin(#arg_temp) },
988                    "cos" => quote! { #arena.cos(#arg_temp) },
989                    "tan" => quote! { #arena.tan(#arg_temp) },
990                    "asin" => quote! { #arena.asin(#arg_temp) },
991                    "acos" => quote! { #arena.acos(#arg_temp) },
992                    "atan" => quote! { #arena.atan(#arg_temp) },
993                    "sinh" => quote! { #arena.sinh(#arg_temp) },
994                    "cosh" => quote! { #arena.cosh(#arg_temp) },
995                    "tanh" => quote! { #arena.tanh(#arg_temp) },
996                    "asinh" => quote! { #arena.asinh(#arg_temp) },
997                    "acosh" => quote! { #arena.acosh(#arg_temp) },
998                    "atanh" => quote! { #arena.atanh(#arg_temp) },
999                    // Exp/log/root
1000                    "exp" => quote! { #arena.exp(#arg_temp) },
1001                    "ln" => quote! { #arena.ln(#arg_temp) },
1002                    "sqrt" => quote! { #arena.sqrt(#arg_temp) },
1003                    "cbrt" => quote! { #arena.cbrt(#arg_temp) },
1004                    "abs" => quote! { #arena.abs(#arg_temp) },
1005                    "sign" => quote! { #arena.sign(#arg_temp) },
1006                    // Wave B: Floor, Ceiling
1007                    "floor" => quote! { #arena.floor(#arg_temp) },
1008                    "ceiling" => quote! { #arena.ceiling(#arg_temp) },
1009                    // Wave J: Special functions
1010                    "gamma" => quote! { #arena.gamma(#arg_temp) },
1011                    "log_gamma" => quote! { #arena.log_gamma(#arg_temp) },
1012                    "digamma" => quote! { #arena.digamma(#arg_temp) },
1013                    "erf" => quote! { #arena.erf(#arg_temp) },
1014                    "erfc" => quote! { #arena.erfc(#arg_temp) },
1015                    // Wave S/δ: Heaviside, DiracDelta, LambertW
1016                    "heaviside" => quote! { #arena.heaviside(#arg_temp) },
1017                    "dirac_delta" => quote! { #arena.dirac_delta(#arg_temp) },
1018                    "lambertw" => quote! { #arena.lambertw(#arg_temp) },
1019                    // Wave R: Combinatorial (1-arg, Apply-based but have arena methods)
1020                    "fibonacci" => quote! { #arena.fibonacci(#arg_temp) },
1021                    "lucas" => quote! { #arena.lucas(#arg_temp) },
1022                    "catalan_number" => quote! { #arena.catalan_number(#arg_temp) },
1023                    "bell" => quote! { #arena.bell(#arg_temp) },
1024                    "euler_number" => quote! { #arena.euler_number(#arg_temp) },
1025                    "harmonic" => quote! { #arena.harmonic(#arg_temp) },
1026                    "subfactorial" => quote! { #arena.subfactorial(#arg_temp) },
1027                    "factorial2" => quote! { #arena.factorial2(#arg_temp) },
1028                    "bernoulli_number" => quote! { #arena.bernoulli_number(#arg_temp) },
1029                    other => {
1030                        return Err(syn::Error::new(
1031                            *span,
1032                            format!(
1033                                "function '{}' is recognised but not yet supported in rule!()",
1034                                other
1035                            ),
1036                        ));
1037                    }
1038                };
1039
1040                self.bindings.push(quote! { let #temp = #call; });
1041                Ok(temp)
1042            }
1043        }
1044    }
1045}
1046
1047// ═══════════════════════════════════════════════════════════════════════════
1048// matrix! macro
1049// ═══════════════════════════════════════════════════════════════════════════
1050
1051/// Build a symbolic matrix using natural math syntax.
1052///
1053/// # Examples
1054///
1055/// ```ignore
1056/// use symplex::prelude::*;
1057/// use symplex::matrix;
1058///
1059/// let x = symplex::var("x");
1060/// let m = matrix![[x, 1], [0, x]];
1061/// ```
1062#[proc_macro]
1063pub fn matrix(input: TokenStream) -> TokenStream {
1064    let input = syn::parse_macro_input!(input as MatrixMacroInput);
1065    match generate_matrix(&input.ctx, &input) {
1066        Ok(tokens) => tokens.into(),
1067        Err(e) => e.to_compile_error().into(),
1068    }
1069}
1070
1071fn generate_matrix(ctx: &Ident, input: &MatrixMacroInput) -> syn::Result<TokenStream2> {
1072    let mut row_codes = Vec::new();
1073    for row in &input.rows {
1074        let mut cell_codes = Vec::new();
1075        for cell in row {
1076            let cell_expr = generate_expr_as_ex(ctx, cell)?;
1077            cell_codes.push(quote! { #cell_expr });
1078        }
1079        row_codes.push(quote! { vec![#(#cell_codes),*] });
1080    }
1081    Ok(quote! {
1082        ::symplex::matrix::Matrix::new(vec![#(#row_codes),*])
1083            .expect("matrix! macro: invalid literal data")
1084    })
1085}
1086
1087// ═══════════════════════════════════════════════════════════════════════════
1088// eq! macro
1089// ═══════════════════════════════════════════════════════════════════════════
1090
1091/// Build a symbolic equation using natural math syntax.
1092///
1093/// # Examples
1094///
1095/// ```ignore
1096/// use symplex::prelude::*;
1097/// use symplex::eq;
1098///
1099/// let x = symplex::var("x");
1100/// let equation = eq!(x^2 + x = 6);
1101/// ```
1102#[proc_macro]
1103pub fn eq(input: TokenStream) -> TokenStream {
1104    let input = syn::parse_macro_input!(input as EqMacroInput);
1105    match generate_eq(&input.ctx, &input) {
1106        Ok(tokens) => tokens.into(),
1107        Err(e) => e.to_compile_error().into(),
1108    }
1109}
1110
1111fn generate_eq(ctx: &Ident, input: &EqMacroInput) -> syn::Result<TokenStream2> {
1112    let lhs_code = generate_expr_as_ex(ctx, &input.lhs)?;
1113    let rhs_code = generate_expr_as_ex(ctx, &input.rhs)?;
1114    Ok(quote! {
1115        ::symplex::eq::Equation::new(#lhs_code, #rhs_code)
1116    })
1117}
1118
1119/// Like [`generate_expr`] but guarantees the result is an `Ex`, even for
1120/// bare integer literals (which `generate_expr` emits as plain `i64`).
1121///
1122/// Function calls (including `diff`, `factorial`, `binomial`, `C`, `log`,
1123/// and all single-arg functions) always return `Ex`, so we delegate
1124/// directly to [`generate_expr`] for those.
1125fn generate_expr_as_ex(ctx: &Ident, expr: &MathExpr) -> syn::Result<TokenStream2> {
1126    match expr {
1127        MathExpr::Int(n, _) => Ok(quote! { #ctx.int(#n) }),
1128        MathExpr::Neg(inner) => {
1129            if let Some(n) = inner.as_int() {
1130                let neg_n = -n;
1131                Ok(quote! { #ctx.int(#neg_n) })
1132            } else {
1133                let code = generate_expr(ctx, expr)?;
1134                Ok(quote! { { let __v: ::symplex::expr::Ex = (#code).clone(); __v } })
1135            }
1136        }
1137        MathExpr::Func { .. } => generate_expr(ctx, expr),
1138        _ => {
1139            let code = generate_expr(ctx, expr)?;
1140            Ok(quote! { { let __v: ::symplex::expr::Ex = (#code).clone(); __v } })
1141        }
1142    }
1143}