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 streaming callbacks for memory-efficient processing
721///
722/// This function is the foundation of the streaming architecture. Instead of
723/// accumulating all expressions in memory, it calls the provided callbacks
724/// for each generated expression, allowing immediate processing.
725///
726/// # Memory Efficiency
727///
728/// - Traditional: O(expressions) memory - all expressions stored before processing
729/// - Streaming: O(depth) memory - only the recursion stack is stored
730///
731/// # Early Exit
732///
733/// The callbacks can return `false` to signal early termination. This is useful
734/// when good matches have been found and additional expressions aren't needed.
735///
736/// # Deduplication
737///
738/// The caller is responsible for deduplication if needed. This allows flexibility
739/// in deduplication strategies (e.g., per-batch, per-tier, etc.).
740///
741/// # Example
742///
743/// ```no_run
744/// use ries_rs::gen::{GenConfig, StreamingCallbacks, generate_streaming};
745/// let config = GenConfig::default();
746/// let target = 2.5_f64;
747/// let mut rhs_count = 0;
748/// let mut lhs_count = 0;
749/// let mut callbacks = StreamingCallbacks {
750///     on_rhs: &mut |_expr| {
751///         rhs_count += 1;
752///         true // continue generation
753///     },
754///     on_lhs: &mut |_expr| {
755///         lhs_count += 1;
756///         true // continue generation
757///     },
758/// };
759/// generate_streaming(&config, target, &mut callbacks);
760/// ```
761pub fn generate_streaming(config: &GenConfig, target: f64, callbacks: &mut StreamingCallbacks) {
762    generate_streaming_with_context(
763        config,
764        target,
765        &EvalContext::from_slices(&config.user_constants, &config.user_functions),
766        callbacks,
767    );
768}
769
770/// Generate expressions with streaming callbacks using an explicit evaluation context.
771pub fn generate_streaming_with_context(
772    config: &GenConfig,
773    target: f64,
774    eval_context: &EvalContext<'_>,
775    callbacks: &mut StreamingCallbacks,
776) {
777    if config.generate_lhs && config.generate_rhs && has_rhs_symbol_overrides(config) {
778        let mut lhs_config = config.clone();
779        lhs_config.generate_lhs = true;
780        lhs_config.generate_rhs = false;
781        if !generate_recursive_streaming(
782            &lhs_config,
783            target,
784            *eval_context,
785            &mut Expression::new(),
786            0,
787            callbacks,
788        ) {
789            return;
790        }
791
792        let rhs_config = rhs_only_config(config);
793        generate_recursive_streaming(
794            &rhs_config,
795            target,
796            *eval_context,
797            &mut Expression::new(),
798            0,
799            callbacks,
800        );
801    } else {
802        generate_recursive_streaming(
803            config,
804            target,
805            *eval_context,
806            &mut Expression::new(),
807            0, // current stack depth
808            callbacks,
809        );
810    }
811}
812
813/// Count generated expressions with an early-abort limit using streaming callbacks.
814///
815/// Returns `Some(count)` when generation completes within the limit, or `None`
816/// as soon as the count exceeds `max_expressions`.
817pub fn count_expressions_with_limit_and_context(
818    config: &GenConfig,
819    target: f64,
820    eval_context: &EvalContext<'_>,
821    max_expressions: usize,
822) -> Option<usize> {
823    let count = std::cell::Cell::new(0usize);
824    let mut callbacks = StreamingCallbacks {
825        on_lhs: &mut |_expr| {
826            let next = count.get() + 1;
827            count.set(next);
828            next <= max_expressions
829        },
830        on_rhs: &mut |_expr| {
831            let next = count.get() + 1;
832            count.set(next);
833            next <= max_expressions
834        },
835    };
836
837    generate_streaming_with_context(config, target, eval_context, &mut callbacks);
838
839    if count.get() > max_expressions {
840        None
841    } else {
842        Some(count.get())
843    }
844}
845
846#[inline]
847fn has_rhs_symbol_overrides(config: &GenConfig) -> bool {
848    config.rhs_constants.is_some()
849        || config.rhs_unary_ops.is_some()
850        || config.rhs_binary_ops.is_some()
851        || config.rhs_symbol_max_counts.is_some()
852}
853
854/// Check if an evaluated expression meets generation criteria
855///
856/// This shared helper function is used by both batch and streaming generation
857/// to validate expressions before including them in results.
858#[inline]
859fn should_include_expression(
860    result: &crate::eval::EvalResult,
861    config: &GenConfig,
862    complexity: u32,
863    contains_x: bool,
864) -> bool {
865    result.value.is_finite()
866        && result.value.abs() <= MAX_GENERATED_VALUE
867        && result.num_type >= config.min_num_type
868        && if contains_x {
869            config.generate_lhs && complexity <= config.max_lhs_complexity
870        } else {
871            config.generate_rhs && complexity <= config.max_rhs_complexity
872        }
873}
874
875/// Calculate the appropriate complexity limit based on whether expression contains x
876///
877/// For expressions containing x, uses LHS limit.
878/// For RHS-only paths, uses RHS limit.
879/// For paths that might still add x, uses the max of both limits.
880#[inline]
881fn get_max_complexity(config: &GenConfig, contains_x: bool) -> u32 {
882    if contains_x {
883        config.max_lhs_complexity
884    } else {
885        // For RHS-only paths, use RHS limit
886        // For paths that might still add x, use the max of both
887        std::cmp::max(config.max_lhs_complexity, config.max_rhs_complexity)
888    }
889}
890
891fn rhs_only_config(config: &GenConfig) -> GenConfig {
892    let mut rhs_config = config.clone();
893    rhs_config.generate_lhs = false;
894    rhs_config.generate_rhs = true;
895    if let Some(constants) = &config.rhs_constants {
896        rhs_config.constants = constants.clone();
897    }
898    if let Some(unary_ops) = &config.rhs_unary_ops {
899        rhs_config.unary_ops = unary_ops.clone();
900    }
901    if let Some(binary_ops) = &config.rhs_binary_ops {
902        rhs_config.binary_ops = binary_ops.clone();
903    }
904    if let Some(rhs_symbol_max_counts) = &config.rhs_symbol_max_counts {
905        rhs_config.symbol_max_counts = rhs_symbol_max_counts.clone();
906    }
907    rhs_config
908}
909
910#[inline]
911fn exceeds_symbol_limit(config: &GenConfig, current: &Expression, sym: Symbol) -> bool {
912    config
913        .symbol_max_counts
914        .get(&sym)
915        .is_some_and(|&max| current.count_symbol(sym) >= max)
916}
917
918/// Recursively generate expressions with streaming callbacks
919///
920/// This is the core streaming generation function. It mirrors `generate_recursive`
921/// but calls callbacks instead of accumulating expressions.
922fn generate_recursive_streaming(
923    config: &GenConfig,
924    target: f64,
925    eval_context: EvalContext<'_>,
926    current: &mut Expression,
927    stack_depth: usize,
928    callbacks: &mut StreamingCallbacks,
929) -> bool {
930    // Check if we have a complete expression
931    if stack_depth == 1 && !current.is_empty() {
932        // Try to evaluate it with user constants and functions support
933        match evaluate_fast_with_context(current, target, &eval_context) {
934            Ok(result) => {
935                // Use shared validation helper
936                if should_include_expression(
937                    &result,
938                    config,
939                    current.complexity(),
940                    current.contains_x(),
941                ) {
942                    let expr = current.clone();
943                    let eval_expr =
944                        EvaluatedExpr::new(expr, result.value, result.derivative, result.num_type);
945
946                    // Call the appropriate callback; return false if it signals stop
947                    let should_continue = if current.contains_x() {
948                        (callbacks.on_lhs)(&eval_expr)
949                    } else {
950                        (callbacks.on_rhs)(&eval_expr)
951                    };
952                    if !should_continue {
953                        return false;
954                    }
955                }
956            }
957            Err(e) => {
958                // Expression was pruned due to arithmetic error
959                if config.show_pruned_arith {
960                    eprintln!(
961                        "  [pruned arith] expression=\"{}\" reason={:?}",
962                        current.to_postfix(),
963                        e
964                    );
965                }
966            }
967        }
968    }
969
970    // Check limits before recursing
971    if current.len() >= config.max_length {
972        return true;
973    }
974
975    // Use shared helper for complexity limit calculation
976    let max_complexity = get_max_complexity(config, current.contains_x());
977
978    if current.complexity() >= max_complexity {
979        return true;
980    }
981
982    // Calculate minimum additional complexity needed to complete expression
983    let min_remaining = min_complexity_to_complete(stack_depth, config);
984    if current.complexity() + min_remaining > max_complexity {
985        return true;
986    }
987
988    // Try adding each possible symbol
989
990    // Constants (Seft::A) - always increase stack by 1
991    for &sym in &config.constants {
992        let sym_weight = config.symbol_table.weight(sym);
993        if current.complexity() + sym_weight > max_complexity {
994            continue;
995        }
996        if exceeds_symbol_limit(config, current, sym) {
997            continue;
998        }
999
1000        // Skip x if we only want RHS
1001        if sym == Symbol::X && !config.generate_lhs {
1002            continue;
1003        }
1004
1005        current.push_with_table(sym, &config.symbol_table);
1006        if !generate_recursive_streaming(
1007            config,
1008            target,
1009            eval_context,
1010            current,
1011            stack_depth + 1,
1012            callbacks,
1013        ) {
1014            current.pop_with_table(&config.symbol_table);
1015            return false;
1016        }
1017        current.pop_with_table(&config.symbol_table);
1018    }
1019
1020    // Also add x for LHS generation
1021    if config.generate_lhs && !config.constants.contains(&Symbol::X) {
1022        let sym = Symbol::X;
1023        let sym_weight = config.symbol_table.weight(sym);
1024        if current.complexity() + sym_weight <= max_complexity
1025            && !exceeds_symbol_limit(config, current, sym)
1026        {
1027            current.push_with_table(sym, &config.symbol_table);
1028            if !generate_recursive_streaming(
1029                config,
1030                target,
1031                eval_context,
1032                current,
1033                stack_depth + 1,
1034                callbacks,
1035            ) {
1036                current.pop_with_table(&config.symbol_table);
1037                return false;
1038            }
1039            current.pop_with_table(&config.symbol_table);
1040        }
1041    }
1042
1043    // Unary operators (Seft::B) - need at least 1 on stack
1044    if stack_depth >= 1 {
1045        for &sym in &config.unary_ops {
1046            let sym_weight = config.symbol_table.weight(sym);
1047            if current.complexity() + sym_weight > max_complexity {
1048                continue;
1049            }
1050            if exceeds_symbol_limit(config, current, sym) {
1051                continue;
1052            }
1053
1054            // Apply pruning rules
1055            if should_prune_unary(current, sym) {
1056                continue;
1057            }
1058
1059            current.push_with_table(sym, &config.symbol_table);
1060            if !generate_recursive_streaming(
1061                config,
1062                target,
1063                eval_context,
1064                current,
1065                stack_depth,
1066                callbacks,
1067            ) {
1068                current.pop_with_table(&config.symbol_table);
1069                return false;
1070            }
1071            current.pop_with_table(&config.symbol_table);
1072        }
1073    }
1074
1075    // Binary operators (Seft::C) - need at least 2 on stack
1076    if stack_depth >= 2 {
1077        for &sym in &config.binary_ops {
1078            let sym_weight = config.symbol_table.weight(sym);
1079            if current.complexity() + sym_weight > max_complexity {
1080                continue;
1081            }
1082            if exceeds_symbol_limit(config, current, sym) {
1083                continue;
1084            }
1085
1086            // Apply pruning rules
1087            if should_prune_binary(current, sym) {
1088                continue;
1089            }
1090
1091            current.push_with_table(sym, &config.symbol_table);
1092            if !generate_recursive_streaming(
1093                config,
1094                target,
1095                eval_context,
1096                current,
1097                stack_depth - 1,
1098                callbacks,
1099            ) {
1100                current.pop_with_table(&config.symbol_table);
1101                return false;
1102            }
1103            current.pop_with_table(&config.symbol_table);
1104        }
1105    }
1106
1107    true
1108}
1109
1110/// Recursively generate expressions
1111fn generate_recursive(
1112    config: &GenConfig,
1113    target: f64,
1114    eval_context: EvalContext<'_>,
1115    current: &mut Expression,
1116    stack_depth: usize,
1117    lhs_out: &mut Vec<EvaluatedExpr>,
1118    rhs_out: &mut Vec<EvaluatedExpr>,
1119) {
1120    // Check if we have a complete expression
1121    if stack_depth == 1 && !current.is_empty() {
1122        // Try to evaluate it with user constants and functions support
1123        match evaluate_fast_with_context(current, target, &eval_context) {
1124            Ok(result) => {
1125                // Use shared validation helper
1126                if should_include_expression(
1127                    &result,
1128                    config,
1129                    current.complexity(),
1130                    current.contains_x(),
1131                ) {
1132                    let expr = current.clone();
1133                    let eval_expr =
1134                        EvaluatedExpr::new(expr, result.value, result.derivative, result.num_type);
1135
1136                    // Keep all LHS expressions; derivative≈0 cases handled in search
1137                    if current.contains_x() {
1138                        lhs_out.push(eval_expr);
1139                    } else {
1140                        rhs_out.push(eval_expr);
1141                    }
1142                }
1143            }
1144            Err(e) => {
1145                // Expression was pruned due to arithmetic error
1146                if config.show_pruned_arith {
1147                    eprintln!(
1148                        "  [pruned arith] expression=\"{}\" reason={:?}",
1149                        current.to_postfix(),
1150                        e
1151                    );
1152                }
1153            }
1154        }
1155    }
1156
1157    // Check limits before recursing
1158    if current.len() >= config.max_length {
1159        return;
1160    }
1161
1162    // Use shared helper for complexity limit calculation
1163    let max_complexity = get_max_complexity(config, current.contains_x());
1164
1165    if current.complexity() >= max_complexity {
1166        return;
1167    }
1168
1169    // Calculate minimum additional complexity needed to complete expression
1170    let min_remaining = min_complexity_to_complete(stack_depth, config);
1171    if current.complexity() + min_remaining > max_complexity {
1172        return;
1173    }
1174
1175    // Try adding each possible symbol
1176
1177    // Constants (Seft::A) - always increase stack by 1
1178    for &sym in &config.constants {
1179        let sym_weight = config.symbol_table.weight(sym);
1180        if current.complexity() + sym_weight > max_complexity {
1181            continue;
1182        }
1183        if exceeds_symbol_limit(config, current, sym) {
1184            continue;
1185        }
1186
1187        // Skip x if we only want RHS
1188        if sym == Symbol::X && !config.generate_lhs {
1189            continue;
1190        }
1191
1192        current.push_with_table(sym, &config.symbol_table);
1193        generate_recursive(
1194            config,
1195            target,
1196            eval_context,
1197            current,
1198            stack_depth + 1,
1199            lhs_out,
1200            rhs_out,
1201        );
1202        current.pop_with_table(&config.symbol_table);
1203    }
1204
1205    // Also add x for LHS generation
1206    if config.generate_lhs && !config.constants.contains(&Symbol::X) {
1207        let sym = Symbol::X;
1208        let sym_weight = config.symbol_table.weight(sym);
1209        if current.complexity() + sym_weight <= max_complexity
1210            && !exceeds_symbol_limit(config, current, sym)
1211        {
1212            current.push_with_table(sym, &config.symbol_table);
1213            generate_recursive(
1214                config,
1215                target,
1216                eval_context,
1217                current,
1218                stack_depth + 1,
1219                lhs_out,
1220                rhs_out,
1221            );
1222            current.pop_with_table(&config.symbol_table);
1223        }
1224    }
1225
1226    // Unary operators (Seft::B) - need at least 1 on stack
1227    if stack_depth >= 1 {
1228        for &sym in &config.unary_ops {
1229            let sym_weight = config.symbol_table.weight(sym);
1230            if current.complexity() + sym_weight > max_complexity {
1231                continue;
1232            }
1233            if exceeds_symbol_limit(config, current, sym) {
1234                continue;
1235            }
1236
1237            // Apply pruning rules
1238            if should_prune_unary(current, sym) {
1239                continue;
1240            }
1241
1242            current.push_with_table(sym, &config.symbol_table);
1243            generate_recursive(
1244                config,
1245                target,
1246                eval_context,
1247                current,
1248                stack_depth,
1249                lhs_out,
1250                rhs_out,
1251            );
1252            current.pop_with_table(&config.symbol_table);
1253        }
1254    }
1255
1256    // Binary operators (Seft::C) - need at least 2 on stack
1257    if stack_depth >= 2 {
1258        for &sym in &config.binary_ops {
1259            let sym_weight = config.symbol_table.weight(sym);
1260            if current.complexity() + sym_weight > max_complexity {
1261                continue;
1262            }
1263            if exceeds_symbol_limit(config, current, sym) {
1264                continue;
1265            }
1266
1267            // Apply pruning rules
1268            if should_prune_binary(current, sym) {
1269                continue;
1270            }
1271
1272            current.push_with_table(sym, &config.symbol_table);
1273            generate_recursive(
1274                config,
1275                target,
1276                eval_context,
1277                current,
1278                stack_depth - 1,
1279                lhs_out,
1280                rhs_out,
1281            );
1282            current.pop_with_table(&config.symbol_table);
1283        }
1284    }
1285}
1286
1287/// Calculate minimum complexity needed to reduce stack to depth 1
1288fn min_complexity_to_complete(stack_depth: usize, config: &GenConfig) -> u32 {
1289    if stack_depth <= 1 {
1290        return 0;
1291    }
1292
1293    // Need (stack_depth - 1) binary operators to reduce to 1
1294    let min_binary_weight = config
1295        .binary_ops
1296        .iter()
1297        .map(|s| config.symbol_table.weight(*s))
1298        .min()
1299        .unwrap_or(4);
1300
1301    ((stack_depth - 1) as u32) * min_binary_weight
1302}
1303
1304/// Pruning rules for unary operators to avoid redundant expressions
1305fn should_prune_unary(expr: &Expression, sym: Symbol) -> bool {
1306    let symbols = expr.symbols();
1307    if symbols.is_empty() {
1308        return false;
1309    }
1310
1311    let last = symbols[symbols.len() - 1];
1312
1313    use Symbol::*;
1314
1315    match (last, sym) {
1316        // Double negation: --a = a
1317        (Neg, Neg) => true,
1318        // Double reciprocal: 1/(1/a) = a
1319        (Recip, Recip) => true,
1320        // sqrt(a^2) = |a| (we don't handle absolute value)
1321        (Square, Sqrt) => true,
1322        // (sqrt(a))^2 = a
1323        (Sqrt, Square) => true,
1324        // ln(e^a) = a
1325        (Exp, Ln) => true,
1326        // e^(ln(a)) = a
1327        (Ln, Exp) => true,
1328
1329        // Additional pruning rules for cleaner output:
1330        // 1/sqrt(a) and 1/a^2 are rare, prefer a^-0.5 or a^-2 notation
1331        (Sqrt, Recip) => true,
1332        (Square, Recip) => true,
1333        // 1/ln(a) is rarely useful
1334        (Ln, Recip) => true,
1335        // Double square: (a^2)^2 = a^4, use power directly
1336        (Square, Square) => true,
1337        // Double sqrt: sqrt(sqrt(a)) = a^0.25, use power directly
1338        (Sqrt, Sqrt) => true,
1339        // Negation after subtraction is redundant with addition
1340        // e.g., -(a-b) = b-a which we could express directly
1341        (Sub, Neg) => true,
1342
1343        // ===== ENHANCED PRUNING RULES =====
1344        // Trig reduction: asin(sin(pi*x)/pi) = x, similar for acos
1345        // These are rarely useful and add many redundant expressions
1346        (SinPi, SinPi) => true,
1347        (CosPi, CosPi) => true,
1348        // asin after sinpi is identity (mod periodicity)
1349        // acos after cospi is identity (mod periodicity)
1350        // These patterns are captured by double application above
1351
1352        // Exp grows too fast - double exp is almost never useful
1353        (Exp, Exp) => true,
1354
1355        // LambertW after exp: W(e^a) = a, so W(e^x) = x
1356        (Exp, LambertW) => true,
1357
1358        // LambertW on small values often doesn't converge usefully
1359        // W of reciprocal is rarely needed
1360        (Recip, LambertW) => true,
1361
1362        _ => false,
1363    }
1364}
1365
1366/// Pruning rules for binary operators
1367fn should_prune_binary(expr: &Expression, sym: Symbol) -> bool {
1368    let symbols = expr.symbols();
1369    if symbols.len() < 2 {
1370        return false;
1371    }
1372
1373    let last = symbols[symbols.len() - 1];
1374    let prev = symbols[symbols.len() - 2];
1375
1376    use Symbol::*;
1377
1378    match sym {
1379        // a - a = 0 (if both operands are identical)
1380        Sub if is_same_subexpr(symbols, 2) => true,
1381        // x - x = 0 (trivial - always 0)
1382        Sub if last == X && prev == X => true,
1383
1384        // a / a = 1 (degenerate if a contains x)
1385        Div if is_same_subexpr(symbols, 2) => true,
1386        // x / x = 1 (trivial identity)
1387        Div if last == X && prev == X => true,
1388        // Division by 1: a/1 = a (useless)
1389        Div if last == One => true,
1390
1391        // Prefer a*2 over a+a
1392        Add if is_same_subexpr(symbols, 2) => true,
1393        // x + (-x) = 0 - check for negated x
1394        Add if last == Neg
1395            && symbols.len() >= 3
1396            && symbols[symbols.len() - 2] == X
1397            && prev == X =>
1398        {
1399            true
1400        }
1401
1402        // 1^b = 1 (degenerate - always equals 1 regardless of b)
1403        // This catches 1^x, 1^(anything)
1404        Pow if prev == One => true,
1405        // a^1 = a (useless)
1406        Pow if last == One => true,
1407
1408        // x * 1 = x, 1 * x = x
1409        Mul if last == One || prev == One => true,
1410
1411        // a"/1 = a^(1/1) = a (1st root is identity)
1412        // But more importantly: 1"/x = 1^(1/x) = 1 (degenerate)
1413        Root if prev == One => true,
1414        // x"/1 means 1^(1/x) = 1 (degenerate)
1415        Root if last == One => true,
1416        // 2nd root is just sqrt, prefer using sqrt
1417        Root if last == Two => true,
1418
1419        // log_x(x) = 1 (trivial identity)
1420        Log if last == X && prev == X => true,
1421        // log_1(anything) is undefined/infinite, log_a(1) = 0
1422        Log if prev == One || last == One => true,
1423        // log_e(a) = ln(a) - prefer ln notation
1424        Log if prev == E => true,
1425
1426        // Ordering: prefer 2+3 over 3+2 for commutative ops
1427        Add | Mul if prev > last && is_constant(last) && is_constant(prev) => true,
1428
1429        _ => false,
1430    }
1431}
1432
1433/// Check whether the top two stack subexpressions are identical.
1434///
1435/// This is only ever used with `n == 2` (the generator compares the two
1436/// operands feeding a binary operator). The boundary of a complete postfix
1437/// subexpression is found with a single backward scan that tracks how many
1438/// operands still need to be supplied to the left: atoms supply one
1439/// (`-1`), unary ops are neutral (`0`), binary ops demand one more (`+1`).
1440/// When the running balance reaches zero, the subexpression starts there.
1441///
1442/// This avoids the prefix-depth table the previous implementation built on
1443/// every call — a hot path during generation, since it runs for every
1444/// commutative/cancelling binary candidate.
1445fn is_same_subexpr(symbols: &[Symbol], n: usize) -> bool {
1446    // Each subexpression is at least one symbol, so two of them need >= 2
1447    // symbols. The original guard also rejected anything below `n * 2`, which
1448    // for the only supported case (`n == 2`) means a length of at least 4;
1449    // preserve that exactly so single-atom operands stay handled by the
1450    // dedicated `last == X && prev == X` arms in `should_prune_binary`.
1451    if n != 2 || symbols.len() < n * 2 {
1452        return false;
1453    }
1454
1455    /// Walk left from `end - 1` until one complete subexpression has been
1456    /// consumed, returning its start index (or `None` on underflow).
1457    fn subexpr_start(symbols: &[Symbol], end: usize) -> Option<usize> {
1458        let mut needed: i32 = 1;
1459        for i in (0..end).rev() {
1460            needed += match symbols[i].seft() {
1461                Seft::A => -1,
1462                Seft::B => 0,
1463                Seft::C => 1,
1464            };
1465            if needed == 0 {
1466                return Some(i);
1467            }
1468        }
1469        None
1470    }
1471
1472    let end = symbols.len();
1473    let Some(top_start) = subexpr_start(symbols, end) else {
1474        return false;
1475    };
1476    let Some(prev_start) = subexpr_start(symbols, top_start) else {
1477        return false;
1478    };
1479
1480    // Slice equality already compares lengths, so unequal-length operands
1481    // short-circuit to `false`.
1482    symbols[prev_start..top_start] == symbols[top_start..end]
1483}
1484
1485/// Check if a symbol is a constant (no x)
1486fn is_constant(sym: Symbol) -> bool {
1487    matches!(sym.seft(), Seft::A) && sym != Symbol::X
1488}
1489
1490/// Generate expressions in parallel using Rayon
1491#[cfg(feature = "parallel")]
1492pub fn generate_all_parallel(config: &GenConfig, target: f64) -> GeneratedExprs {
1493    generate_all_parallel_with_context(
1494        config,
1495        target,
1496        &EvalContext::from_slices(&config.user_constants, &config.user_functions),
1497    )
1498}
1499
1500/// Generate expressions in parallel using Rayon with an explicit evaluation context.
1501#[cfg(feature = "parallel")]
1502pub fn generate_all_parallel_with_context(
1503    config: &GenConfig,
1504    target: f64,
1505    eval_context: &EvalContext<'_>,
1506) -> GeneratedExprs {
1507    use rayon::prelude::*;
1508
1509    // Parallel path currently assumes shared LHS/RHS symbol sets.
1510    if has_rhs_symbol_overrides(config) {
1511        return generate_all_with_context(config, target, eval_context);
1512    }
1513
1514    // Generate valid prefixes of length 1 and 2 to create smaller,
1515    // more evenly distributed tasks for Rayon to schedule.
1516    let mut prefixes: Vec<(Expression, usize)> = Vec::new();
1517    let mut immediate_results_lhs = Vec::new();
1518    let mut immediate_results_rhs = Vec::new();
1519
1520    let first_symbols: Vec<Symbol> = config
1521        .constants
1522        .iter()
1523        .copied()
1524        .chain(
1525            if config.generate_lhs && !config.constants.contains(&Symbol::X) {
1526                Some(Symbol::X)
1527            } else {
1528                None
1529            },
1530        )
1531        .filter(|&sym| {
1532            config
1533                .symbol_max_counts
1534                .get(&sym)
1535                .is_none_or(|&max| max > 0)
1536        })
1537        .collect();
1538
1539    for sym1 in first_symbols {
1540        let mut expr1 = Expression::new();
1541        expr1.push_with_table(sym1, &config.symbol_table);
1542
1543        let max_complexity = if expr1.contains_x() {
1544            config.max_lhs_complexity
1545        } else {
1546            std::cmp::max(config.max_lhs_complexity, config.max_rhs_complexity)
1547        };
1548
1549        if expr1.complexity() > max_complexity {
1550            continue;
1551        }
1552
1553        // 1. Evaluate length-1 prefix (simulate top of generate_recursive)
1554        if let Ok(result) = evaluate_fast_with_context(&expr1, target, eval_context) {
1555            if result.value.is_finite()
1556                && result.value.abs() <= MAX_GENERATED_VALUE
1557                && result.num_type >= config.min_num_type
1558            {
1559                let eval_expr = EvaluatedExpr::new(
1560                    expr1.clone(),
1561                    result.value,
1562                    result.derivative,
1563                    result.num_type,
1564                );
1565
1566                if expr1.contains_x() {
1567                    if config.generate_lhs && expr1.complexity() <= config.max_lhs_complexity {
1568                        immediate_results_lhs.push(eval_expr);
1569                    }
1570                } else if config.generate_rhs && expr1.complexity() <= config.max_rhs_complexity {
1571                    immediate_results_rhs.push(eval_expr);
1572                }
1573            }
1574        }
1575
1576        if expr1.len() >= config.max_length {
1577            continue;
1578        }
1579
1580        // 2. Add next symbols (simulate bottom of generate_recursive)
1581
1582        // Constants (+1 stack)
1583        let mut next_constants = config.constants.clone();
1584        if config.generate_lhs && !next_constants.contains(&Symbol::X) {
1585            next_constants.push(Symbol::X);
1586        }
1587
1588        for &sym2 in &next_constants {
1589            let sym2_weight = config.symbol_table.weight(sym2);
1590            let next_max = if expr1.contains_x() || sym2 == Symbol::X {
1591                config.max_lhs_complexity
1592            } else {
1593                std::cmp::max(config.max_lhs_complexity, config.max_rhs_complexity)
1594            };
1595
1596            if expr1.complexity() + sym2_weight <= next_max
1597                && !exceeds_symbol_limit(config, &expr1, sym2)
1598            {
1599                let mut expr2 = expr1.clone();
1600                expr2.push_with_table(sym2, &config.symbol_table);
1601                // Min complexity check: for stack depth 2, we need at least 1 binary op
1602                let min_remaining = min_complexity_to_complete(2, config);
1603                if expr2.complexity() + min_remaining <= next_max {
1604                    prefixes.push((expr2, 2));
1605                }
1606            }
1607        }
1608
1609        // Unary ops (+0 stack)
1610        for &sym2 in &config.unary_ops {
1611            let sym2_weight = config.symbol_table.weight(sym2);
1612            if expr1.complexity() + sym2_weight <= max_complexity
1613                && !exceeds_symbol_limit(config, &expr1, sym2)
1614                && !should_prune_unary(&expr1, sym2)
1615            {
1616                let mut expr2 = expr1.clone();
1617                expr2.push_with_table(sym2, &config.symbol_table);
1618                let min_remaining = min_complexity_to_complete(1, config);
1619                if expr2.complexity() + min_remaining <= max_complexity {
1620                    prefixes.push((expr2, 1));
1621                }
1622            }
1623        }
1624    }
1625
1626    let results: Vec<(Vec<EvaluatedExpr>, Vec<EvaluatedExpr>)> = prefixes
1627        .into_par_iter()
1628        .map(|(mut expr, depth)| {
1629            let mut lhs = Vec::new();
1630            let mut rhs = Vec::new();
1631            generate_recursive(
1632                config,
1633                target,
1634                *eval_context,
1635                &mut expr,
1636                depth,
1637                &mut lhs,
1638                &mut rhs,
1639            );
1640            (lhs, rhs)
1641        })
1642        .collect();
1643
1644    // Merge results
1645    let mut lhs_raw = immediate_results_lhs;
1646    let mut rhs_raw = immediate_results_rhs;
1647    for (lhs, rhs) in results {
1648        lhs_raw.extend(lhs);
1649        rhs_raw.extend(rhs);
1650    }
1651
1652    // Deduplicate RHS by value, keeping simplest expression for each value
1653    GeneratedExprs {
1654        lhs: dedupe_lhs_expressions(lhs_raw),
1655        rhs: dedupe_rhs_expressions(rhs_raw),
1656    }
1657}
1658
1659#[cfg(test)]
1660mod tests {
1661    use super::*;
1662    #[cfg(not(target_arch = "wasm32"))]
1663    use proptest::prelude::*;
1664
1665    /// Create a fast test config with limited complexity and operators
1666    fn fast_test_config() -> GenConfig {
1667        GenConfig {
1668            max_lhs_complexity: 20,
1669            max_rhs_complexity: 20,
1670            max_length: 8,
1671            constants: vec![
1672                Symbol::One,
1673                Symbol::Two,
1674                Symbol::Three,
1675                Symbol::Four,
1676                Symbol::Five,
1677                Symbol::Pi,
1678                Symbol::E,
1679            ],
1680            unary_ops: vec![Symbol::Neg, Symbol::Recip, Symbol::Square, Symbol::Sqrt],
1681            binary_ops: vec![Symbol::Add, Symbol::Sub, Symbol::Mul, Symbol::Div],
1682            rhs_constants: None,
1683            rhs_unary_ops: None,
1684            rhs_binary_ops: None,
1685            symbol_max_counts: HashMap::new(),
1686            rhs_symbol_max_counts: None,
1687            min_num_type: NumType::Transcendental,
1688            generate_lhs: true,
1689            generate_rhs: true,
1690            user_constants: Vec::new(),
1691            user_functions: Vec::new(),
1692            show_pruned_arith: false,
1693            symbol_table: Arc::new(SymbolTable::new()),
1694        }
1695    }
1696
1697    fn expr_from_postfix(s: &str) -> Expression {
1698        Expression::parse(s).expect("valid expression")
1699    }
1700
1701    fn stub_eval_expr(postfix: &str, value: f64, derivative: f64) -> EvaluatedExpr {
1702        EvaluatedExpr {
1703            expr: expr_from_postfix(postfix),
1704            value,
1705            derivative,
1706            num_type: NumType::Transcendental,
1707        }
1708    }
1709
1710    #[test]
1711    fn test_generate_simple() {
1712        let mut config = fast_test_config();
1713        config.generate_lhs = false; // Only RHS for simpler test
1714
1715        let result = generate_all(&config, 1.0);
1716
1717        // Should have some RHS expressions
1718        assert!(!result.rhs.is_empty());
1719
1720        // All should be valid (evaluate without error)
1721        for expr in &result.rhs {
1722            assert!(!expr.expr.contains_x());
1723        }
1724    }
1725
1726    #[test]
1727    fn test_generate_lhs() {
1728        let mut config = fast_test_config();
1729        config.generate_rhs = false;
1730
1731        let result = generate_all(&config, 2.0);
1732
1733        // Should have LHS expressions containing x
1734        assert!(!result.lhs.is_empty());
1735        for expr in &result.lhs {
1736            assert!(expr.expr.contains_x());
1737        }
1738    }
1739
1740    #[test]
1741    fn test_complexity_limit() {
1742        let config = fast_test_config();
1743
1744        let result = generate_all(&config, 1.0);
1745
1746        for expr in &result.rhs {
1747            assert!(expr.expr.complexity() <= config.max_rhs_complexity);
1748        }
1749        for expr in &result.lhs {
1750            assert!(expr.expr.complexity() <= config.max_lhs_complexity);
1751        }
1752    }
1753
1754    #[test]
1755    fn test_generate_all_with_limit_aborts_when_exceeded() {
1756        // Config with high complexity that will generate many expressions.
1757        // With new calibrated weights, even moderate complexity can generate 100+ expressions.
1758        let mut config = fast_test_config();
1759        config.max_lhs_complexity = 30;
1760        config.max_rhs_complexity = 30;
1761
1762        // First, check how many expressions would be generated without limit.
1763        let unlimited = generate_all(&config, 2.5);
1764        let total_unlimited = unlimited.lhs.len() + unlimited.rhs.len();
1765
1766        // The test only makes sense if we'd generate more than a handful.
1767        assert!(
1768            total_unlimited > 10,
1769            "Test config should generate >10 expressions"
1770        );
1771
1772        // Now test with a limit less than the actual count — should return None.
1773        let limit = total_unlimited / 2; // Set limit to half of what would be generated
1774        let result = generate_all_with_limit(&config, 2.5, limit);
1775
1776        assert!(
1777            result.is_none(),
1778            "generate_all_with_limit should return None when limit ({}) is exceeded (actual: {})",
1779            limit,
1780            total_unlimited
1781        );
1782    }
1783
1784    #[test]
1785    fn test_generate_all_with_limit_succeeds_when_within_limit() {
1786        // Same config but with a generous limit that won't be hit.
1787        let mut config = fast_test_config();
1788        config.max_lhs_complexity = 30;
1789        config.max_rhs_complexity = 30;
1790
1791        // Set limit much higher than expected expression count.
1792        let result = generate_all_with_limit(&config, 2.5, 10_000);
1793
1794        assert!(
1795            result.is_some(),
1796            "generate_all_with_limit should return Some when limit is not exceeded"
1797        );
1798
1799        let generated = result.unwrap();
1800        // Should have generated some expressions.
1801        assert!(!generated.lhs.is_empty() || !generated.rhs.is_empty());
1802    }
1803
1804    #[test]
1805    fn test_count_expressions_with_limit_matches_generated_total() {
1806        let mut config = fast_test_config();
1807        config.max_lhs_complexity = 30;
1808        config.max_rhs_complexity = 30;
1809
1810        let context = EvalContext::from_slices(&config.user_constants, &config.user_functions);
1811        let total_generated = std::cell::Cell::new(0usize);
1812        let mut callbacks = StreamingCallbacks {
1813            on_lhs: &mut |_expr| {
1814                total_generated.set(total_generated.get() + 1);
1815                true
1816            },
1817            on_rhs: &mut |_expr| {
1818                total_generated.set(total_generated.get() + 1);
1819                true
1820            },
1821        };
1822        generate_streaming_with_context(&config, 2.5, &context, &mut callbacks);
1823        let counted = count_expressions_with_limit_and_context(
1824            &config,
1825            2.5,
1826            &context,
1827            total_generated.get() + 1,
1828        );
1829
1830        assert_eq!(counted, Some(total_generated.get()));
1831    }
1832
1833    #[test]
1834    fn test_count_expressions_with_limit_returns_none_when_exceeded() {
1835        let mut config = fast_test_config();
1836        config.max_lhs_complexity = 30;
1837        config.max_rhs_complexity = 30;
1838
1839        let context = EvalContext::from_slices(&config.user_constants, &config.user_functions);
1840        let total_generated = std::cell::Cell::new(0usize);
1841        let mut callbacks = StreamingCallbacks {
1842            on_lhs: &mut |_expr| {
1843                total_generated.set(total_generated.get() + 1);
1844                true
1845            },
1846            on_rhs: &mut |_expr| {
1847                total_generated.set(total_generated.get() + 1);
1848                true
1849            },
1850        };
1851        generate_streaming_with_context(&config, 2.5, &context, &mut callbacks);
1852        let counted = count_expressions_with_limit_and_context(
1853            &config,
1854            2.5,
1855            &context,
1856            total_generated.get() / 2,
1857        );
1858
1859        assert_eq!(counted, None);
1860    }
1861
1862    // ==================== expression_respects_constraints tests ====================
1863
1864    #[test]
1865    fn test_is_same_subexpr_detects_identical_operands() {
1866        use crate::symbol::Symbol::*;
1867
1868        // 2 3 + | 2 3 + : the two operands of a pending binary op are equal.
1869        let same = [Two, Three, Add, Two, Three, Add];
1870        assert!(is_same_subexpr(&same, 2));
1871
1872        // 2 3 + | 2 4 + : operands differ in the last atom.
1873        let diff = [Two, Three, Add, Two, Four, Add];
1874        assert!(!is_same_subexpr(&diff, 2));
1875
1876        // Unequal-length operands: 2 3 + | 5 .
1877        let unequal = [Two, Three, Add, Five];
1878        assert!(!is_same_subexpr(&unequal, 2));
1879
1880        // Nested unary inside identical operands: 2 q n | 2 q n (sqrt then neg).
1881        let nested = [Two, Sqrt, Neg, Two, Sqrt, Neg];
1882        assert!(is_same_subexpr(&nested, 2));
1883
1884        // Single-atom operands stay out of scope (handled by dedicated arms).
1885        let short = [Two, Two];
1886        assert!(!is_same_subexpr(&short, 2));
1887
1888        // Only n == 2 is supported.
1889        let triple = [Two, Three, Add, Two, Three, Add, Two, Three, Add];
1890        assert!(!is_same_subexpr(&triple, 3));
1891    }
1892
1893    #[test]
1894    fn test_quantize_value_signed_boundary_symmetry_around_zero() {
1895        let below = 0.49 / QUANTIZE_SCALE;
1896        let above = 0.51 / QUANTIZE_SCALE;
1897
1898        assert_eq!(quantize_value(below), 0);
1899        assert_eq!(quantize_value(-below), 0);
1900        assert_eq!(quantize_value(above), 1);
1901        assert_eq!(quantize_value(-above), -1);
1902    }
1903
1904    #[test]
1905    fn test_dedupe_rhs_keeps_simplest_expression_within_quantized_bucket() {
1906        let base = 2.0;
1907        let rhs = dedupe_rhs_expressions(vec![
1908            stub_eval_expr("11+", base + 0.49 / QUANTIZE_SCALE, 0.0),
1909            stub_eval_expr("2", base, 0.0),
1910        ]);
1911
1912        assert_eq!(rhs.len(), 1);
1913        assert_eq!(rhs[0].expr.to_postfix(), "2");
1914    }
1915
1916    #[test]
1917    fn test_dedupe_lhs_preserves_adjacent_derivative_buckets() {
1918        let lhs = dedupe_lhs_expressions(vec![
1919            stub_eval_expr("x", 1.0, 1.0),
1920            stub_eval_expr("x1+", 1.0, 1.0 + 0.51 / QUANTIZE_SCALE),
1921        ]);
1922
1923        assert_eq!(lhs.len(), 2);
1924    }
1925
1926    #[cfg(not(target_arch = "wasm32"))]
1927    proptest! {
1928        #[test]
1929        fn test_quantize_value_collapses_values_within_same_bucket(bucket in -1_000_000i64..1_000_000i64) {
1930            let base = bucket as f64 / QUANTIZE_SCALE;
1931            prop_assert_eq!(quantize_value(base + 0.49 / QUANTIZE_SCALE), bucket);
1932            prop_assert_eq!(quantize_value(base - 0.49 / QUANTIZE_SCALE), bucket);
1933        }
1934
1935        #[test]
1936        fn test_quantize_value_separates_adjacent_buckets(bucket in -1_000_000i64..1_000_000i64) {
1937            let base = bucket as f64 / QUANTIZE_SCALE;
1938            prop_assert_eq!(quantize_value(base + 0.51 / QUANTIZE_SCALE), bucket + 1);
1939            prop_assert_eq!(quantize_value(base - 0.51 / QUANTIZE_SCALE), bucket - 1);
1940        }
1941    }
1942
1943    #[test]
1944    fn test_constraints_default_allows_all() {
1945        let opts = ExpressionConstraintOptions::default();
1946
1947        // x^pi should be allowed with default options
1948        let expr = expr_from_postfix("xp^"); // x^pi
1949        assert!(
1950            expression_respects_constraints(&expr, opts),
1951            "x^pi should be allowed with default options"
1952        );
1953
1954        // sinpi(e) should be allowed
1955        let expr = expr_from_postfix("eS"); // e then sinpi (S = SinPi)
1956        assert!(
1957            expression_respects_constraints(&expr, opts),
1958            "sinpi(e) should be allowed with default options"
1959        );
1960    }
1961
1962    #[test]
1963    fn test_constraints_rational_exponents_rejects_transcendental() {
1964        let opts = ExpressionConstraintOptions {
1965            rational_exponents: true,
1966            ..Default::default()
1967        };
1968
1969        // x^pi should be rejected (pi is transcendental)
1970        let expr = expr_from_postfix("xp^");
1971        assert!(
1972            !expression_respects_constraints(&expr, opts),
1973            "x^pi should be rejected with rational_exponents=true"
1974        );
1975
1976        // x^e should be rejected
1977        let expr = expr_from_postfix("xe^");
1978        assert!(
1979            !expression_respects_constraints(&expr, opts),
1980            "x^e should be rejected with rational_exponents=true"
1981        );
1982    }
1983
1984    #[test]
1985    fn test_constraints_rational_exponents_allows_integer() {
1986        let opts = ExpressionConstraintOptions {
1987            rational_exponents: true,
1988            ..Default::default()
1989        };
1990
1991        // x^2 should be allowed (2 is integer)
1992        let expr = expr_from_postfix("x2^");
1993        assert!(
1994            expression_respects_constraints(&expr, opts),
1995            "x^2 should be allowed with rational_exponents=true"
1996        );
1997
1998        // x^1 should be allowed
1999        let expr = expr_from_postfix("x1^");
2000        assert!(
2001            expression_respects_constraints(&expr, opts),
2002            "x^1 should be allowed with rational_exponents=true"
2003        );
2004    }
2005
2006    #[test]
2007    fn test_constraints_rational_trig_args_rejects_irrational() {
2008        let opts = ExpressionConstraintOptions {
2009            rational_trig_args: true,
2010            ..Default::default()
2011        };
2012
2013        // sinpi(e) should be rejected (e is irrational/transcendental)
2014        let expr = expr_from_postfix("eS"); // e then sinpi (S = SinPi)
2015        assert!(
2016            !expression_respects_constraints(&expr, opts),
2017            "sinpi(e) should be rejected with rational_trig_args=true"
2018        );
2019
2020        // sinpi(pi) should be rejected (pi is transcendental)
2021        let expr = expr_from_postfix("pS"); // pi then sinpi
2022        assert!(
2023            !expression_respects_constraints(&expr, opts),
2024            "sinpi(pi) should be rejected with rational_trig_args=true"
2025        );
2026    }
2027
2028    #[test]
2029    fn test_constraints_rational_trig_args_allows_rational() {
2030        let opts = ExpressionConstraintOptions {
2031            rational_trig_args: true,
2032            ..Default::default()
2033        };
2034
2035        // sinpi(1) should be allowed (1 is integer, hence rational)
2036        let expr = expr_from_postfix("1S"); // 1 then sinpi (S = SinPi)
2037        assert!(
2038            expression_respects_constraints(&expr, opts),
2039            "sinpi(1) should be allowed with rational_trig_args=true"
2040        );
2041
2042        // sinpi(2) should be allowed
2043        let expr = expr_from_postfix("2S");
2044        assert!(
2045            expression_respects_constraints(&expr, opts),
2046            "sinpi(2) should be allowed with rational_trig_args=true"
2047        );
2048    }
2049
2050    #[test]
2051    fn test_constraints_rational_trig_args_rejects_x() {
2052        let opts = ExpressionConstraintOptions {
2053            rational_trig_args: true,
2054            ..Default::default()
2055        };
2056
2057        // sinpi(x) should be rejected (x is not a constant rational)
2058        let expr = expr_from_postfix("xS"); // x then sinpi (S = SinPi)
2059        assert!(
2060            !expression_respects_constraints(&expr, opts),
2061            "sinpi(x) should be rejected with rational_trig_args=true"
2062        );
2063    }
2064
2065    #[test]
2066    fn test_constraints_max_trig_cycles() {
2067        let opts = ExpressionConstraintOptions {
2068            max_trig_cycles: Some(2),
2069            ..Default::default()
2070        };
2071
2072        // Single trig: sinpi(x) - should pass
2073        let expr = expr_from_postfix("xS"); // x then sinpi (S = SinPi)
2074        assert!(
2075            expression_respects_constraints(&expr, opts),
2076            "1 trig op should pass with max=2"
2077        );
2078
2079        // Double nested: sinpi(cospi(x)) - should pass
2080        // x C S = sinpi(cospi(x)) where C = CosPi, S = SinPi
2081        let expr = expr_from_postfix("xCS");
2082        assert!(
2083            expression_respects_constraints(&expr, opts),
2084            "2 trig ops should pass with max=2"
2085        );
2086
2087        // Triple nested: sinpi(cospi(tanpi(x))) - should fail
2088        // x T C S = sinpi(cospi(tanpi(x))) where T = TanPi
2089        let expr = expr_from_postfix("xTCS");
2090        assert!(
2091            !expression_respects_constraints(&expr, opts),
2092            "3 trig ops should fail with max=2"
2093        );
2094    }
2095
2096    #[test]
2097    fn test_constraints_max_trig_cycles_none_unlimited() {
2098        let opts = ExpressionConstraintOptions {
2099            max_trig_cycles: None, // No limit
2100            ..Default::default()
2101        };
2102
2103        // Even deeply nested trig should pass
2104        // x T C S T C S = 6 trig ops
2105        let expr = expr_from_postfix("xTCSTCS");
2106        assert!(
2107            expression_respects_constraints(&expr, opts),
2108            "Unlimited trig should pass any depth"
2109        );
2110    }
2111
2112    #[test]
2113    fn test_constraints_combined() {
2114        let opts = ExpressionConstraintOptions {
2115            rational_exponents: true,
2116            rational_trig_args: true,
2117            max_trig_cycles: Some(1),
2118            ..Default::default()
2119        };
2120
2121        // x^2 + sinpi(1) should pass
2122        let expr = expr_from_postfix("x2^1S+"); // S = SinPi
2123        assert!(
2124            expression_respects_constraints(&expr, opts),
2125            "x^2 + sinpi(1) should pass all constraints"
2126        );
2127
2128        // x^pi should fail (rational_exponents)
2129        let expr = expr_from_postfix("xp^");
2130        assert!(
2131            !expression_respects_constraints(&expr, opts),
2132            "x^pi should fail rational_exponents"
2133        );
2134
2135        // sinpi(x) should fail (rational_trig_args)
2136        let expr = expr_from_postfix("xS"); // S = SinPi
2137        assert!(
2138            !expression_respects_constraints(&expr, opts),
2139            "sinpi(x) should fail rational_trig_args"
2140        );
2141
2142        // sinpi(cospi(1)) should fail (max_trig_cycles)
2143        let expr = expr_from_postfix("1CS"); // C = CosPi, S = SinPi
2144        assert!(
2145            !expression_respects_constraints(&expr, opts),
2146            "double trig should fail max_trig_cycles=1"
2147        );
2148    }
2149
2150    #[test]
2151    fn test_constraints_malformed_expression() {
2152        let opts = ExpressionConstraintOptions::default();
2153
2154        // Expression that would cause stack underflow
2155        let expr = Expression::from_symbols(&[crate::symbol::Symbol::Add]); // Just a binary op
2156        assert!(
2157            !expression_respects_constraints(&expr, opts),
2158            "Malformed expression should return false"
2159        );
2160
2161        // Incomplete expression (too many values)
2162        let expr =
2163            Expression::from_symbols(&[crate::symbol::Symbol::One, crate::symbol::Symbol::Two]);
2164        assert!(
2165            !expression_respects_constraints(&expr, opts),
2166            "Incomplete expression should return false"
2167        );
2168    }
2169
2170    #[test]
2171    fn test_constraints_user_constant_types() {
2172        // Set user constant 0 to be Integer type
2173        let mut user_types = [NumType::Transcendental; 16];
2174        user_types[0] = NumType::Integer;
2175
2176        let opts = ExpressionConstraintOptions {
2177            rational_exponents: true,
2178            user_constant_types: user_types,
2179            ..Default::default()
2180        };
2181
2182        // If UserConstant0 is treated as Integer, x^UserConstant0 should be allowed
2183        // (We can't easily test this without actually having user constants in the expression,
2184        // but this verifies the options struct is properly configured)
2185        assert_eq!(opts.user_constant_types[0], NumType::Integer);
2186    }
2187}