Skip to main content

ries_rs/
gen.rs

1//! Expression generation
2//!
3//! Generates valid postfix expressions by enumerating "forms" (stack effect patterns).
4//!
5//! # Streaming Architecture
6//!
7//! For high complexity levels, the traditional approach of generating ALL expressions
8//! into memory before matching can cause memory exhaustion. This module provides both:
9//!
10//! - **Batch generation**: `generate_all()` returns all expressions (backward compatible)
11//! - **Streaming generation**: `generate_streaming()` processes expressions via callbacks
12//!
13//! Streaming reduces memory from O(expressions) to O(depth) by processing expressions
14//! as they're generated rather than accumulating them.
15
16use crate::eval::{evaluate_fast_with_context, EvalContext};
17use crate::profile::{Profile, UserConstant, MAX_USER_SYMBOLS};
18use crate::symbol_table::SymbolTable;
19use crate::thresholds::QUANTIZE_SCALE;
20use std::sync::Arc;
21
22// =============================================================================
23// NAMED CONSTANTS FOR QUANTIZATION AND VALUE LIMITS
24// =============================================================================
25
26/// Maximum absolute value for quantization before using sentinel values.
27///
28/// Values larger than this threshold are represented by sentinel values
29/// (i64::MAX - 1 for positive, i64::MIN + 1 for negative) to avoid
30/// overflow during the quantization calculation.
31const MAX_QUANTIZED_VALUE: f64 = 1e10;
32
33/// Maximum absolute value for generated expressions.
34///
35/// Expressions with values larger than this are considered overflow-prone
36/// and unlikely to be useful, so they are filtered out during generation.
37const MAX_GENERATED_VALUE: f64 = 1e12;
38use crate::expr::{EvaluatedExpr, Expression, MAX_EXPR_LEN};
39use crate::symbol::{NumType, Seft, Symbol};
40use crate::udf::UserFunction;
41use std::collections::HashMap;
42
43/// Configuration for expression generation
44///
45/// Controls which symbols are available, complexity limits,
46/// and various generation options for creating candidate expressions
47/// that may solve a given equation.
48///
49/// # Architecture
50///
51/// Expressions are generated in two categories:
52/// - **LHS (Left-Hand Side)**: Expressions containing `x`, representing functions like `f(x)`
53/// - **RHS (Right-Hand Side)**: Constant expressions not containing `x`, like `π²` or `sqrt(2)`
54///
55/// The generator creates all valid expressions up to the configured complexity limits,
56/// then the solver finds pairs where `LHS(target) ≈ RHS`.
57///
58/// # Example
59///
60/// ```rust
61/// use ries_rs::gen::GenConfig;
62/// use ries_rs::symbol::Symbol;
63/// use std::collections::HashMap;
64///
65/// let config = GenConfig {
66///     max_lhs_complexity: 50,
67///     max_rhs_complexity: 30,
68///     max_length: 12,
69///     constants: vec![Symbol::One, Symbol::Two, Symbol::Pi, Symbol::E],
70///     unary_ops: vec![Symbol::Neg, Symbol::Sqrt, Symbol::Square],
71///     binary_ops: vec![Symbol::Add, Symbol::Sub, Symbol::Mul, Symbol::Div],
72///     ..GenConfig::default()
73/// };
74/// ```
75#[derive(Clone)]
76pub struct GenConfig {
77    /// Maximum complexity score for left-hand-side expressions.
78    ///
79    /// LHS expressions contain `x` and represent the function side of equations.
80    /// Higher values allow more complex expressions (e.g., `sin(x) + x²`), but
81    /// exponentially increase search time and memory usage.
82    ///
83    /// Default: 128 (allows fairly complex expressions)
84    pub max_lhs_complexity: u32,
85
86    /// Maximum complexity score for right-hand-side expressions.
87    ///
88    /// RHS expressions are constants not containing `x`. Since they don't need
89    /// to be solved for, they can typically use lower complexity limits than LHS.
90    ///
91    /// Default: 128
92    pub max_rhs_complexity: u32,
93
94    /// Maximum number of symbols in a single expression.
95    ///
96    /// This is a hard limit on expression length regardless of complexity score.
97    /// Prevents pathological cases with many low-complexity symbols.
98    ///
99    /// Default: `MAX_EXPR_LEN` (21)
100    pub max_length: usize,
101
102    /// Symbols available for constants and variables (Seft::A type).
103    ///
104    /// These push a value onto the expression stack. Typically includes:
105    /// - `One`, `Two`, `Three`, etc. (numeric constants)
106    /// - `Pi`, `E` (mathematical constants)
107    /// - `X` (the variable to solve for)
108    ///
109    /// Default: All built-in constants from `Symbol::constants()`
110    pub constants: Vec<Symbol>,
111
112    /// Symbols available for unary operations (Seft::B type).
113    ///
114    /// These transform a single value: `f(a)`. Includes operations like:
115    /// - `Neg` (negation: `-a`)
116    /// - `Sqrt`, `Square` (powers and roots)
117    /// - `SinPi`, `CosPi` (trigonometric functions)
118    /// - `Ln`, `Exp` (logarithmic and exponential)
119    /// - `Recip` (reciprocal: `1/a`)
120    ///
121    /// Default: All built-in unary operators from `Symbol::unary_ops()`
122    pub unary_ops: Vec<Symbol>,
123
124    /// Symbols available for binary operations (Seft::C type).
125    ///
126    /// These combine two values: `f(a, b)`. Includes operations like:
127    /// - `Add`, `Sub`, `Mul`, `Div` (arithmetic)
128    /// - `Pow`, `Root`, `Log` (power functions and logarithms)
129    ///
130    /// Default: All built-in binary operators from `Symbol::binary_ops()`
131    pub binary_ops: Vec<Symbol>,
132
133    /// Optional override for RHS-only constant symbols.
134    ///
135    /// When set, RHS expressions use these symbols instead of `constants`.
136    /// Useful for generating LHS with more symbols but keeping RHS simple.
137    ///
138    /// Default: `None` (use `constants` for both LHS and RHS)
139    pub rhs_constants: Option<Vec<Symbol>>,
140
141    /// Optional override for RHS-only unary operators.
142    ///
143    /// When set, RHS expressions use these operators instead of `unary_ops`.
144    /// Example: allow Lambert W in LHS only, exclude from RHS constants.
145    ///
146    /// Default: `None` (use `unary_ops` for both LHS and RHS)
147    pub rhs_unary_ops: Option<Vec<Symbol>>,
148
149    /// Optional override for RHS-only binary operators.
150    ///
151    /// When set, RHS expressions use these operators instead of `binary_ops`.
152    ///
153    /// Default: `None` (use `binary_ops` for both LHS and RHS)
154    pub rhs_binary_ops: Option<Vec<Symbol>>,
155
156    /// Maximum usage count per symbol within a single expression.
157    ///
158    /// Maps each symbol to the maximum number of times it can appear.
159    /// Useful for limiting redundancy (e.g., max 2 uses of `Pi`).
160    /// Corresponds to the `-O` command-line option.
161    ///
162    /// Default: Empty (no limits)
163    pub symbol_max_counts: HashMap<Symbol, u32>,
164
165    /// Optional RHS-only symbol count limits.
166    ///
167    /// When set, applies different symbol count limits to RHS expressions.
168    /// Corresponds to the `--O-RHS` command-line option.
169    ///
170    /// Default: `None` (use `symbol_max_counts` for both)
171    pub rhs_symbol_max_counts: Option<HashMap<Symbol, u32>>,
172
173    /// Minimum numeric type required for generated expressions.
174    ///
175    /// Filters expressions by the "sophistication" of numbers they produce:
176    /// - `Integer`: Only integer results
177    /// - `Rational`: Rational numbers (fractions)
178    /// - `Algebraic`: Algebraic numbers (roots of polynomials)
179    /// - `Transcendental`: Any real number (including π, e, trig)
180    ///
181    /// Lower values restrict output to simpler mathematical constructs.
182    ///
183    /// Default: `NumType::Transcendental` (accept all)
184    pub min_num_type: NumType,
185
186    /// Whether to generate LHS expressions containing `x`.
187    ///
188    /// Set to `false` if you only need constant RHS expressions.
189    /// Can significantly reduce generation time when LHS is not needed.
190    ///
191    /// Default: `true`
192    pub generate_lhs: bool,
193
194    /// Whether to generate RHS constant expressions.
195    ///
196    /// Set to `false` if you only need LHS expressions.
197    /// Useful for specific analysis tasks.
198    ///
199    /// Default: `true`
200    pub generate_rhs: bool,
201
202    /// User-defined constants for custom searches.
203    ///
204    /// These constants are available during expression evaluation,
205    /// allowing searches involving domain-specific values.
206    /// Defined via `-N` command-line option.
207    ///
208    /// Default: Empty
209    pub user_constants: Vec<UserConstant>,
210
211    /// User-defined functions for custom searches.
212    ///
213    /// Custom functions that can appear in generated expressions,
214    /// extending the available operations beyond built-in symbols.
215    /// Defined via `-F` command-line option.
216    ///
217    /// Default: Empty
218    pub user_functions: Vec<UserFunction>,
219
220    /// Enable diagnostic output for arithmetic pruning.
221    ///
222    /// When `true`, prints information about expressions that were
223    /// discarded due to arithmetic errors (overflow, domain errors, etc.).
224    /// Useful for debugging generation behavior.
225    ///
226    /// Default: `false`
227    pub show_pruned_arith: bool,
228
229    /// Symbol table with weights and display names.
230    ///
231    /// Provides complexity weights for each symbol and custom display
232    /// names. Weights control how "expensive" each symbol is toward
233    /// the complexity limit.
234    ///
235    /// Default: Empty table (uses built-in default weights)
236    pub symbol_table: Arc<SymbolTable>,
237}
238
239/// Options for additional expression constraints
240///
241/// These constraints allow filtering expressions based on their numeric properties
242/// or structural limits (like trig cycles or exponent types).
243#[derive(Debug, Clone, Copy)]
244pub struct ExpressionConstraintOptions {
245    /// If true, power exponents must be rational (no transcendental exponents like x^pi)
246    pub rational_exponents: bool,
247    /// If true, trigonometric function arguments must be rational
248    pub rational_trig_args: bool,
249    /// Maximum number of trigonometric operations allowed in an expression
250    pub max_trig_cycles: Option<u32>,
251    /// Inherited numeric types for user-defined constants 0-15
252    pub user_constant_types: [NumType; 16],
253    /// Inherited numeric types for user-defined functions 0-15
254    pub user_function_types: [NumType; 16],
255}
256
257impl Default for ExpressionConstraintOptions {
258    fn default() -> Self {
259        Self {
260            rational_exponents: false,
261            rational_trig_args: false,
262            max_trig_cycles: None,
263            user_constant_types: [NumType::Transcendental; 16],
264            user_function_types: [NumType::Transcendental; 16],
265        }
266    }
267}
268
269/// Check if an expression respects the configured structural and numeric constraints.
270///
271/// This performs a symbolic walkthrough of the expression to verify that it
272/// matches the requested properties (e.g., no transcendental exponents).
273pub fn expression_respects_constraints(
274    expression: &Expression,
275    opts: ExpressionConstraintOptions,
276) -> bool {
277    #[derive(Clone, Copy)]
278    struct ConstraintValue {
279        has_x: bool,
280        num_type: NumType,
281    }
282
283    let mut stack: Vec<ConstraintValue> = Vec::with_capacity(expression.len());
284    let mut trig_ops: u32 = 0;
285
286    for &sym in expression.symbols() {
287        match sym.seft() {
288            Seft::A => {
289                let num_type = if let Some(idx) = sym.user_constant_index() {
290                    opts.user_constant_types[idx as usize]
291                } else {
292                    sym.inherent_type()
293                };
294                stack.push(ConstraintValue {
295                    has_x: sym == Symbol::X,
296                    num_type,
297                });
298            }
299            Seft::B => {
300                let Some(arg) = stack.pop() else {
301                    return false;
302                };
303
304                if matches!(sym, Symbol::SinPi | Symbol::CosPi | Symbol::TanPi) {
305                    trig_ops = trig_ops.saturating_add(1);
306                    if opts.rational_trig_args && (arg.has_x || arg.num_type < NumType::Rational) {
307                        return false;
308                    }
309                }
310
311                let num_type = match sym {
312                    Symbol::Neg | Symbol::Square => arg.num_type,
313                    Symbol::Recip => {
314                        if arg.num_type >= NumType::Rational {
315                            NumType::Rational
316                        } else {
317                            arg.num_type
318                        }
319                    }
320                    Symbol::Sqrt => {
321                        if arg.num_type >= NumType::Rational {
322                            NumType::Algebraic
323                        } else {
324                            arg.num_type
325                        }
326                    }
327                    Symbol::UserFunction0
328                    | Symbol::UserFunction1
329                    | Symbol::UserFunction2
330                    | Symbol::UserFunction3
331                    | Symbol::UserFunction4
332                    | Symbol::UserFunction5
333                    | Symbol::UserFunction6
334                    | Symbol::UserFunction7
335                    | Symbol::UserFunction8
336                    | Symbol::UserFunction9
337                    | Symbol::UserFunction10
338                    | Symbol::UserFunction11
339                    | Symbol::UserFunction12
340                    | Symbol::UserFunction13
341                    | Symbol::UserFunction14
342                    | Symbol::UserFunction15 => {
343                        let idx = sym.user_function_index().unwrap_or(0) as usize;
344                        opts.user_function_types[idx]
345                    }
346                    _ => NumType::Transcendental,
347                };
348
349                stack.push(ConstraintValue {
350                    has_x: arg.has_x,
351                    num_type,
352                });
353            }
354            Seft::C => {
355                let Some(rhs) = stack.pop() else {
356                    return false;
357                };
358                let Some(lhs) = stack.pop() else {
359                    return false;
360                };
361
362                if opts.rational_exponents
363                    && sym == Symbol::Pow
364                    && (rhs.has_x || rhs.num_type < NumType::Rational)
365                {
366                    return false;
367                }
368
369                let num_type = match sym {
370                    Symbol::Add | Symbol::Sub | Symbol::Mul => lhs.num_type.combine(rhs.num_type),
371                    Symbol::Div => {
372                        let combined = lhs.num_type.combine(rhs.num_type);
373                        if combined == NumType::Integer {
374                            NumType::Rational
375                        } else {
376                            combined
377                        }
378                    }
379                    Symbol::Pow => {
380                        if rhs.has_x {
381                            NumType::Transcendental
382                        } else if rhs.num_type == NumType::Integer {
383                            lhs.num_type
384                        } else if lhs.num_type >= NumType::Rational
385                            && rhs.num_type >= NumType::Rational
386                        {
387                            NumType::Algebraic
388                        } else {
389                            NumType::Transcendental
390                        }
391                    }
392                    Symbol::Root => NumType::Algebraic,
393                    Symbol::Log | Symbol::Atan2 => NumType::Transcendental,
394                    _ => NumType::Transcendental,
395                };
396
397                stack.push(ConstraintValue {
398                    has_x: lhs.has_x || rhs.has_x,
399                    num_type,
400                });
401            }
402        }
403    }
404
405    if stack.len() != 1 {
406        return false;
407    }
408
409    opts.max_trig_cycles
410        .is_none_or(|max_cycles| trig_ops <= max_cycles)
411}
412
413impl Default for GenConfig {
414    fn default() -> Self {
415        Self {
416            max_lhs_complexity: 128,
417            max_rhs_complexity: 128,
418            max_length: MAX_EXPR_LEN,
419            constants: Symbol::constants().to_vec(),
420            unary_ops: Symbol::unary_ops().to_vec(),
421            binary_ops: Symbol::binary_ops().to_vec(),
422            rhs_constants: None,
423            rhs_unary_ops: None,
424            rhs_binary_ops: None,
425            symbol_max_counts: HashMap::new(),
426            rhs_symbol_max_counts: None,
427            min_num_type: NumType::Transcendental,
428            generate_lhs: true,
429            generate_rhs: true,
430            user_constants: Vec::new(),
431            user_functions: Vec::new(),
432            show_pruned_arith: false,
433            symbol_table: Arc::new(SymbolTable::new()),
434        }
435    }
436}
437
438/// Build a baseline generation config from a profile.
439///
440/// This is the shared source of truth for Python, WASM, and CLI setup before
441/// CLI-specific symbol filtering is applied.
442pub fn build_gen_config_from_profile(
443    max_lhs_complexity: u32,
444    max_rhs_complexity: u32,
445    profile: &Profile,
446) -> Result<GenConfig, String> {
447    validate_user_symbol_capacity(profile.constants.len(), profile.functions.len())?;
448
449    let mut config = GenConfig {
450        max_lhs_complexity,
451        max_rhs_complexity,
452        user_constants: profile.constants.clone(),
453        user_functions: profile.functions.clone(),
454        symbol_table: Arc::new(SymbolTable::from_profile(profile)),
455        ..GenConfig::default()
456    };
457
458    for idx in 0..profile.constants.len() {
459        if let Some(sym) = Symbol::from_byte(128 + idx as u8) {
460            config.constants.push(sym);
461        }
462    }
463    for idx in 0..profile.functions.len() {
464        if let Some(sym) = Symbol::from_byte(144 + idx as u8) {
465            config.unary_ops.push(sym);
466        }
467    }
468
469    Ok(config)
470}
471
472/// Validate that the fixed user symbol slots are not exceeded.
473pub fn validate_user_symbol_capacity(
474    constant_count: usize,
475    function_count: usize,
476) -> Result<(), String> {
477    if constant_count > MAX_USER_SYMBOLS {
478        return Err(format!(
479            "At most {} user constants are supported (got {})",
480            MAX_USER_SYMBOLS, constant_count
481        ));
482    }
483    if function_count > MAX_USER_SYMBOLS {
484        return Err(format!(
485            "At most {} user functions are supported (got {})",
486            MAX_USER_SYMBOLS, function_count
487        ));
488    }
489    Ok(())
490}
491
492/// Result of expression generation
493pub struct GeneratedExprs {
494    /// LHS expressions (contain x)
495    pub lhs: Vec<EvaluatedExpr>,
496    /// RHS expressions (constants only)
497    pub rhs: Vec<EvaluatedExpr>,
498}
499
500/// Callbacks for streaming expression generation
501///
502/// Using callbacks instead of accumulation allows processing expressions
503/// as they're generated, reducing memory from O(expressions) to O(depth).
504pub struct StreamingCallbacks<'a> {
505    /// Called for each RHS (constant-only) expression generated
506    /// Return false to stop generation early
507    pub on_rhs: &'a mut dyn FnMut(&EvaluatedExpr) -> bool,
508    /// Called for each LHS (contains x) expression generated
509    /// Return false to stop generation early
510    pub on_lhs: &'a mut dyn FnMut(&EvaluatedExpr) -> bool,
511}
512
513/// Quantize a value to reduce floating-point noise
514/// Key for LHS deduplication: (quantized value, quantized derivative)
515pub type LhsKey = (i64, i64);
516
517/// Uses ~8 significant digits for deduplication
518#[inline]
519pub fn quantize_value(v: f64) -> i64 {
520    if !v.is_finite() || v.abs() > MAX_QUANTIZED_VALUE {
521        // For very large values, use a different quantization to avoid overflow
522        if v > MAX_QUANTIZED_VALUE {
523            return i64::MAX - 1;
524        } else if v < -MAX_QUANTIZED_VALUE {
525            return i64::MIN + 1;
526        }
527        return i64::MAX;
528    }
529    // Scale to preserve ~8 significant digits (avoid overflow)
530    (v * QUANTIZE_SCALE).round() as i64
531}
532
533fn dedupe_rhs_expressions(rhs_raw: Vec<EvaluatedExpr>) -> Vec<EvaluatedExpr> {
534    use std::collections::hash_map::Entry;
535
536    let mut rhs_map: HashMap<i64, EvaluatedExpr> = HashMap::new();
537    for expr in rhs_raw {
538        let key = quantize_value(expr.value);
539        match rhs_map.entry(key) {
540            Entry::Occupied(mut slot) => {
541                if expr.expr.complexity() < slot.get().expr.complexity() {
542                    *slot.get_mut() = expr;
543                }
544            }
545            Entry::Vacant(slot) => {
546                slot.insert(expr);
547            }
548        }
549    }
550    rhs_map.into_values().collect()
551}
552
553fn dedupe_lhs_expressions(lhs_raw: Vec<EvaluatedExpr>) -> Vec<EvaluatedExpr> {
554    use std::collections::hash_map::Entry;
555
556    let mut lhs_map: HashMap<LhsKey, EvaluatedExpr> = HashMap::new();
557    for expr in lhs_raw {
558        let key = (quantize_value(expr.value), quantize_value(expr.derivative));
559        match lhs_map.entry(key) {
560            Entry::Occupied(mut slot) => {
561                if expr.expr.complexity() < slot.get().expr.complexity() {
562                    *slot.get_mut() = expr;
563                }
564            }
565            Entry::Vacant(slot) => {
566                slot.insert(expr);
567            }
568        }
569    }
570    lhs_map.into_values().collect()
571}
572
573/// Generate all valid expressions up to the configured limits
574pub fn generate_all(config: &GenConfig, target: f64) -> GeneratedExprs {
575    generate_all_with_context(
576        config,
577        target,
578        &EvalContext::from_slices(&config.user_constants, &config.user_functions),
579    )
580}
581
582/// Generate all valid expressions up to the configured limits using an explicit evaluation context.
583pub fn generate_all_with_context(
584    config: &GenConfig,
585    target: f64,
586    eval_context: &EvalContext<'_>,
587) -> GeneratedExprs {
588    let mut lhs_raw = Vec::new();
589    let mut rhs_raw = Vec::new();
590
591    if config.generate_lhs && config.generate_rhs && has_rhs_symbol_overrides(config) {
592        // LHS pass with base symbol set.
593        let mut lhs_config = config.clone();
594        lhs_config.generate_lhs = true;
595        lhs_config.generate_rhs = false;
596        generate_recursive(
597            &lhs_config,
598            target,
599            *eval_context,
600            &mut Expression::new(),
601            0,
602            &mut lhs_raw,
603            &mut rhs_raw,
604        );
605
606        // RHS pass with RHS-specific symbol overrides.
607        let rhs_config = rhs_only_config(config);
608        generate_recursive(
609            &rhs_config,
610            target,
611            *eval_context,
612            &mut Expression::new(),
613            0,
614            &mut lhs_raw,
615            &mut rhs_raw,
616        );
617    } else {
618        // Generate expressions for each possible "form" (sequence of stack effects)
619        generate_recursive(
620            config,
621            target,
622            *eval_context,
623            &mut Expression::new(),
624            0, // current stack depth
625            &mut lhs_raw,
626            &mut rhs_raw,
627        );
628    }
629
630    // Deduplicate RHS by value, keeping simplest expression for each value
631    GeneratedExprs {
632        lhs: dedupe_lhs_expressions(lhs_raw),
633        rhs: dedupe_rhs_expressions(rhs_raw),
634    }
635}
636
637/// Generate expressions with an early-abort limit on total count.
638///
639/// Returns `Some(expressions)` if generation completed within the limit,
640/// or `None` if the limit was exceeded (caller should use streaming mode instead).
641///
642/// This is a safety mechanism to prevent OOM from unexpectedly large generation
643/// at high complexity levels. The limit check happens during generation, not after.
644///
645/// # Arguments
646///
647/// * `config` - Generation configuration (complexity limits, symbols)
648/// * `target` - Target value for evaluation
649/// * `max_expressions` - Maximum total expressions (LHS + RHS) before aborting
650///
651/// # Returns
652///
653/// * `Some(GeneratedExprs)` - if generation completed within limit
654/// * `None` - if the limit was exceeded during generation
655pub fn generate_all_with_limit(
656    config: &GenConfig,
657    target: f64,
658    max_expressions: usize,
659) -> Option<GeneratedExprs> {
660    generate_all_with_limit_and_context(
661        config,
662        target,
663        &EvalContext::from_slices(&config.user_constants, &config.user_functions),
664        max_expressions,
665    )
666}
667
668/// Generate expressions with an early-abort limit using an explicit evaluation context.
669pub fn generate_all_with_limit_and_context(
670    config: &GenConfig,
671    target: f64,
672    eval_context: &EvalContext<'_>,
673    max_expressions: usize,
674) -> Option<GeneratedExprs> {
675    use std::sync::atomic::{AtomicUsize, Ordering};
676    use std::sync::Arc;
677
678    let count = Arc::new(AtomicUsize::new(0));
679    let limit = max_expressions;
680
681    // Collect expressions if within limit
682    let mut lhs_raw = Vec::new();
683    let mut rhs_raw = Vec::new();
684
685    // Callback that counts expressions and stops when limit is hit
686    let mut callbacks = StreamingCallbacks {
687        on_lhs: &mut |expr| {
688            let current = count.fetch_add(1, Ordering::Relaxed) + 1;
689            if current > limit {
690                return false; // Abort generation
691            }
692            lhs_raw.push(expr.clone());
693            true
694        },
695        on_rhs: &mut |expr| {
696            let current = count.fetch_add(1, Ordering::Relaxed) + 1;
697            if current > limit {
698                return false; // Abort generation
699            }
700            rhs_raw.push(expr.clone());
701            true
702        },
703    };
704
705    generate_streaming_with_context(config, target, eval_context, &mut callbacks);
706
707    // Check if we exceeded the limit
708    let final_count = count.load(Ordering::Relaxed);
709    if final_count > limit {
710        return None;
711    }
712
713    // Deduplicate (same logic as generate_all)
714    Some(GeneratedExprs {
715        lhs: dedupe_lhs_expressions(lhs_raw),
716        rhs: dedupe_rhs_expressions(rhs_raw),
717    })
718}
719
720/// Generate expressions with an early-abort limit, preserving every LHS.
721///
722/// The streaming search path matches every generated LHS expression and only
723/// deduplicates RHS expressions for the value database. Turbo uses this helper
724/// when it wants to materialize the same LHS universe for parallel matching.
725/// Deduplicating LHS by target-local `(value, derivative)` can discard a useful
726/// non-degenerate expression in favor of a lower-complexity degenerate
727/// representative, changing the best match for flat exact roots.
728#[cfg(feature = "parallel")]
729pub fn generate_all_preserving_lhs_with_limit_and_context(
730    config: &GenConfig,
731    target: f64,
732    eval_context: &EvalContext<'_>,
733    max_expressions: usize,
734) -> Option<GeneratedExprs> {
735    use std::sync::atomic::{AtomicUsize, Ordering};
736    use std::sync::Arc;
737
738    let count = Arc::new(AtomicUsize::new(0));
739    let limit = max_expressions;
740
741    let mut lhs_raw = Vec::new();
742    let mut rhs_raw = Vec::new();
743
744    let mut callbacks = StreamingCallbacks {
745        on_lhs: &mut |expr| {
746            let current = count.fetch_add(1, Ordering::Relaxed) + 1;
747            if current > limit {
748                return false;
749            }
750            lhs_raw.push(expr.clone());
751            true
752        },
753        on_rhs: &mut |expr| {
754            let current = count.fetch_add(1, Ordering::Relaxed) + 1;
755            if current > limit {
756                return false;
757            }
758            rhs_raw.push(expr.clone());
759            true
760        },
761    };
762
763    generate_streaming_with_context(config, target, eval_context, &mut callbacks);
764
765    let final_count = count.load(Ordering::Relaxed);
766    if final_count > limit {
767        return None;
768    }
769
770    Some(GeneratedExprs {
771        lhs: lhs_raw,
772        rhs: dedupe_rhs_expressions(rhs_raw),
773    })
774}
775
776/// Generate expressions with streaming callbacks for memory-efficient processing
777///
778/// This function is the foundation of the streaming architecture. Instead of
779/// accumulating all expressions in memory, it calls the provided callbacks
780/// for each generated expression, allowing immediate processing.
781///
782/// # Memory Efficiency
783///
784/// - Traditional: O(expressions) memory - all expressions stored before processing
785/// - Streaming: O(depth) memory - only the recursion stack is stored
786///
787/// # Early Exit
788///
789/// The callbacks can return `false` to signal early termination. This is useful
790/// when good matches have been found and additional expressions aren't needed.
791///
792/// # Deduplication
793///
794/// The caller is responsible for deduplication if needed. This allows flexibility
795/// in deduplication strategies (e.g., per-batch, per-tier, etc.).
796///
797/// # Example
798///
799/// ```no_run
800/// use ries_rs::gen::{GenConfig, StreamingCallbacks, generate_streaming};
801/// let config = GenConfig::default();
802/// let target = 2.5_f64;
803/// let mut rhs_count = 0;
804/// let mut lhs_count = 0;
805/// let mut callbacks = StreamingCallbacks {
806///     on_rhs: &mut |_expr| {
807///         rhs_count += 1;
808///         true // continue generation
809///     },
810///     on_lhs: &mut |_expr| {
811///         lhs_count += 1;
812///         true // continue generation
813///     },
814/// };
815/// generate_streaming(&config, target, &mut callbacks);
816/// ```
817pub fn generate_streaming(config: &GenConfig, target: f64, callbacks: &mut StreamingCallbacks) {
818    generate_streaming_with_context(
819        config,
820        target,
821        &EvalContext::from_slices(&config.user_constants, &config.user_functions),
822        callbacks,
823    );
824}
825
826/// Generate expressions with streaming callbacks using an explicit evaluation context.
827pub fn generate_streaming_with_context(
828    config: &GenConfig,
829    target: f64,
830    eval_context: &EvalContext<'_>,
831    callbacks: &mut StreamingCallbacks,
832) {
833    if config.generate_lhs && config.generate_rhs && has_rhs_symbol_overrides(config) {
834        let mut lhs_config = config.clone();
835        lhs_config.generate_lhs = true;
836        lhs_config.generate_rhs = false;
837        if !generate_recursive_streaming(
838            &lhs_config,
839            target,
840            *eval_context,
841            &mut Expression::new(),
842            0,
843            callbacks,
844        ) {
845            return;
846        }
847
848        let rhs_config = rhs_only_config(config);
849        generate_recursive_streaming(
850            &rhs_config,
851            target,
852            *eval_context,
853            &mut Expression::new(),
854            0,
855            callbacks,
856        );
857    } else {
858        generate_recursive_streaming(
859            config,
860            target,
861            *eval_context,
862            &mut Expression::new(),
863            0, // current stack depth
864            callbacks,
865        );
866    }
867}
868
869/// Count generated expressions with an early-abort limit using streaming callbacks.
870///
871/// Returns `Some(count)` when generation completes within the limit, or `None`
872/// as soon as the count exceeds `max_expressions`.
873pub fn count_expressions_with_limit_and_context(
874    config: &GenConfig,
875    target: f64,
876    eval_context: &EvalContext<'_>,
877    max_expressions: usize,
878) -> Option<usize> {
879    let count = std::cell::Cell::new(0usize);
880    let mut callbacks = StreamingCallbacks {
881        on_lhs: &mut |_expr| {
882            let next = count.get() + 1;
883            count.set(next);
884            next <= max_expressions
885        },
886        on_rhs: &mut |_expr| {
887            let next = count.get() + 1;
888            count.set(next);
889            next <= max_expressions
890        },
891    };
892
893    generate_streaming_with_context(config, target, eval_context, &mut callbacks);
894
895    if count.get() > max_expressions {
896        None
897    } else {
898        Some(count.get())
899    }
900}
901
902#[inline]
903fn has_rhs_symbol_overrides(config: &GenConfig) -> bool {
904    config.rhs_constants.is_some()
905        || config.rhs_unary_ops.is_some()
906        || config.rhs_binary_ops.is_some()
907        || config.rhs_symbol_max_counts.is_some()
908}
909
910/// Check if an evaluated expression meets generation criteria
911///
912/// This shared helper function is used by both batch and streaming generation
913/// to validate expressions before including them in results.
914#[inline]
915fn should_include_expression(
916    result: &crate::eval::EvalResult,
917    config: &GenConfig,
918    complexity: u32,
919    contains_x: bool,
920) -> bool {
921    result.value.is_finite()
922        && result.value.abs() <= MAX_GENERATED_VALUE
923        && result.num_type >= config.min_num_type
924        && if contains_x {
925            config.generate_lhs && complexity <= config.max_lhs_complexity
926        } else {
927            config.generate_rhs && complexity <= config.max_rhs_complexity
928        }
929}
930
931/// Calculate the appropriate complexity limit based on whether expression contains x
932///
933/// For expressions containing x, uses LHS limit.
934/// For RHS-only paths, uses RHS limit.
935/// For paths that might still add x, uses the max of both limits.
936#[inline]
937fn get_max_complexity(config: &GenConfig, contains_x: bool) -> u32 {
938    if contains_x {
939        config.max_lhs_complexity
940    } else {
941        // For RHS-only paths, use RHS limit
942        // For paths that might still add x, use the max of both
943        std::cmp::max(config.max_lhs_complexity, config.max_rhs_complexity)
944    }
945}
946
947fn rhs_only_config(config: &GenConfig) -> GenConfig {
948    let mut rhs_config = config.clone();
949    rhs_config.generate_lhs = false;
950    rhs_config.generate_rhs = true;
951    if let Some(constants) = &config.rhs_constants {
952        rhs_config.constants = constants.clone();
953    }
954    if let Some(unary_ops) = &config.rhs_unary_ops {
955        rhs_config.unary_ops = unary_ops.clone();
956    }
957    if let Some(binary_ops) = &config.rhs_binary_ops {
958        rhs_config.binary_ops = binary_ops.clone();
959    }
960    if let Some(rhs_symbol_max_counts) = &config.rhs_symbol_max_counts {
961        rhs_config.symbol_max_counts = rhs_symbol_max_counts.clone();
962    }
963    rhs_config
964}
965
966#[inline]
967fn exceeds_symbol_limit(config: &GenConfig, current: &Expression, sym: Symbol) -> bool {
968    config
969        .symbol_max_counts
970        .get(&sym)
971        .is_some_and(|&max| current.count_symbol(sym) >= max)
972}
973
974/// Recursively generate expressions with streaming callbacks
975///
976/// This is the core streaming generation function. It mirrors `generate_recursive`
977/// but calls callbacks instead of accumulating expressions.
978fn generate_recursive_streaming(
979    config: &GenConfig,
980    target: f64,
981    eval_context: EvalContext<'_>,
982    current: &mut Expression,
983    stack_depth: usize,
984    callbacks: &mut StreamingCallbacks,
985) -> bool {
986    // Check if we have a complete expression
987    if stack_depth == 1 && !current.is_empty() {
988        // Try to evaluate it with user constants and functions support
989        match evaluate_fast_with_context(current, target, &eval_context) {
990            Ok(result) => {
991                // Use shared validation helper
992                if should_include_expression(
993                    &result,
994                    config,
995                    current.complexity(),
996                    current.contains_x(),
997                ) {
998                    let expr = current.clone();
999                    let eval_expr =
1000                        EvaluatedExpr::new(expr, result.value, result.derivative, result.num_type);
1001
1002                    // Call the appropriate callback; return false if it signals stop
1003                    let should_continue = if current.contains_x() {
1004                        (callbacks.on_lhs)(&eval_expr)
1005                    } else {
1006                        (callbacks.on_rhs)(&eval_expr)
1007                    };
1008                    if !should_continue {
1009                        return false;
1010                    }
1011                }
1012            }
1013            Err(e) => {
1014                // Expression was pruned due to arithmetic error
1015                if config.show_pruned_arith {
1016                    eprintln!(
1017                        "  [pruned arith] expression=\"{}\" reason={:?}",
1018                        current.to_postfix(),
1019                        e
1020                    );
1021                }
1022            }
1023        }
1024    }
1025
1026    // Check limits before recursing
1027    if current.len() >= config.max_length {
1028        return true;
1029    }
1030
1031    // Use shared helper for complexity limit calculation
1032    let max_complexity = get_max_complexity(config, current.contains_x());
1033
1034    if current.complexity() >= max_complexity {
1035        return true;
1036    }
1037
1038    // Calculate minimum additional complexity needed to complete expression
1039    let min_remaining = min_complexity_to_complete(stack_depth, config);
1040    if current.complexity() + min_remaining > max_complexity {
1041        return true;
1042    }
1043
1044    // Try adding each possible symbol
1045
1046    // Constants (Seft::A) - always increase stack by 1
1047    for &sym in &config.constants {
1048        let sym_weight = config.symbol_table.weight(sym);
1049        if current.complexity() + sym_weight > max_complexity {
1050            continue;
1051        }
1052        if exceeds_symbol_limit(config, current, sym) {
1053            continue;
1054        }
1055
1056        // Skip x if we only want RHS
1057        if sym == Symbol::X && !config.generate_lhs {
1058            continue;
1059        }
1060
1061        current.push_with_table(sym, &config.symbol_table);
1062        if !generate_recursive_streaming(
1063            config,
1064            target,
1065            eval_context,
1066            current,
1067            stack_depth + 1,
1068            callbacks,
1069        ) {
1070            current.pop_with_table(&config.symbol_table);
1071            return false;
1072        }
1073        current.pop_with_table(&config.symbol_table);
1074    }
1075
1076    // Also add x for LHS generation
1077    if config.generate_lhs && !config.constants.contains(&Symbol::X) {
1078        let sym = Symbol::X;
1079        let sym_weight = config.symbol_table.weight(sym);
1080        if current.complexity() + sym_weight <= max_complexity
1081            && !exceeds_symbol_limit(config, current, sym)
1082        {
1083            current.push_with_table(sym, &config.symbol_table);
1084            if !generate_recursive_streaming(
1085                config,
1086                target,
1087                eval_context,
1088                current,
1089                stack_depth + 1,
1090                callbacks,
1091            ) {
1092                current.pop_with_table(&config.symbol_table);
1093                return false;
1094            }
1095            current.pop_with_table(&config.symbol_table);
1096        }
1097    }
1098
1099    // Unary operators (Seft::B) - need at least 1 on stack
1100    if stack_depth >= 1 {
1101        for &sym in &config.unary_ops {
1102            let sym_weight = config.symbol_table.weight(sym);
1103            if current.complexity() + sym_weight > max_complexity {
1104                continue;
1105            }
1106            if exceeds_symbol_limit(config, current, sym) {
1107                continue;
1108            }
1109
1110            // Apply pruning rules
1111            if should_prune_unary(current, sym) {
1112                continue;
1113            }
1114
1115            current.push_with_table(sym, &config.symbol_table);
1116            if !generate_recursive_streaming(
1117                config,
1118                target,
1119                eval_context,
1120                current,
1121                stack_depth,
1122                callbacks,
1123            ) {
1124                current.pop_with_table(&config.symbol_table);
1125                return false;
1126            }
1127            current.pop_with_table(&config.symbol_table);
1128        }
1129    }
1130
1131    // Binary operators (Seft::C) - need at least 2 on stack
1132    if stack_depth >= 2 {
1133        for &sym in &config.binary_ops {
1134            let sym_weight = config.symbol_table.weight(sym);
1135            if current.complexity() + sym_weight > max_complexity {
1136                continue;
1137            }
1138            if exceeds_symbol_limit(config, current, sym) {
1139                continue;
1140            }
1141
1142            // Apply pruning rules
1143            if should_prune_binary(current, sym) {
1144                continue;
1145            }
1146
1147            current.push_with_table(sym, &config.symbol_table);
1148            if !generate_recursive_streaming(
1149                config,
1150                target,
1151                eval_context,
1152                current,
1153                stack_depth - 1,
1154                callbacks,
1155            ) {
1156                current.pop_with_table(&config.symbol_table);
1157                return false;
1158            }
1159            current.pop_with_table(&config.symbol_table);
1160        }
1161    }
1162
1163    true
1164}
1165
1166/// Recursively generate expressions
1167fn generate_recursive(
1168    config: &GenConfig,
1169    target: f64,
1170    eval_context: EvalContext<'_>,
1171    current: &mut Expression,
1172    stack_depth: usize,
1173    lhs_out: &mut Vec<EvaluatedExpr>,
1174    rhs_out: &mut Vec<EvaluatedExpr>,
1175) {
1176    // Check if we have a complete expression
1177    if stack_depth == 1 && !current.is_empty() {
1178        // Try to evaluate it with user constants and functions support
1179        match evaluate_fast_with_context(current, target, &eval_context) {
1180            Ok(result) => {
1181                // Use shared validation helper
1182                if should_include_expression(
1183                    &result,
1184                    config,
1185                    current.complexity(),
1186                    current.contains_x(),
1187                ) {
1188                    let expr = current.clone();
1189                    let eval_expr =
1190                        EvaluatedExpr::new(expr, result.value, result.derivative, result.num_type);
1191
1192                    // Keep all LHS expressions; derivative≈0 cases handled in search
1193                    if current.contains_x() {
1194                        lhs_out.push(eval_expr);
1195                    } else {
1196                        rhs_out.push(eval_expr);
1197                    }
1198                }
1199            }
1200            Err(e) => {
1201                // Expression was pruned due to arithmetic error
1202                if config.show_pruned_arith {
1203                    eprintln!(
1204                        "  [pruned arith] expression=\"{}\" reason={:?}",
1205                        current.to_postfix(),
1206                        e
1207                    );
1208                }
1209            }
1210        }
1211    }
1212
1213    // Check limits before recursing
1214    if current.len() >= config.max_length {
1215        return;
1216    }
1217
1218    // Use shared helper for complexity limit calculation
1219    let max_complexity = get_max_complexity(config, current.contains_x());
1220
1221    if current.complexity() >= max_complexity {
1222        return;
1223    }
1224
1225    // Calculate minimum additional complexity needed to complete expression
1226    let min_remaining = min_complexity_to_complete(stack_depth, config);
1227    if current.complexity() + min_remaining > max_complexity {
1228        return;
1229    }
1230
1231    // Try adding each possible symbol
1232
1233    // Constants (Seft::A) - always increase stack by 1
1234    for &sym in &config.constants {
1235        let sym_weight = config.symbol_table.weight(sym);
1236        if current.complexity() + sym_weight > max_complexity {
1237            continue;
1238        }
1239        if exceeds_symbol_limit(config, current, sym) {
1240            continue;
1241        }
1242
1243        // Skip x if we only want RHS
1244        if sym == Symbol::X && !config.generate_lhs {
1245            continue;
1246        }
1247
1248        current.push_with_table(sym, &config.symbol_table);
1249        generate_recursive(
1250            config,
1251            target,
1252            eval_context,
1253            current,
1254            stack_depth + 1,
1255            lhs_out,
1256            rhs_out,
1257        );
1258        current.pop_with_table(&config.symbol_table);
1259    }
1260
1261    // Also add x for LHS generation
1262    if config.generate_lhs && !config.constants.contains(&Symbol::X) {
1263        let sym = Symbol::X;
1264        let sym_weight = config.symbol_table.weight(sym);
1265        if current.complexity() + sym_weight <= max_complexity
1266            && !exceeds_symbol_limit(config, current, sym)
1267        {
1268            current.push_with_table(sym, &config.symbol_table);
1269            generate_recursive(
1270                config,
1271                target,
1272                eval_context,
1273                current,
1274                stack_depth + 1,
1275                lhs_out,
1276                rhs_out,
1277            );
1278            current.pop_with_table(&config.symbol_table);
1279        }
1280    }
1281
1282    // Unary operators (Seft::B) - need at least 1 on stack
1283    if stack_depth >= 1 {
1284        for &sym in &config.unary_ops {
1285            let sym_weight = config.symbol_table.weight(sym);
1286            if current.complexity() + sym_weight > max_complexity {
1287                continue;
1288            }
1289            if exceeds_symbol_limit(config, current, sym) {
1290                continue;
1291            }
1292
1293            // Apply pruning rules
1294            if should_prune_unary(current, sym) {
1295                continue;
1296            }
1297
1298            current.push_with_table(sym, &config.symbol_table);
1299            generate_recursive(
1300                config,
1301                target,
1302                eval_context,
1303                current,
1304                stack_depth,
1305                lhs_out,
1306                rhs_out,
1307            );
1308            current.pop_with_table(&config.symbol_table);
1309        }
1310    }
1311
1312    // Binary operators (Seft::C) - need at least 2 on stack
1313    if stack_depth >= 2 {
1314        for &sym in &config.binary_ops {
1315            let sym_weight = config.symbol_table.weight(sym);
1316            if current.complexity() + sym_weight > max_complexity {
1317                continue;
1318            }
1319            if exceeds_symbol_limit(config, current, sym) {
1320                continue;
1321            }
1322
1323            // Apply pruning rules
1324            if should_prune_binary(current, sym) {
1325                continue;
1326            }
1327
1328            current.push_with_table(sym, &config.symbol_table);
1329            generate_recursive(
1330                config,
1331                target,
1332                eval_context,
1333                current,
1334                stack_depth - 1,
1335                lhs_out,
1336                rhs_out,
1337            );
1338            current.pop_with_table(&config.symbol_table);
1339        }
1340    }
1341}
1342
1343/// Calculate minimum complexity needed to reduce stack to depth 1
1344fn min_complexity_to_complete(stack_depth: usize, config: &GenConfig) -> u32 {
1345    if stack_depth <= 1 {
1346        return 0;
1347    }
1348
1349    // Need (stack_depth - 1) binary operators to reduce to 1
1350    let min_binary_weight = config
1351        .binary_ops
1352        .iter()
1353        .map(|s| config.symbol_table.weight(*s))
1354        .min()
1355        .unwrap_or(4);
1356
1357    ((stack_depth - 1) as u32) * min_binary_weight
1358}
1359
1360/// Pruning rules for unary operators to avoid redundant expressions
1361fn should_prune_unary(expr: &Expression, sym: Symbol) -> bool {
1362    let symbols = expr.symbols();
1363    if symbols.is_empty() {
1364        return false;
1365    }
1366
1367    let last = symbols[symbols.len() - 1];
1368
1369    use Symbol::*;
1370
1371    match (last, sym) {
1372        // Double negation: --a = a
1373        (Neg, Neg) => true,
1374        // Double reciprocal: 1/(1/a) = a
1375        (Recip, Recip) => true,
1376        // sqrt(a^2) = |a| (we don't handle absolute value)
1377        (Square, Sqrt) => true,
1378        // (sqrt(a))^2 = a
1379        (Sqrt, Square) => true,
1380        // ln(e^a) = a
1381        (Exp, Ln) => true,
1382        // e^(ln(a)) = a
1383        (Ln, Exp) => true,
1384
1385        // Additional pruning rules for cleaner output:
1386        // 1/sqrt(a) and 1/a^2 are rare, prefer a^-0.5 or a^-2 notation
1387        (Sqrt, Recip) => true,
1388        (Square, Recip) => true,
1389        // 1/ln(a) is rarely useful
1390        (Ln, Recip) => true,
1391        // Double square: (a^2)^2 = a^4, use power directly
1392        (Square, Square) => true,
1393        // Double sqrt: sqrt(sqrt(a)) = a^0.25, use power directly
1394        (Sqrt, Sqrt) => true,
1395        // Negation after subtraction is redundant with addition
1396        // e.g., -(a-b) = b-a which we could express directly
1397        (Sub, Neg) => true,
1398
1399        // ===== ENHANCED PRUNING RULES =====
1400        // Trig reduction: asin(sin(pi*x)/pi) = x, similar for acos
1401        // These are rarely useful and add many redundant expressions
1402        (SinPi, SinPi) => true,
1403        (CosPi, CosPi) => true,
1404        // asin after sinpi is identity (mod periodicity)
1405        // acos after cospi is identity (mod periodicity)
1406        // These patterns are captured by double application above
1407
1408        // Exp grows too fast - double exp is almost never useful
1409        (Exp, Exp) => true,
1410
1411        // LambertW after exp: W(e^a) = a, so W(e^x) = x
1412        (Exp, LambertW) => true,
1413
1414        // LambertW on small values often doesn't converge usefully
1415        // W of reciprocal is rarely needed
1416        (Recip, LambertW) => true,
1417
1418        _ => false,
1419    }
1420}
1421
1422/// Pruning rules for binary operators
1423fn should_prune_binary(expr: &Expression, sym: Symbol) -> bool {
1424    let symbols = expr.symbols();
1425    if symbols.len() < 2 {
1426        return false;
1427    }
1428
1429    let last = symbols[symbols.len() - 1];
1430    let prev = symbols[symbols.len() - 2];
1431
1432    use Symbol::*;
1433
1434    match sym {
1435        // a - a = 0 (if both operands are identical)
1436        Sub if is_same_subexpr(symbols, 2) => true,
1437        // x - x = 0 (trivial - always 0)
1438        Sub if last == X && prev == X => true,
1439
1440        // a / a = 1 (degenerate if a contains x)
1441        Div if is_same_subexpr(symbols, 2) => true,
1442        // x / x = 1 (trivial identity)
1443        Div if last == X && prev == X => true,
1444        // Division by 1: a/1 = a (useless)
1445        Div if last == One => true,
1446
1447        // Prefer a*2 over a+a
1448        Add if is_same_subexpr(symbols, 2) => true,
1449        // x + (-x) = 0 - check for negated x
1450        Add if last == Neg
1451            && symbols.len() >= 3
1452            && symbols[symbols.len() - 2] == X
1453            && prev == X =>
1454        {
1455            true
1456        }
1457
1458        // 1^b = 1 (degenerate - always equals 1 regardless of b)
1459        // This catches 1^x, 1^(anything)
1460        Pow if prev == One => true,
1461        // a^1 = a (useless)
1462        Pow if last == One => true,
1463
1464        // x * 1 = x, 1 * x = x
1465        Mul if last == One || prev == One => true,
1466
1467        // a"/1 = a^(1/1) = a (1st root is identity)
1468        // But more importantly: 1"/x = 1^(1/x) = 1 (degenerate)
1469        Root if prev == One => true,
1470        // x"/1 means 1^(1/x) = 1 (degenerate)
1471        Root if last == One => true,
1472        // 2nd root is just sqrt, prefer using sqrt
1473        Root if last == Two => true,
1474
1475        // log_x(x) = 1 (trivial identity)
1476        Log if last == X && prev == X => true,
1477        // log_1(anything) is undefined/infinite, log_a(1) = 0
1478        Log if prev == One || last == One => true,
1479        // log_e(a) = ln(a) - prefer ln notation
1480        Log if prev == E => true,
1481
1482        // Ordering: prefer 2+3 over 3+2 for commutative ops
1483        Add | Mul if prev > last && is_constant(last) && is_constant(prev) => true,
1484
1485        _ => false,
1486    }
1487}
1488
1489/// Check whether the top two stack subexpressions are identical.
1490///
1491/// This is only ever used with `n == 2` (the generator compares the two
1492/// operands feeding a binary operator). The boundary of a complete postfix
1493/// subexpression is found with a single backward scan that tracks how many
1494/// operands still need to be supplied to the left: atoms supply one
1495/// (`-1`), unary ops are neutral (`0`), binary ops demand one more (`+1`).
1496/// When the running balance reaches zero, the subexpression starts there.
1497///
1498/// This avoids the prefix-depth table the previous implementation built on
1499/// every call — a hot path during generation, since it runs for every
1500/// commutative/cancelling binary candidate.
1501fn is_same_subexpr(symbols: &[Symbol], n: usize) -> bool {
1502    // Each subexpression is at least one symbol, so two of them need >= 2
1503    // symbols. The original guard also rejected anything below `n * 2`, which
1504    // for the only supported case (`n == 2`) means a length of at least 4;
1505    // preserve that exactly so single-atom operands stay handled by the
1506    // dedicated `last == X && prev == X` arms in `should_prune_binary`.
1507    if n != 2 || symbols.len() < n * 2 {
1508        return false;
1509    }
1510
1511    /// Walk left from `end - 1` until one complete subexpression has been
1512    /// consumed, returning its start index (or `None` on underflow).
1513    fn subexpr_start(symbols: &[Symbol], end: usize) -> Option<usize> {
1514        let mut needed: i32 = 1;
1515        for i in (0..end).rev() {
1516            needed += match symbols[i].seft() {
1517                Seft::A => -1,
1518                Seft::B => 0,
1519                Seft::C => 1,
1520            };
1521            if needed == 0 {
1522                return Some(i);
1523            }
1524        }
1525        None
1526    }
1527
1528    let end = symbols.len();
1529    let Some(top_start) = subexpr_start(symbols, end) else {
1530        return false;
1531    };
1532    let Some(prev_start) = subexpr_start(symbols, top_start) else {
1533        return false;
1534    };
1535
1536    // Slice equality already compares lengths, so unequal-length operands
1537    // short-circuit to `false`.
1538    symbols[prev_start..top_start] == symbols[top_start..end]
1539}
1540
1541/// Check if a symbol is a constant (no x)
1542fn is_constant(sym: Symbol) -> bool {
1543    matches!(sym.seft(), Seft::A) && sym != Symbol::X
1544}
1545
1546/// Generate expressions in parallel using Rayon
1547#[cfg(feature = "parallel")]
1548pub fn generate_all_parallel(config: &GenConfig, target: f64) -> GeneratedExprs {
1549    generate_all_parallel_with_context(
1550        config,
1551        target,
1552        &EvalContext::from_slices(&config.user_constants, &config.user_functions),
1553    )
1554}
1555
1556/// Generate expressions in parallel using Rayon with an explicit evaluation context.
1557#[cfg(feature = "parallel")]
1558pub fn generate_all_parallel_with_context(
1559    config: &GenConfig,
1560    target: f64,
1561    eval_context: &EvalContext<'_>,
1562) -> GeneratedExprs {
1563    use rayon::prelude::*;
1564
1565    // Parallel path currently assumes shared LHS/RHS symbol sets.
1566    if has_rhs_symbol_overrides(config) {
1567        return generate_all_with_context(config, target, eval_context);
1568    }
1569
1570    // Generate valid prefixes of length 1 and 2 to create smaller,
1571    // more evenly distributed tasks for Rayon to schedule.
1572    let mut prefixes: Vec<(Expression, usize)> = Vec::new();
1573    let mut immediate_results_lhs = Vec::new();
1574    let mut immediate_results_rhs = Vec::new();
1575
1576    let first_symbols: Vec<Symbol> = config
1577        .constants
1578        .iter()
1579        .copied()
1580        .chain(
1581            if config.generate_lhs && !config.constants.contains(&Symbol::X) {
1582                Some(Symbol::X)
1583            } else {
1584                None
1585            },
1586        )
1587        .filter(|&sym| {
1588            config
1589                .symbol_max_counts
1590                .get(&sym)
1591                .is_none_or(|&max| max > 0)
1592        })
1593        .collect();
1594
1595    for sym1 in first_symbols {
1596        let mut expr1 = Expression::new();
1597        expr1.push_with_table(sym1, &config.symbol_table);
1598
1599        let max_complexity = if expr1.contains_x() {
1600            config.max_lhs_complexity
1601        } else {
1602            std::cmp::max(config.max_lhs_complexity, config.max_rhs_complexity)
1603        };
1604
1605        if expr1.complexity() > max_complexity {
1606            continue;
1607        }
1608
1609        // 1. Evaluate length-1 prefix (simulate top of generate_recursive)
1610        if let Ok(result) = evaluate_fast_with_context(&expr1, target, eval_context) {
1611            if result.value.is_finite()
1612                && result.value.abs() <= MAX_GENERATED_VALUE
1613                && result.num_type >= config.min_num_type
1614            {
1615                let eval_expr = EvaluatedExpr::new(
1616                    expr1.clone(),
1617                    result.value,
1618                    result.derivative,
1619                    result.num_type,
1620                );
1621
1622                if expr1.contains_x() {
1623                    if config.generate_lhs && expr1.complexity() <= config.max_lhs_complexity {
1624                        immediate_results_lhs.push(eval_expr);
1625                    }
1626                } else if config.generate_rhs && expr1.complexity() <= config.max_rhs_complexity {
1627                    immediate_results_rhs.push(eval_expr);
1628                }
1629            }
1630        }
1631
1632        if expr1.len() >= config.max_length {
1633            continue;
1634        }
1635
1636        // 2. Add next symbols (simulate bottom of generate_recursive)
1637
1638        // Constants (+1 stack)
1639        let mut next_constants = config.constants.clone();
1640        if config.generate_lhs && !next_constants.contains(&Symbol::X) {
1641            next_constants.push(Symbol::X);
1642        }
1643
1644        for &sym2 in &next_constants {
1645            let sym2_weight = config.symbol_table.weight(sym2);
1646            let next_max = if expr1.contains_x() || sym2 == Symbol::X {
1647                config.max_lhs_complexity
1648            } else {
1649                std::cmp::max(config.max_lhs_complexity, config.max_rhs_complexity)
1650            };
1651
1652            if expr1.complexity() + sym2_weight <= next_max
1653                && !exceeds_symbol_limit(config, &expr1, sym2)
1654            {
1655                let mut expr2 = expr1.clone();
1656                expr2.push_with_table(sym2, &config.symbol_table);
1657                // Min complexity check: for stack depth 2, we need at least 1 binary op
1658                let min_remaining = min_complexity_to_complete(2, config);
1659                if expr2.complexity() + min_remaining <= next_max {
1660                    prefixes.push((expr2, 2));
1661                }
1662            }
1663        }
1664
1665        // Unary ops (+0 stack)
1666        for &sym2 in &config.unary_ops {
1667            let sym2_weight = config.symbol_table.weight(sym2);
1668            if expr1.complexity() + sym2_weight <= max_complexity
1669                && !exceeds_symbol_limit(config, &expr1, sym2)
1670                && !should_prune_unary(&expr1, sym2)
1671            {
1672                let mut expr2 = expr1.clone();
1673                expr2.push_with_table(sym2, &config.symbol_table);
1674                let min_remaining = min_complexity_to_complete(1, config);
1675                if expr2.complexity() + min_remaining <= max_complexity {
1676                    prefixes.push((expr2, 1));
1677                }
1678            }
1679        }
1680    }
1681
1682    let results: Vec<(Vec<EvaluatedExpr>, Vec<EvaluatedExpr>)> = prefixes
1683        .into_par_iter()
1684        .map(|(mut expr, depth)| {
1685            let mut lhs = Vec::new();
1686            let mut rhs = Vec::new();
1687            generate_recursive(
1688                config,
1689                target,
1690                *eval_context,
1691                &mut expr,
1692                depth,
1693                &mut lhs,
1694                &mut rhs,
1695            );
1696            (lhs, rhs)
1697        })
1698        .collect();
1699
1700    // Merge results
1701    let mut lhs_raw = immediate_results_lhs;
1702    let mut rhs_raw = immediate_results_rhs;
1703    for (lhs, rhs) in results {
1704        lhs_raw.extend(lhs);
1705        rhs_raw.extend(rhs);
1706    }
1707
1708    // Deduplicate RHS by value, keeping simplest expression for each value
1709    GeneratedExprs {
1710        lhs: dedupe_lhs_expressions(lhs_raw),
1711        rhs: dedupe_rhs_expressions(rhs_raw),
1712    }
1713}
1714
1715#[cfg(test)]
1716mod tests {
1717    use super::*;
1718    #[cfg(not(target_arch = "wasm32"))]
1719    use proptest::prelude::*;
1720
1721    /// Create a fast test config with limited complexity and operators
1722    fn fast_test_config() -> GenConfig {
1723        GenConfig {
1724            max_lhs_complexity: 20,
1725            max_rhs_complexity: 20,
1726            max_length: 8,
1727            constants: vec![
1728                Symbol::One,
1729                Symbol::Two,
1730                Symbol::Three,
1731                Symbol::Four,
1732                Symbol::Five,
1733                Symbol::Pi,
1734                Symbol::E,
1735            ],
1736            unary_ops: vec![Symbol::Neg, Symbol::Recip, Symbol::Square, Symbol::Sqrt],
1737            binary_ops: vec![Symbol::Add, Symbol::Sub, Symbol::Mul, Symbol::Div],
1738            rhs_constants: None,
1739            rhs_unary_ops: None,
1740            rhs_binary_ops: None,
1741            symbol_max_counts: HashMap::new(),
1742            rhs_symbol_max_counts: None,
1743            min_num_type: NumType::Transcendental,
1744            generate_lhs: true,
1745            generate_rhs: true,
1746            user_constants: Vec::new(),
1747            user_functions: Vec::new(),
1748            show_pruned_arith: false,
1749            symbol_table: Arc::new(SymbolTable::new()),
1750        }
1751    }
1752
1753    fn expr_from_postfix(s: &str) -> Expression {
1754        Expression::parse(s).expect("valid expression")
1755    }
1756
1757    fn stub_eval_expr(postfix: &str, value: f64, derivative: f64) -> EvaluatedExpr {
1758        EvaluatedExpr {
1759            expr: expr_from_postfix(postfix),
1760            value,
1761            derivative,
1762            num_type: NumType::Transcendental,
1763        }
1764    }
1765
1766    #[test]
1767    fn test_generate_simple() {
1768        let mut config = fast_test_config();
1769        config.generate_lhs = false; // Only RHS for simpler test
1770
1771        let result = generate_all(&config, 1.0);
1772
1773        // Should have some RHS expressions
1774        assert!(!result.rhs.is_empty());
1775
1776        // All should be valid (evaluate without error)
1777        for expr in &result.rhs {
1778            assert!(!expr.expr.contains_x());
1779        }
1780    }
1781
1782    #[test]
1783    fn test_generate_lhs() {
1784        let mut config = fast_test_config();
1785        config.generate_rhs = false;
1786
1787        let result = generate_all(&config, 2.0);
1788
1789        // Should have LHS expressions containing x
1790        assert!(!result.lhs.is_empty());
1791        for expr in &result.lhs {
1792            assert!(expr.expr.contains_x());
1793        }
1794    }
1795
1796    #[test]
1797    fn test_complexity_limit() {
1798        let config = fast_test_config();
1799
1800        let result = generate_all(&config, 1.0);
1801
1802        for expr in &result.rhs {
1803            assert!(expr.expr.complexity() <= config.max_rhs_complexity);
1804        }
1805        for expr in &result.lhs {
1806            assert!(expr.expr.complexity() <= config.max_lhs_complexity);
1807        }
1808    }
1809
1810    #[test]
1811    fn test_generate_all_with_limit_aborts_when_exceeded() {
1812        // Config with high complexity that will generate many expressions.
1813        // With new calibrated weights, even moderate complexity can generate 100+ expressions.
1814        let mut config = fast_test_config();
1815        config.max_lhs_complexity = 30;
1816        config.max_rhs_complexity = 30;
1817
1818        // First, check how many expressions would be generated without limit.
1819        let unlimited = generate_all(&config, 2.5);
1820        let total_unlimited = unlimited.lhs.len() + unlimited.rhs.len();
1821
1822        // The test only makes sense if we'd generate more than a handful.
1823        assert!(
1824            total_unlimited > 10,
1825            "Test config should generate >10 expressions"
1826        );
1827
1828        // Now test with a limit less than the actual count — should return None.
1829        let limit = total_unlimited / 2; // Set limit to half of what would be generated
1830        let result = generate_all_with_limit(&config, 2.5, limit);
1831
1832        assert!(
1833            result.is_none(),
1834            "generate_all_with_limit should return None when limit ({}) is exceeded (actual: {})",
1835            limit,
1836            total_unlimited
1837        );
1838    }
1839
1840    #[test]
1841    fn test_generate_all_with_limit_succeeds_when_within_limit() {
1842        // Same config but with a generous limit that won't be hit.
1843        let mut config = fast_test_config();
1844        config.max_lhs_complexity = 30;
1845        config.max_rhs_complexity = 30;
1846
1847        // Set limit much higher than expected expression count.
1848        let result = generate_all_with_limit(&config, 2.5, 10_000);
1849
1850        assert!(
1851            result.is_some(),
1852            "generate_all_with_limit should return Some when limit is not exceeded"
1853        );
1854
1855        let generated = result.unwrap();
1856        // Should have generated some expressions.
1857        assert!(!generated.lhs.is_empty() || !generated.rhs.is_empty());
1858    }
1859
1860    #[test]
1861    fn test_count_expressions_with_limit_matches_generated_total() {
1862        let mut config = fast_test_config();
1863        config.max_lhs_complexity = 30;
1864        config.max_rhs_complexity = 30;
1865
1866        let context = EvalContext::from_slices(&config.user_constants, &config.user_functions);
1867        let total_generated = std::cell::Cell::new(0usize);
1868        let mut callbacks = StreamingCallbacks {
1869            on_lhs: &mut |_expr| {
1870                total_generated.set(total_generated.get() + 1);
1871                true
1872            },
1873            on_rhs: &mut |_expr| {
1874                total_generated.set(total_generated.get() + 1);
1875                true
1876            },
1877        };
1878        generate_streaming_with_context(&config, 2.5, &context, &mut callbacks);
1879        let counted = count_expressions_with_limit_and_context(
1880            &config,
1881            2.5,
1882            &context,
1883            total_generated.get() + 1,
1884        );
1885
1886        assert_eq!(counted, Some(total_generated.get()));
1887    }
1888
1889    #[test]
1890    fn test_count_expressions_with_limit_returns_none_when_exceeded() {
1891        let mut config = fast_test_config();
1892        config.max_lhs_complexity = 30;
1893        config.max_rhs_complexity = 30;
1894
1895        let context = EvalContext::from_slices(&config.user_constants, &config.user_functions);
1896        let total_generated = std::cell::Cell::new(0usize);
1897        let mut callbacks = StreamingCallbacks {
1898            on_lhs: &mut |_expr| {
1899                total_generated.set(total_generated.get() + 1);
1900                true
1901            },
1902            on_rhs: &mut |_expr| {
1903                total_generated.set(total_generated.get() + 1);
1904                true
1905            },
1906        };
1907        generate_streaming_with_context(&config, 2.5, &context, &mut callbacks);
1908        let counted = count_expressions_with_limit_and_context(
1909            &config,
1910            2.5,
1911            &context,
1912            total_generated.get() / 2,
1913        );
1914
1915        assert_eq!(counted, None);
1916    }
1917
1918    // ==================== expression_respects_constraints tests ====================
1919
1920    #[test]
1921    fn test_is_same_subexpr_detects_identical_operands() {
1922        use crate::symbol::Symbol::*;
1923
1924        // 2 3 + | 2 3 + : the two operands of a pending binary op are equal.
1925        let same = [Two, Three, Add, Two, Three, Add];
1926        assert!(is_same_subexpr(&same, 2));
1927
1928        // 2 3 + | 2 4 + : operands differ in the last atom.
1929        let diff = [Two, Three, Add, Two, Four, Add];
1930        assert!(!is_same_subexpr(&diff, 2));
1931
1932        // Unequal-length operands: 2 3 + | 5 .
1933        let unequal = [Two, Three, Add, Five];
1934        assert!(!is_same_subexpr(&unequal, 2));
1935
1936        // Nested unary inside identical operands: 2 q n | 2 q n (sqrt then neg).
1937        let nested = [Two, Sqrt, Neg, Two, Sqrt, Neg];
1938        assert!(is_same_subexpr(&nested, 2));
1939
1940        // Single-atom operands stay out of scope (handled by dedicated arms).
1941        let short = [Two, Two];
1942        assert!(!is_same_subexpr(&short, 2));
1943
1944        // Only n == 2 is supported.
1945        let triple = [Two, Three, Add, Two, Three, Add, Two, Three, Add];
1946        assert!(!is_same_subexpr(&triple, 3));
1947    }
1948
1949    #[test]
1950    fn test_quantize_value_signed_boundary_symmetry_around_zero() {
1951        let below = 0.49 / QUANTIZE_SCALE;
1952        let above = 0.51 / QUANTIZE_SCALE;
1953
1954        assert_eq!(quantize_value(below), 0);
1955        assert_eq!(quantize_value(-below), 0);
1956        assert_eq!(quantize_value(above), 1);
1957        assert_eq!(quantize_value(-above), -1);
1958    }
1959
1960    #[test]
1961    fn test_dedupe_rhs_keeps_simplest_expression_within_quantized_bucket() {
1962        let base = 2.0;
1963        let rhs = dedupe_rhs_expressions(vec![
1964            stub_eval_expr("11+", base + 0.49 / QUANTIZE_SCALE, 0.0),
1965            stub_eval_expr("2", base, 0.0),
1966        ]);
1967
1968        assert_eq!(rhs.len(), 1);
1969        assert_eq!(rhs[0].expr.to_postfix(), "2");
1970    }
1971
1972    #[test]
1973    fn test_dedupe_lhs_preserves_adjacent_derivative_buckets() {
1974        let lhs = dedupe_lhs_expressions(vec![
1975            stub_eval_expr("x", 1.0, 1.0),
1976            stub_eval_expr("x1+", 1.0, 1.0 + 0.51 / QUANTIZE_SCALE),
1977        ]);
1978
1979        assert_eq!(lhs.len(), 2);
1980    }
1981
1982    #[cfg(not(target_arch = "wasm32"))]
1983    proptest! {
1984        #[test]
1985        fn test_quantize_value_collapses_values_within_same_bucket(bucket in -1_000_000i64..1_000_000i64) {
1986            let base = bucket as f64 / QUANTIZE_SCALE;
1987            prop_assert_eq!(quantize_value(base + 0.49 / QUANTIZE_SCALE), bucket);
1988            prop_assert_eq!(quantize_value(base - 0.49 / QUANTIZE_SCALE), bucket);
1989        }
1990
1991        #[test]
1992        fn test_quantize_value_separates_adjacent_buckets(bucket in -1_000_000i64..1_000_000i64) {
1993            let base = bucket as f64 / QUANTIZE_SCALE;
1994            prop_assert_eq!(quantize_value(base + 0.51 / QUANTIZE_SCALE), bucket + 1);
1995            prop_assert_eq!(quantize_value(base - 0.51 / QUANTIZE_SCALE), bucket - 1);
1996        }
1997    }
1998
1999    #[test]
2000    fn test_constraints_default_allows_all() {
2001        let opts = ExpressionConstraintOptions::default();
2002
2003        // x^pi should be allowed with default options
2004        let expr = expr_from_postfix("xp^"); // x^pi
2005        assert!(
2006            expression_respects_constraints(&expr, opts),
2007            "x^pi should be allowed with default options"
2008        );
2009
2010        // sinpi(e) should be allowed
2011        let expr = expr_from_postfix("eS"); // e then sinpi (S = SinPi)
2012        assert!(
2013            expression_respects_constraints(&expr, opts),
2014            "sinpi(e) should be allowed with default options"
2015        );
2016    }
2017
2018    #[test]
2019    fn test_constraints_rational_exponents_rejects_transcendental() {
2020        let opts = ExpressionConstraintOptions {
2021            rational_exponents: true,
2022            ..Default::default()
2023        };
2024
2025        // x^pi should be rejected (pi is transcendental)
2026        let expr = expr_from_postfix("xp^");
2027        assert!(
2028            !expression_respects_constraints(&expr, opts),
2029            "x^pi should be rejected with rational_exponents=true"
2030        );
2031
2032        // x^e should be rejected
2033        let expr = expr_from_postfix("xe^");
2034        assert!(
2035            !expression_respects_constraints(&expr, opts),
2036            "x^e should be rejected with rational_exponents=true"
2037        );
2038    }
2039
2040    #[test]
2041    fn test_constraints_rational_exponents_allows_integer() {
2042        let opts = ExpressionConstraintOptions {
2043            rational_exponents: true,
2044            ..Default::default()
2045        };
2046
2047        // x^2 should be allowed (2 is integer)
2048        let expr = expr_from_postfix("x2^");
2049        assert!(
2050            expression_respects_constraints(&expr, opts),
2051            "x^2 should be allowed with rational_exponents=true"
2052        );
2053
2054        // x^1 should be allowed
2055        let expr = expr_from_postfix("x1^");
2056        assert!(
2057            expression_respects_constraints(&expr, opts),
2058            "x^1 should be allowed with rational_exponents=true"
2059        );
2060    }
2061
2062    #[test]
2063    fn test_constraints_rational_trig_args_rejects_irrational() {
2064        let opts = ExpressionConstraintOptions {
2065            rational_trig_args: true,
2066            ..Default::default()
2067        };
2068
2069        // sinpi(e) should be rejected (e is irrational/transcendental)
2070        let expr = expr_from_postfix("eS"); // e then sinpi (S = SinPi)
2071        assert!(
2072            !expression_respects_constraints(&expr, opts),
2073            "sinpi(e) should be rejected with rational_trig_args=true"
2074        );
2075
2076        // sinpi(pi) should be rejected (pi is transcendental)
2077        let expr = expr_from_postfix("pS"); // pi then sinpi
2078        assert!(
2079            !expression_respects_constraints(&expr, opts),
2080            "sinpi(pi) should be rejected with rational_trig_args=true"
2081        );
2082    }
2083
2084    #[test]
2085    fn test_constraints_rational_trig_args_allows_rational() {
2086        let opts = ExpressionConstraintOptions {
2087            rational_trig_args: true,
2088            ..Default::default()
2089        };
2090
2091        // sinpi(1) should be allowed (1 is integer, hence rational)
2092        let expr = expr_from_postfix("1S"); // 1 then sinpi (S = SinPi)
2093        assert!(
2094            expression_respects_constraints(&expr, opts),
2095            "sinpi(1) should be allowed with rational_trig_args=true"
2096        );
2097
2098        // sinpi(2) should be allowed
2099        let expr = expr_from_postfix("2S");
2100        assert!(
2101            expression_respects_constraints(&expr, opts),
2102            "sinpi(2) should be allowed with rational_trig_args=true"
2103        );
2104    }
2105
2106    #[test]
2107    fn test_constraints_rational_trig_args_rejects_x() {
2108        let opts = ExpressionConstraintOptions {
2109            rational_trig_args: true,
2110            ..Default::default()
2111        };
2112
2113        // sinpi(x) should be rejected (x is not a constant rational)
2114        let expr = expr_from_postfix("xS"); // x then sinpi (S = SinPi)
2115        assert!(
2116            !expression_respects_constraints(&expr, opts),
2117            "sinpi(x) should be rejected with rational_trig_args=true"
2118        );
2119    }
2120
2121    #[test]
2122    fn test_constraints_max_trig_cycles() {
2123        let opts = ExpressionConstraintOptions {
2124            max_trig_cycles: Some(2),
2125            ..Default::default()
2126        };
2127
2128        // Single trig: sinpi(x) - should pass
2129        let expr = expr_from_postfix("xS"); // x then sinpi (S = SinPi)
2130        assert!(
2131            expression_respects_constraints(&expr, opts),
2132            "1 trig op should pass with max=2"
2133        );
2134
2135        // Double nested: sinpi(cospi(x)) - should pass
2136        // x C S = sinpi(cospi(x)) where C = CosPi, S = SinPi
2137        let expr = expr_from_postfix("xCS");
2138        assert!(
2139            expression_respects_constraints(&expr, opts),
2140            "2 trig ops should pass with max=2"
2141        );
2142
2143        // Triple nested: sinpi(cospi(tanpi(x))) - should fail
2144        // x T C S = sinpi(cospi(tanpi(x))) where T = TanPi
2145        let expr = expr_from_postfix("xTCS");
2146        assert!(
2147            !expression_respects_constraints(&expr, opts),
2148            "3 trig ops should fail with max=2"
2149        );
2150    }
2151
2152    #[test]
2153    fn test_constraints_max_trig_cycles_none_unlimited() {
2154        let opts = ExpressionConstraintOptions {
2155            max_trig_cycles: None, // No limit
2156            ..Default::default()
2157        };
2158
2159        // Even deeply nested trig should pass
2160        // x T C S T C S = 6 trig ops
2161        let expr = expr_from_postfix("xTCSTCS");
2162        assert!(
2163            expression_respects_constraints(&expr, opts),
2164            "Unlimited trig should pass any depth"
2165        );
2166    }
2167
2168    #[test]
2169    fn test_constraints_combined() {
2170        let opts = ExpressionConstraintOptions {
2171            rational_exponents: true,
2172            rational_trig_args: true,
2173            max_trig_cycles: Some(1),
2174            ..Default::default()
2175        };
2176
2177        // x^2 + sinpi(1) should pass
2178        let expr = expr_from_postfix("x2^1S+"); // S = SinPi
2179        assert!(
2180            expression_respects_constraints(&expr, opts),
2181            "x^2 + sinpi(1) should pass all constraints"
2182        );
2183
2184        // x^pi should fail (rational_exponents)
2185        let expr = expr_from_postfix("xp^");
2186        assert!(
2187            !expression_respects_constraints(&expr, opts),
2188            "x^pi should fail rational_exponents"
2189        );
2190
2191        // sinpi(x) should fail (rational_trig_args)
2192        let expr = expr_from_postfix("xS"); // S = SinPi
2193        assert!(
2194            !expression_respects_constraints(&expr, opts),
2195            "sinpi(x) should fail rational_trig_args"
2196        );
2197
2198        // sinpi(cospi(1)) should fail (max_trig_cycles)
2199        let expr = expr_from_postfix("1CS"); // C = CosPi, S = SinPi
2200        assert!(
2201            !expression_respects_constraints(&expr, opts),
2202            "double trig should fail max_trig_cycles=1"
2203        );
2204    }
2205
2206    #[test]
2207    fn test_constraints_malformed_expression() {
2208        let opts = ExpressionConstraintOptions::default();
2209
2210        // Expression that would cause stack underflow
2211        let expr = Expression::from_symbols(&[crate::symbol::Symbol::Add]); // Just a binary op
2212        assert!(
2213            !expression_respects_constraints(&expr, opts),
2214            "Malformed expression should return false"
2215        );
2216
2217        // Incomplete expression (too many values)
2218        let expr =
2219            Expression::from_symbols(&[crate::symbol::Symbol::One, crate::symbol::Symbol::Two]);
2220        assert!(
2221            !expression_respects_constraints(&expr, opts),
2222            "Incomplete expression should return false"
2223        );
2224    }
2225
2226    #[test]
2227    fn test_constraints_user_constant_types() {
2228        // Set user constant 0 to be Integer type
2229        let mut user_types = [NumType::Transcendental; 16];
2230        user_types[0] = NumType::Integer;
2231
2232        let opts = ExpressionConstraintOptions {
2233            rational_exponents: true,
2234            user_constant_types: user_types,
2235            ..Default::default()
2236        };
2237
2238        // If UserConstant0 is treated as Integer, x^UserConstant0 should be allowed
2239        // (We can't easily test this without actually having user constants in the expression,
2240        // but this verifies the options struct is properly configured)
2241        assert_eq!(opts.user_constant_types[0], NumType::Integer);
2242    }
2243}