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