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!(ctx, x^2 + 2*x + 1);
70/// let f = expr!(ctx, 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            // 0.9 special functions with parameters, in SymPy's argument
321            // order `f(params…, x)`; the `Ex` method lives on `x` and takes
322            // the parameters after it: `expint(n, x)` → `x.expint(&n)`.
323            if let Some((method, nparams)) = match name.as_str() {
324                "expint" => Some(("expint", 1)),
325                "lowergamma" => Some(("lowergamma", 1)),
326                "uppergamma" => Some(("uppergamma", 1)),
327                "polylog" => Some(("polylog", 1)),
328                "elliptic_f" => Some(("elliptic_f", 1)),
329                "elliptic_pi" => Some(("elliptic_pi", 1)),
330                "gegenbauer" => Some(("gegenbauer", 2)),
331                "assoc_legendre" => Some(("assoc_legendre", 2)),
332                "assoc_laguerre" => Some(("assoc_laguerre", 2)),
333                "jacobi" => Some(("jacobi", 3)),
334                _ => None,
335            } {
336                if args.len() != nparams + 1 {
337                    return Err(syn::Error::new(
338                        *span,
339                        format!(
340                            "{name}() takes exactly {} arguments in expr!()",
341                            nparams + 1
342                        ),
343                    ));
344                }
345                let x = generate_expr_as_ex(ctx, &args[nparams])?;
346                let params = args[..nparams]
347                    .iter()
348                    .map(|a| generate_expr_as_ex(ctx, a))
349                    .collect::<syn::Result<Vec<_>>>()?;
350                let method = format_ident!("{method}");
351                return Ok(quote! { (#x).#method(#(&(#params)),*) });
352            }
353
354            if !is_known_function(name)
355                && ![
356                    "log",
357                    "diff",
358                    "factorial",
359                    "binomial",
360                    "C",
361                    "atan2",
362                    "rising_factorial",
363                    "falling_factorial",
364                    "beta",
365                    "min",
366                    "max",
367                    "expint",
368                    "lowergamma",
369                    "uppergamma",
370                    "polylog",
371                    "elliptic_f",
372                    "elliptic_pi",
373                    "gegenbauer",
374                    "assoc_legendre",
375                    "assoc_laguerre",
376                    "jacobi",
377                ]
378                .contains(&name.as_str())
379            {
380                return Err(syn::Error::new(
381                    *span,
382                    format!(
383                        "unknown function '{}' in expr!(). Supported: {}, log, diff, factorial, binomial, C, atan2, rising_factorial, falling_factorial, beta, min, max",
384                        name,
385                        KNOWN_FUNCTIONS.join(", ")
386                    ),
387                ));
388            }
389            if args.len() != 1 {
390                return Err(syn::Error::new(
391                    *span,
392                    format!("{}() takes exactly 1 argument in expr!()", name),
393                ));
394            }
395            let arg_code = generate_expr_as_ex(ctx, &args[0])?;
396            let method = match name.as_str() {
397                "sin" => quote! { sin },
398                "cos" => quote! { cos },
399                "tan" => quote! { tan },
400                "asin" => quote! { asin },
401                "acos" => quote! { acos },
402                "atan" => quote! { atan },
403                "sinh" => quote! { sinh },
404                "cosh" => quote! { cosh },
405                "tanh" => quote! { tanh },
406                "asinh" => quote! { asinh },
407                "acosh" => quote! { acosh },
408                "atanh" => quote! { atanh },
409                "exp" => quote! { exp },
410                "ln" => quote! { ln },
411                "sqrt" => quote! { sqrt },
412                "cbrt" => quote! { cbrt },
413                "abs" => quote! { abs },
414                "sign" => quote! { sign },
415                "floor" => quote! { floor },
416                "ceiling" => quote! { ceiling },
417                // Wave A: reciprocal trig/hyp
418                "sec" => quote! { sec },
419                "csc" => quote! { csc },
420                "cot" => quote! { cot },
421                "acot" => quote! { acot },
422                "asec" => quote! { asec },
423                "acsc" => quote! { acsc },
424                "coth" => quote! { coth },
425                "sech" => quote! { sech },
426                "csch" => quote! { csch },
427                "acoth" => quote! { acoth },
428                "asech" => quote! { asech },
429                "acsch" => quote! { acsch },
430                "sinc" => quote! { sinc },
431                // Wave O: complex
432                "arg" => quote! { arg },
433                "conjugate" => quote! { conjugate },
434                // Wave R: combinatorial (1-arg)
435                "fibonacci" => quote! { fibonacci },
436                "lucas" => quote! { lucas },
437                "catalan_number" => quote! { catalan_number },
438                "bell" => quote! { bell },
439                "euler_number" => quote! { euler_number },
440                "harmonic" => quote! { harmonic },
441                "subfactorial" => quote! { subfactorial },
442                "factorial2" => quote! { factorial2 },
443                "bernoulli_number" => quote! { bernoulli_number },
444                // Wave S: special elementary
445                "heaviside" => quote! { heaviside },
446                "dirac_delta" => quote! { dirac_delta },
447                "lambertw" => quote! { lambertw },
448                // Wave J: special functions (1-arg)
449                "gamma" => quote! { gamma },
450                "log_gamma" => quote! { log_gamma },
451                "digamma" => quote! { digamma },
452                "erf" => quote! { erf },
453                "erfc" => quote! { erfc },
454                // 0.9: more special functions (1-arg); the method name is
455                // the SymPy name in lower case.
456                "erfi" | "erfinv" | "erfcinv" | "e1" | "shi" | "chi" | "fresnels" | "fresnelc"
457                | "dirichlet_eta" | "airyai" | "airybi" | "airyaiprime" | "airybiprime"
458                | "elliptic_k" | "elliptic_e" => {
459                    let m = format_ident!("{}", name.as_str());
460                    quote! { #m }
461                }
462                other => {
463                    return Err(syn::Error::new(
464                        *span,
465                        format!("function '{other}' is not supported in expr!()"),
466                    ));
467                }
468            };
469            Ok(quote! { (#arg_code).#method() })
470        }
471    }
472}
473
474// ═══════════════════════════════════════════════════════════════════════════
475// dim! macro
476// ═══════════════════════════════════════════════════════════════════════════
477
478/// Build a dimension-checked physical quantity using natural math syntax.
479///
480/// # Syntax
481///
482/// ```ignore
483/// dim!(OutputType: math_expression)
484/// ```
485///
486/// The macro parses the math expression (same syntax as [`expr!`]), generates
487/// code that operates on `Qty<D>` values (preserving compile-time dimension
488/// tracking), and converts the result to `OutputType` via [`FromDimExpr`].
489///
490/// If the computed dimension doesn't match `OutputType`, the compiler emits
491/// a clear error message.
492///
493/// # How it works
494///
495/// - Identifiers refer to named quantity variables (e.g. `Mass`, `Length`).
496///   They are cloned and converted to `Qty<D>` via `.as_qty()`.
497/// - Integer literals become `Dimensionless::constant(n).as_qty()`.
498/// - `+`, `-`, `*`, `/` use the `Qty` operator impls which track dimensions
499///   at the type level.
500/// - `x^n` for small integer `n` (0–8) expands to repeated multiplication,
501///   preserving type-level dimension tracking. For larger or non-literal
502///   exponents, the macro falls back to extracting the inner `Ex` and using
503///   `.powi()` / `.pow()`, which loses dimension tracking (treats result as
504///   dimensionless).
505/// - Functions like `sin`, `cos`, `exp`, `ln` extract the inner `Ex`, call
506///   the method, and wrap the result as `Dimensionless`.
507///
508/// # Examples
509///
510/// ```ignore
511/// use symplex::prelude::*;
512/// use symplex::units::*;
513///
514/// let m = Mass::symbol("m");
515/// let g = Acceleration::symbol("g");
516/// let h = Length::symbol("h");
517/// let pe = symplex::dim!(Energy: m * g * h);
518/// ```
519///
520/// [`FromDimExpr`]: ::symplex::units::qty::FromDimExpr
521#[proc_macro]
522pub fn dim(input: TokenStream) -> TokenStream {
523    let input = syn::parse_macro_input!(input as DimMacroInput);
524    let output_type = &input.output_type;
525    match generate_dim_expr(&input.ctx, &input.expr) {
526        Ok(expr_tokens) => quote! {
527            <#output_type as ::symplex::units::qty::FromDimExpr<_>>::from_dim_expr(#expr_tokens)
528        }
529        .into(),
530        Err(e) => e.to_compile_error().into(),
531    }
532}
533
534/// Generate Rust code for a `dim!` invocation.
535///
536/// Each `MathExpr` node is translated to code producing a `Qty<D>`,
537/// where the dimension `D` is computed at the type level by Rust's
538/// type system via the `Qty` arithmetic operator impls.
539fn generate_dim_expr(ctx: &Ident, expr: &MathExpr) -> syn::Result<TokenStream2> {
540    match expr {
541        MathExpr::Int(n, _span) => Ok(quote! {
542            ::symplex::units::Dimensionless::from_ex(#ctx.int(#n)).as_qty()
543        }),
544
545        MathExpr::Ident(id) => {
546            let name = id.to_string();
547            match name.as_str() {
548                "pi" | "Pi" | "PI" => Ok(quote! {
549                    ::symplex::units::Dimensionless::from_ex(#ctx.pi()).as_qty()
550                }),
551                "E" => Ok(quote! {
552                    ::symplex::units::Dimensionless::from_ex(#ctx.e()).as_qty()
553                }),
554                _ => Ok(quote! { (#id).clone().as_qty() }),
555            }
556        }
557
558        MathExpr::Neg(inner) => {
559            let inner_code = generate_dim_expr(ctx, inner)?;
560            Ok(quote! { (-(#inner_code)) })
561        }
562
563        MathExpr::LogicalNot(_) => Err(syn::Error::new(
564            Span::call_site(),
565            "logical NOT (!) is not supported in dim!()",
566        )),
567
568        MathExpr::BinOp { op, lhs, rhs } => match op {
569            BinOp::Add => {
570                let l = generate_dim_expr(ctx, lhs)?;
571                let r = generate_dim_expr(ctx, rhs)?;
572                Ok(quote! { ((#l) + (#r)) })
573            }
574            BinOp::Sub => {
575                let l = generate_dim_expr(ctx, lhs)?;
576                let r = generate_dim_expr(ctx, rhs)?;
577                Ok(quote! { ((#l) - (#r)) })
578            }
579            BinOp::Mul => {
580                let l = generate_dim_expr(ctx, lhs)?;
581                let r = generate_dim_expr(ctx, rhs)?;
582                Ok(quote! { ((#l) * (#r)) })
583            }
584            BinOp::Div => {
585                // int / int → exact rational (dimensionless)
586                if let (Some(p), Some(q)) = (lhs.as_int(), rhs.as_int()) {
587                    if q == 0 {
588                        return Err(syn::Error::new(
589                            Span::call_site(),
590                            "division by zero in dim!()",
591                        ));
592                    }
593                    return Ok(quote! {
594                        ::symplex::units::Dimensionless::from_ex(#ctx.rational(#p, #q)).as_qty()
595                    });
596                }
597                // -int / int → rational(-n, q)
598                if let MathExpr::Neg(inner_lhs) = lhs.as_ref()
599                    && let (Some(p), Some(q)) = (inner_lhs.as_int(), rhs.as_int())
600                {
601                    if q == 0 {
602                        return Err(syn::Error::new(
603                            Span::call_site(),
604                            "division by zero in dim!()",
605                        ));
606                    }
607                    let neg_p = -p;
608                    return Ok(quote! {
609                        ::symplex::units::Dimensionless::from_ex(#ctx.rational(#neg_p, #q)).as_qty()
610                    });
611                }
612                let l = generate_dim_expr(ctx, lhs)?;
613                let r = generate_dim_expr(ctx, rhs)?;
614                Ok(quote! { ((#l) / (#r)) })
615            }
616            BinOp::Pow => {
617                // Integer exponents: expand to repeated multiplication for
618                // type-level dimension tracking.
619                if let Some(n) = rhs.as_int() {
620                    return generate_dim_pow(ctx, lhs, n);
621                }
622                // Negative integer exponent: x^(-n) = 1 / x^n
623                if let MathExpr::Neg(inner_rhs) = rhs.as_ref()
624                    && let Some(n) = inner_rhs.as_int()
625                {
626                    let pow_code = generate_dim_pow(ctx, lhs, n)?;
627                    return Ok(quote! {
628                        (::symplex::units::Dimensionless::from_ex(#ctx.int(1)).as_qty() / (#pow_code))
629                    });
630                }
631                // Non-integer exponent: fall back to inner Ex operations.
632                // This loses dimension tracking — result is Dimensionless.
633                let b = generate_dim_expr(ctx, lhs)?;
634                let e = generate_dim_expr(ctx, rhs)?;
635                Ok(quote! {
636                    ::symplex::units::Dimensionless::from_ex(
637                        (#b).into_inner().pow(&#e.into_inner())
638                    ).as_qty()
639                })
640            }
641            _ => Err(syn::Error::new(
642                Span::call_site(),
643                format!("operator {:?} is not supported in dim!()", op),
644            )),
645        },
646
647        MathExpr::Func { name, span, args } => {
648            // Transcendental functions produce dimensionless results.
649            // Extract inner Ex, call the method, wrap as Dimensionless.
650            let func_str = name.as_str();
651            if args.len() == 1 {
652                let arg = generate_dim_expr(ctx, &args[0])?;
653                let method = match func_str {
654                    "sin" => quote! { sin },
655                    "cos" => quote! { cos },
656                    "tan" => quote! { tan },
657                    "asin" => quote! { asin },
658                    "acos" => quote! { acos },
659                    "atan" => quote! { atan },
660                    "sinh" => quote! { sinh },
661                    "cosh" => quote! { cosh },
662                    "tanh" => quote! { tanh },
663                    "exp" => quote! { exp },
664                    "ln" => quote! { ln },
665                    "sqrt" => quote! { sqrt },
666                    "abs" => quote! { abs },
667                    _ => {
668                        return Err(syn::Error::new(
669                            *span,
670                            format!("dim!: unsupported function '{}'", func_str),
671                        ));
672                    }
673                };
674                Ok(quote! {
675                    ::symplex::units::Dimensionless::from_ex(
676                        (#arg).into_inner().#method()
677                    ).as_qty()
678                })
679            } else {
680                Err(syn::Error::new(
681                    *span,
682                    format!(
683                        "dim!: function '{}' with {} args is not supported",
684                        func_str,
685                        args.len()
686                    ),
687                ))
688            }
689        }
690    }
691}
692
693/// Generate code for `base^n` where `n` is a known integer literal.
694///
695/// For small `n` (0–8), this expands to repeated multiplication so the
696/// type system tracks the resulting dimension.  For larger `n`, it falls
697/// back to `.powi()` on the inner `Ex` (losing dimension tracking).
698fn generate_dim_pow(ctx: &Ident, base: &MathExpr, n: i64) -> syn::Result<TokenStream2> {
699    if n == 0 {
700        return Ok(quote! { ::symplex::units::Dimensionless::from_ex(#ctx.int(1)).as_qty() });
701    }
702    if n == 1 {
703        return generate_dim_expr(ctx, base);
704    }
705    if (2..=8).contains(&n) {
706        // Expand x^n = x * x * ... * x  (n factors).
707        // Each factor is an independent evaluation of `base` so the
708        // type-level dimension products compose correctly.
709        let mut factors = Vec::new();
710        for _ in 0..n {
711            factors.push(generate_dim_expr(ctx, base)?);
712        }
713        let mut result = factors.remove(0);
714        for factor in factors {
715            result = quote! { ((#result) * (#factor)) };
716        }
717        return Ok(result);
718    }
719    // n > 8: fall back to extracting inner Ex and using powi.
720    // Dimension tracking is lost — the result is treated as Dimensionless.
721    let base_code = generate_dim_expr(ctx, base)?;
722    Ok(quote! {
723        ::symplex::units::Dimensionless::from_ex(
724            (#base_code).into_inner().powi(#n)
725        ).as_qty()
726    })
727}
728
729// ═══════════════════════════════════════════════════════════════════════════
730// rule! macro
731// ═══════════════════════════════════════════════════════════════════════════
732
733/// Define a rewrite rule with pattern/template syntax.
734///
735/// # Syntax
736///
737/// ```ignore
738/// rule!(arena, "rule_name", LHS_PATTERN => RHS_TEMPLATE)
739/// rule!(arena, "rule_name", LHS_PATTERN => RHS_TEMPLATE if condition_expr)
740/// ```
741///
742/// - `arena` — an expression of type `&mut Arena`.
743/// - `"rule_name"` — a string literal used for tracing.
744/// - Identifiers ending in `_` are **wilds** (pattern variables):
745///   they match any sub-expression and bind it.
746/// - Known constants: `pi`, `E`, `I`, `oo`, `nan`, `zoo`.
747/// - Integer literals: `0`, `1`, `2`, `-3`, etc.
748/// - Functions: `sin`, `cos`, `tan`, `exp`, `ln`, `sqrt`, `abs`.
749/// - `=>` separates the pattern (LHS) from the template (RHS).
750/// - An optional `if <expr>` after the RHS specifies a condition
751///   function: `fn(&Arena, &Substitution) -> bool`.
752///
753/// # Examples
754///
755/// ```ignore
756/// use symplex::prelude::*;
757/// use symplex::rule;
758///
759/// let rules = vec![
760///     rule!(arena, "pythagorean", sin(w_)^2 + cos(w_)^2 => 1),
761///     rule!(arena, "exp_ln", exp(ln(w_)) => w_),
762///     rule!(arena, "ln_exp", ln(exp(w_)) => w_),
763/// ];
764/// ```
765///
766/// # Limitations
767///
768/// - Only wilds (`w_`), integers, known constants, and known functions
769///   are allowed.  Bare identifiers that are not wilds or constants
770///   produce a compile error.
771#[proc_macro]
772pub fn rule(input: TokenStream) -> TokenStream {
773    let input = syn::parse_macro_input!(input as RuleMacroInput);
774    match generate_rule(&input) {
775        Ok(tokens) => tokens.into(),
776        Err(e) => e.to_compile_error().into(),
777    }
778}
779
780/// Generate Rust code for a `rule!` invocation.
781///
782/// All sub-expressions are bound to flat `let __tN = arena.method(...)`
783/// temporaries to avoid double-mutable-borrow errors.
784fn generate_rule(input: &RuleMacroInput) -> syn::Result<TokenStream2> {
785    let arena = &input.arena;
786    let name = &input.name;
787
788    // Collect wilds from LHS.
789    let lhs_wilds = input.lhs.collect_wilds();
790
791    // Validate that all RHS wilds also appear in LHS.
792    for w in input.rhs.collect_wilds() {
793        if !lhs_wilds.iter().any(|existing| existing == &w) {
794            return Err(syn::Error::new_spanned(
795                &input.name,
796                format!(
797                    "wild '{}' appears in RHS but not in LHS — it will never be bound by matching",
798                    w
799                ),
800            ));
801        }
802    }
803
804    let all_wilds = lhs_wilds;
805
806    let mut codegen = RuleCodeGen {
807        arena: arena.clone(),
808        temp_counter: 0,
809        bindings: Vec::new(),
810        wild_expr_idents: Vec::new(),
811        wild_wid_idents: Vec::new(),
812        wild_names: Vec::new(),
813    };
814
815    // Generate wild declarations.
816    for wild in &all_wilds {
817        let wild_name = wild.to_string();
818        let expr_ident = format_ident!("__wild_expr_{}", wild_name);
819        let wid_ident = format_ident!("__wild_wid_{}", wild_name);
820        codegen.bindings.push(quote! {
821            let (#expr_ident, #wid_ident) = #arena.wild();
822        });
823        codegen.wild_expr_idents.push(expr_ident);
824        codegen.wild_wid_idents.push(wid_ident);
825        codegen.wild_names.push(wild.clone());
826    }
827
828    // Generate LHS expression tree.
829    let lhs_temp = codegen.generate_arena_expr(&input.lhs)?;
830
831    // Generate RHS expression tree.
832    let rhs_temp = codegen.generate_arena_expr(&input.rhs)?;
833
834    // Build the wilds map.
835    let wild_inserts: Vec<TokenStream2> = codegen
836        .wild_names
837        .iter()
838        .zip(
839            codegen
840                .wild_expr_idents
841                .iter()
842                .zip(codegen.wild_wid_idents.iter()),
843        )
844        .map(|(_, (expr_id, wid_id))| {
845            quote! { __wilds.insert(#expr_id, #wid_id); }
846        })
847        .collect();
848
849    let bindings = &codegen.bindings;
850
851    let condition_code = if let Some(cond) = &input.condition {
852        quote! { Some(#cond) }
853    } else {
854        quote! { None }
855    };
856
857    Ok(quote! {
858        {
859            #(#bindings)*
860
861            let mut __wilds = ::symplex::__macro_support::FxHashMap::default();
862            #(#wild_inserts)*
863
864            ::symplex::__macro_support::Rule {
865                name: #name,
866                pattern: ::symplex::__macro_support::Pattern {
867                    root: #lhs_temp,
868                    wilds: __wilds,
869                },
870                template: #rhs_temp,
871                condition: #condition_code,
872            }
873        }
874    })
875}
876
877/// Code generator for `rule!` that emits flat let-bindings for every
878/// sub-expression, avoiding double-mutable-borrow issues.
879struct RuleCodeGen {
880    arena: Ident,
881    temp_counter: usize,
882    bindings: Vec<TokenStream2>,
883    wild_expr_idents: Vec<Ident>,
884    wild_wid_idents: Vec<Ident>,
885    wild_names: Vec<Ident>,
886}
887
888impl RuleCodeGen {
889    /// Allocate a fresh temporary name.
890    fn fresh_temp(&mut self) -> Ident {
891        let id = format_ident!("__t{}", self.temp_counter);
892        self.temp_counter += 1;
893        id
894    }
895
896    /// Generate arena method calls for a MathExpr, returning the
897    /// identifier of the temporary holding the final ExprId.
898    fn generate_arena_expr(&mut self, expr: &MathExpr) -> syn::Result<Ident> {
899        let arena = self.arena.clone();
900
901        match expr {
902            MathExpr::Int(n, _) => {
903                let temp = self.fresh_temp();
904                match *n {
905                    0 => {
906                        self.bindings.push(quote! { let #temp = #arena.zero(); });
907                    }
908                    1 => {
909                        self.bindings.push(quote! { let #temp = #arena.one(); });
910                    }
911                    -1 => {
912                        self.bindings.push(quote! { let #temp = #arena.neg_one(); });
913                    }
914                    _ => {
915                        self.bindings.push(quote! { let #temp = #arena.int(#n); });
916                    }
917                }
918                Ok(temp)
919            }
920
921            MathExpr::Ident(id) => {
922                let name = id.to_string();
923
924                // Wild: return the pre-declared wild expression id.
925                if name.ends_with('_') {
926                    for (i, wn) in self.wild_names.iter().enumerate() {
927                        if wn == id {
928                            return Ok(self.wild_expr_idents[i].clone());
929                        }
930                    }
931                    return Err(syn::Error::new(id.span(), format!("unknown wild '{name}'")));
932                }
933
934                // Known constant.
935                if is_known_constant(&name) {
936                    let temp = self.fresh_temp();
937                    let access = match name.as_str() {
938                        "pi" => quote! { #arena.pi() },
939                        "E" => quote! { #arena.e_const() },
940                        "I" => quote! { #arena.i_unit() },
941                        "oo" => quote! { #arena.infinity() },
942                        "nan" => quote! { #arena.nan() },
943                        "zoo" => quote! { #arena.complex_infinity() },
944                        _ => unreachable!(),
945                    };
946                    self.bindings.push(quote! { let #temp = #access; });
947                    return Ok(temp);
948                }
949
950                // Unknown identifier — error.
951                Err(syn::Error::new(
952                    id.span(),
953                    format!(
954                        "unknown identifier '{name}' in rule!(). \
955                         Use '{name}_' for a wild, or a known constant (pi, E, I, oo, nan, zoo), \
956                         or an integer literal."
957                    ),
958                ))
959            }
960
961            MathExpr::Neg(inner) => {
962                let inner_temp = self.generate_arena_expr(inner)?;
963                let temp = self.fresh_temp();
964                self.bindings
965                    .push(quote! { let #temp = #arena.neg(#inner_temp); });
966                Ok(temp)
967            }
968
969            MathExpr::LogicalNot(inner) => {
970                let inner_temp = self.generate_arena_expr(inner)?;
971                let temp = self.fresh_temp();
972                self.bindings
973                    .push(quote! { let #temp = #arena.not(#inner_temp); });
974                Ok(temp)
975            }
976
977            MathExpr::BinOp { op, lhs, rhs } => {
978                let lhs_temp = self.generate_arena_expr(lhs)?;
979                let rhs_temp = self.generate_arena_expr(rhs)?;
980                let temp = self.fresh_temp();
981
982                let call = match op {
983                    BinOp::Add => quote! { #arena.add(&[#lhs_temp, #rhs_temp]) },
984                    BinOp::Sub => quote! { #arena.sub(#lhs_temp, #rhs_temp) },
985                    BinOp::Mul => quote! { #arena.mul(&[#lhs_temp, #rhs_temp]) },
986                    BinOp::Div => quote! { #arena.div(#lhs_temp, #rhs_temp) },
987                    BinOp::Pow => quote! { #arena.pow(#lhs_temp, #rhs_temp) },
988                    BinOp::Gt => quote! { #arena.gt(#lhs_temp, #rhs_temp) },
989                    BinOp::Lt => quote! { #arena.gt(#rhs_temp, #lhs_temp) },
990                    BinOp::Ge => quote! { #arena.ge(#lhs_temp, #rhs_temp) },
991                    BinOp::Le => quote! { #arena.ge(#rhs_temp, #lhs_temp) },
992                    BinOp::EqEq => quote! { #arena.eq_(#lhs_temp, #rhs_temp) },
993                    BinOp::Ne => quote! { #arena.ne_(#lhs_temp, #rhs_temp) },
994                    BinOp::AndAnd => quote! { #arena.and(&[#lhs_temp, #rhs_temp]) },
995                    BinOp::OrOr => quote! { #arena.or(&[#lhs_temp, #rhs_temp]) },
996                };
997
998                self.bindings.push(quote! { let #temp = #call; });
999                Ok(temp)
1000            }
1001
1002            MathExpr::Func { name, span, args } => {
1003                if !is_known_function(name) {
1004                    return Err(syn::Error::new(
1005                        *span,
1006                        format!(
1007                            "unknown function '{}' in rule!(). Supported: {}",
1008                            name,
1009                            KNOWN_FUNCTIONS.join(", ")
1010                        ),
1011                    ));
1012                }
1013
1014                // Binary functions: beta(a, b), atan2(y, x)
1015                if name == "beta" && args.len() == 2 {
1016                    let a_temp = self.generate_arena_expr(&args[0])?;
1017                    let b_temp = self.generate_arena_expr(&args[1])?;
1018                    let temp = self.fresh_temp();
1019                    self.bindings
1020                        .push(quote! { let #temp = #arena.beta(#a_temp, #b_temp); });
1021                    return Ok(temp);
1022                }
1023                if name == "atan2" && args.len() == 2 {
1024                    let y_temp = self.generate_arena_expr(&args[0])?;
1025                    let x_temp = self.generate_arena_expr(&args[1])?;
1026                    let temp = self.fresh_temp();
1027                    self.bindings
1028                        .push(quote! { let #temp = #arena.atan2(#y_temp, #x_temp); });
1029                    return Ok(temp);
1030                }
1031
1032                if args.len() != 1 {
1033                    return Err(syn::Error::new(
1034                        *span,
1035                        format!("{}() takes exactly 1 argument in rule!()", name),
1036                    ));
1037                }
1038
1039                let arg_temp = self.generate_arena_expr(&args[0])?;
1040                let temp = self.fresh_temp();
1041
1042                let call = match name.as_str() {
1043                    // Core trig
1044                    "sin" => quote! { #arena.sin(#arg_temp) },
1045                    "cos" => quote! { #arena.cos(#arg_temp) },
1046                    "tan" => quote! { #arena.tan(#arg_temp) },
1047                    "asin" => quote! { #arena.asin(#arg_temp) },
1048                    "acos" => quote! { #arena.acos(#arg_temp) },
1049                    "atan" => quote! { #arena.atan(#arg_temp) },
1050                    "sinh" => quote! { #arena.sinh(#arg_temp) },
1051                    "cosh" => quote! { #arena.cosh(#arg_temp) },
1052                    "tanh" => quote! { #arena.tanh(#arg_temp) },
1053                    "asinh" => quote! { #arena.asinh(#arg_temp) },
1054                    "acosh" => quote! { #arena.acosh(#arg_temp) },
1055                    "atanh" => quote! { #arena.atanh(#arg_temp) },
1056                    // Exp/log/root
1057                    "exp" => quote! { #arena.exp(#arg_temp) },
1058                    "ln" => quote! { #arena.ln(#arg_temp) },
1059                    "sqrt" => quote! { #arena.sqrt(#arg_temp) },
1060                    "cbrt" => quote! { #arena.cbrt(#arg_temp) },
1061                    "abs" => quote! { #arena.abs(#arg_temp) },
1062                    "sign" => quote! { #arena.sign(#arg_temp) },
1063                    // Wave B: Floor, Ceiling
1064                    "floor" => quote! { #arena.floor(#arg_temp) },
1065                    "ceiling" => quote! { #arena.ceiling(#arg_temp) },
1066                    // Wave J: Special functions
1067                    "gamma" => quote! { #arena.gamma(#arg_temp) },
1068                    "log_gamma" => quote! { #arena.log_gamma(#arg_temp) },
1069                    "digamma" => quote! { #arena.digamma(#arg_temp) },
1070                    "erf" => quote! { #arena.erf(#arg_temp) },
1071                    "erfc" => quote! { #arena.erfc(#arg_temp) },
1072                    // Wave S/δ: Heaviside, DiracDelta, LambertW
1073                    "heaviside" => quote! { #arena.heaviside(#arg_temp) },
1074                    "dirac_delta" => quote! { #arena.dirac_delta(#arg_temp) },
1075                    "lambertw" => quote! { #arena.lambertw(#arg_temp) },
1076                    // Wave R: Combinatorial (1-arg, Apply-based but have arena methods)
1077                    "fibonacci" => quote! { #arena.fibonacci(#arg_temp) },
1078                    "lucas" => quote! { #arena.lucas(#arg_temp) },
1079                    "catalan_number" => quote! { #arena.catalan_number(#arg_temp) },
1080                    "bell" => quote! { #arena.bell(#arg_temp) },
1081                    "euler_number" => quote! { #arena.euler_number(#arg_temp) },
1082                    "harmonic" => quote! { #arena.harmonic(#arg_temp) },
1083                    "subfactorial" => quote! { #arena.subfactorial(#arg_temp) },
1084                    "factorial2" => quote! { #arena.factorial2(#arg_temp) },
1085                    "bernoulli_number" => quote! { #arena.bernoulli_number(#arg_temp) },
1086                    other => {
1087                        return Err(syn::Error::new(
1088                            *span,
1089                            format!(
1090                                "function '{}' is recognised but not yet supported in rule!()",
1091                                other
1092                            ),
1093                        ));
1094                    }
1095                };
1096
1097                self.bindings.push(quote! { let #temp = #call; });
1098                Ok(temp)
1099            }
1100        }
1101    }
1102}
1103
1104// ═══════════════════════════════════════════════════════════════════════════
1105// matrix! macro
1106// ═══════════════════════════════════════════════════════════════════════════
1107
1108/// Build a symbolic matrix using natural math syntax.
1109///
1110/// # Examples
1111///
1112/// ```ignore
1113/// use symplex::prelude::*;
1114/// use symplex::matrix;
1115///
1116/// let ctx = Context::new();
1117/// let x = ctx.symbol("x");
1118/// let m = matrix![ctx, [x, 1], [0, x]];
1119/// ```
1120#[proc_macro]
1121pub fn matrix(input: TokenStream) -> TokenStream {
1122    let input = syn::parse_macro_input!(input as MatrixMacroInput);
1123    match generate_matrix(&input.ctx, &input) {
1124        Ok(tokens) => tokens.into(),
1125        Err(e) => e.to_compile_error().into(),
1126    }
1127}
1128
1129fn generate_matrix(ctx: &Ident, input: &MatrixMacroInput) -> syn::Result<TokenStream2> {
1130    let mut row_codes = Vec::new();
1131    for row in &input.rows {
1132        let mut cell_codes = Vec::new();
1133        for cell in row {
1134            let cell_expr = generate_expr_as_ex(ctx, cell)?;
1135            cell_codes.push(quote! { #cell_expr });
1136        }
1137        row_codes.push(quote! { vec![#(#cell_codes),*] });
1138    }
1139    // The parser has checked the shape (non-empty, rectangular), so the
1140    // literal is valid by construction: build it without a fallible call.
1141    let nrows = input.rows.len();
1142    let ncols = input.rows[0].len();
1143    Ok(quote! {
1144        {
1145            let __rows: ::std::vec::Vec<::std::vec::Vec<::symplex::expr::Ex>> = vec![#(#row_codes),*];
1146            ::symplex::matrix::Matrix::from_fn(#nrows, #ncols, |__i, __j| __rows[__i][__j].clone())
1147        }
1148    })
1149}
1150
1151// ═══════════════════════════════════════════════════════════════════════════
1152// eq! macro
1153// ═══════════════════════════════════════════════════════════════════════════
1154
1155/// Build a symbolic equation using natural math syntax.
1156///
1157/// # Examples
1158///
1159/// ```ignore
1160/// use symplex::prelude::*;
1161/// use symplex::eq;
1162///
1163/// let ctx = Context::new();
1164/// let x = ctx.symbol("x");
1165/// let equation = eq!(ctx, x^2 + x = 6);
1166/// ```
1167#[proc_macro]
1168pub fn eq(input: TokenStream) -> TokenStream {
1169    let input = syn::parse_macro_input!(input as EqMacroInput);
1170    match generate_eq(&input.ctx, &input) {
1171        Ok(tokens) => tokens.into(),
1172        Err(e) => e.to_compile_error().into(),
1173    }
1174}
1175
1176fn generate_eq(ctx: &Ident, input: &EqMacroInput) -> syn::Result<TokenStream2> {
1177    let lhs_code = generate_expr_as_ex(ctx, &input.lhs)?;
1178    let rhs_code = generate_expr_as_ex(ctx, &input.rhs)?;
1179    Ok(quote! {
1180        ::symplex::eq::Equation::new(#lhs_code, #rhs_code)
1181    })
1182}
1183
1184/// Like [`generate_expr`] but guarantees the result is an `Ex`, even for
1185/// bare integer literals (which `generate_expr` emits as plain `i64`).
1186///
1187/// Function calls (including `diff`, `factorial`, `binomial`, `C`, `log`,
1188/// and all single-arg functions) always return `Ex`, so we delegate
1189/// directly to [`generate_expr`] for those.
1190fn generate_expr_as_ex(ctx: &Ident, expr: &MathExpr) -> syn::Result<TokenStream2> {
1191    match expr {
1192        MathExpr::Int(n, _) => Ok(quote! { #ctx.int(#n) }),
1193        MathExpr::Neg(inner) => {
1194            if let Some(n) = inner.as_int() {
1195                let neg_n = -n;
1196                Ok(quote! { #ctx.int(#neg_n) })
1197            } else {
1198                let code = generate_expr(ctx, expr)?;
1199                Ok(quote! { { let __v: ::symplex::expr::Ex = (#code).clone(); __v } })
1200            }
1201        }
1202        MathExpr::Func { .. } => generate_expr(ctx, expr),
1203        _ => {
1204            let code = generate_expr(ctx, expr)?;
1205            Ok(quote! { { let __v: ::symplex::expr::Ex = (#code).clone(); __v } })
1206        }
1207    }
1208}