1use 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
22const MAX_QUANTIZED_VALUE: f64 = 1e10;
32
33const 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#[derive(Clone)]
76pub struct GenConfig {
77 pub max_lhs_complexity: u32,
85
86 pub max_rhs_complexity: u32,
93
94 pub max_length: usize,
101
102 pub constants: Vec<Symbol>,
111
112 pub unary_ops: Vec<Symbol>,
123
124 pub binary_ops: Vec<Symbol>,
132
133 pub rhs_constants: Option<Vec<Symbol>>,
140
141 pub rhs_unary_ops: Option<Vec<Symbol>>,
148
149 pub rhs_binary_ops: Option<Vec<Symbol>>,
155
156 pub symbol_max_counts: HashMap<Symbol, u32>,
164
165 pub rhs_symbol_max_counts: Option<HashMap<Symbol, u32>>,
172
173 pub min_num_type: NumType,
185
186 pub generate_lhs: bool,
193
194 pub generate_rhs: bool,
201
202 pub user_constants: Vec<UserConstant>,
210
211 pub user_functions: Vec<UserFunction>,
219
220 pub show_pruned_arith: bool,
228
229 pub symbol_table: Arc<SymbolTable>,
237}
238
239#[derive(Debug, Clone, Copy)]
244pub struct ExpressionConstraintOptions {
245 pub rational_exponents: bool,
247 pub rational_trig_args: bool,
249 pub max_trig_cycles: Option<u32>,
251 pub user_constant_types: [NumType; 16],
253 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
269pub 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
438pub 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
472pub 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
492pub struct GeneratedExprs {
494 pub lhs: Vec<EvaluatedExpr>,
496 pub rhs: Vec<EvaluatedExpr>,
498}
499
500pub struct StreamingCallbacks<'a> {
505 pub on_rhs: &'a mut dyn FnMut(&EvaluatedExpr) -> bool,
508 pub on_lhs: &'a mut dyn FnMut(&EvaluatedExpr) -> bool,
511}
512
513pub type LhsKey = (i64, i64);
516
517#[inline]
519pub fn quantize_value(v: f64) -> i64 {
520 if !v.is_finite() || v.abs() > MAX_QUANTIZED_VALUE {
521 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 (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
573pub 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
582pub 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 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 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_recursive(
620 config,
621 target,
622 *eval_context,
623 &mut Expression::new(),
624 0, &mut lhs_raw,
626 &mut rhs_raw,
627 );
628 }
629
630 GeneratedExprs {
632 lhs: dedupe_lhs_expressions(lhs_raw),
633 rhs: dedupe_rhs_expressions(rhs_raw),
634 }
635}
636
637pub 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
668pub 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 let mut lhs_raw = Vec::new();
683 let mut rhs_raw = Vec::new();
684
685 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; }
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; }
700 rhs_raw.push(expr.clone());
701 true
702 },
703 };
704
705 generate_streaming_with_context(config, target, eval_context, &mut callbacks);
706
707 let final_count = count.load(Ordering::Relaxed);
709 if final_count > limit {
710 return None;
711 }
712
713 Some(GeneratedExprs {
715 lhs: dedupe_lhs_expressions(lhs_raw),
716 rhs: dedupe_rhs_expressions(rhs_raw),
717 })
718}
719
720pub 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
770pub 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, callbacks,
809 );
810 }
811}
812
813pub 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#[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#[inline]
881fn get_max_complexity(config: &GenConfig, contains_x: bool) -> u32 {
882 if contains_x {
883 config.max_lhs_complexity
884 } else {
885 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
918fn 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 if stack_depth == 1 && !current.is_empty() {
932 match evaluate_fast_with_context(current, target, &eval_context) {
934 Ok(result) => {
935 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 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 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 if current.len() >= config.max_length {
972 return true;
973 }
974
975 let max_complexity = get_max_complexity(config, current.contains_x());
977
978 if current.complexity() >= max_complexity {
979 return true;
980 }
981
982 let min_remaining = min_complexity_to_complete(stack_depth, config);
984 if current.complexity() + min_remaining > max_complexity {
985 return true;
986 }
987
988 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 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 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 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 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 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 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
1110fn 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 if stack_depth == 1 && !current.is_empty() {
1122 match evaluate_fast_with_context(current, target, &eval_context) {
1124 Ok(result) => {
1125 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 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 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 if current.len() >= config.max_length {
1159 return;
1160 }
1161
1162 let max_complexity = get_max_complexity(config, current.contains_x());
1164
1165 if current.complexity() >= max_complexity {
1166 return;
1167 }
1168
1169 let min_remaining = min_complexity_to_complete(stack_depth, config);
1171 if current.complexity() + min_remaining > max_complexity {
1172 return;
1173 }
1174
1175 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 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 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 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 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 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 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
1287fn min_complexity_to_complete(stack_depth: usize, config: &GenConfig) -> u32 {
1289 if stack_depth <= 1 {
1290 return 0;
1291 }
1292
1293 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
1304fn 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 (Neg, Neg) => true,
1318 (Recip, Recip) => true,
1320 (Square, Sqrt) => true,
1322 (Sqrt, Square) => true,
1324 (Exp, Ln) => true,
1326 (Ln, Exp) => true,
1328
1329 (Sqrt, Recip) => true,
1332 (Square, Recip) => true,
1333 (Ln, Recip) => true,
1335 (Square, Square) => true,
1337 (Sqrt, Sqrt) => true,
1339 (Sub, Neg) => true,
1342
1343 (SinPi, SinPi) => true,
1347 (CosPi, CosPi) => true,
1348 (Exp, Exp) => true,
1354
1355 (Exp, LambertW) => true,
1357
1358 (Recip, LambertW) => true,
1361
1362 _ => false,
1363 }
1364}
1365
1366fn 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 Sub if is_same_subexpr(symbols, 2) => true,
1381 Sub if last == X && prev == X => true,
1383
1384 Div if is_same_subexpr(symbols, 2) => true,
1386 Div if last == X && prev == X => true,
1388 Div if last == One => true,
1390
1391 Add if is_same_subexpr(symbols, 2) => true,
1393 Add if last == Neg
1395 && symbols.len() >= 3
1396 && symbols[symbols.len() - 2] == X
1397 && prev == X =>
1398 {
1399 true
1400 }
1401
1402 Pow if prev == One => true,
1405 Pow if last == One => true,
1407
1408 Mul if last == One || prev == One => true,
1410
1411 Root if prev == One => true,
1414 Root if last == One => true,
1416 Root if last == Two => true,
1418
1419 Log if last == X && prev == X => true,
1421 Log if prev == One || last == One => true,
1423 Log if prev == E => true,
1425
1426 Add | Mul if prev > last && is_constant(last) && is_constant(prev) => true,
1428
1429 _ => false,
1430 }
1431}
1432
1433fn is_same_subexpr(symbols: &[Symbol], n: usize) -> bool {
1446 if n != 2 || symbols.len() < n * 2 {
1452 return false;
1453 }
1454
1455 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 symbols[prev_start..top_start] == symbols[top_start..end]
1483}
1484
1485fn is_constant(sym: Symbol) -> bool {
1487 matches!(sym.seft(), Seft::A) && sym != Symbol::X
1488}
1489
1490#[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#[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 if has_rhs_symbol_overrides(config) {
1511 return generate_all_with_context(config, target, eval_context);
1512 }
1513
1514 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 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 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 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 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 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 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 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; let result = generate_all(&config, 1.0);
1716
1717 assert!(!result.rhs.is_empty());
1719
1720 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 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 let mut config = fast_test_config();
1759 config.max_lhs_complexity = 30;
1760 config.max_rhs_complexity = 30;
1761
1762 let unlimited = generate_all(&config, 2.5);
1764 let total_unlimited = unlimited.lhs.len() + unlimited.rhs.len();
1765
1766 assert!(
1768 total_unlimited > 10,
1769 "Test config should generate >10 expressions"
1770 );
1771
1772 let limit = total_unlimited / 2; 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 let mut config = fast_test_config();
1788 config.max_lhs_complexity = 30;
1789 config.max_rhs_complexity = 30;
1790
1791 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 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 #[test]
1865 fn test_is_same_subexpr_detects_identical_operands() {
1866 use crate::symbol::Symbol::*;
1867
1868 let same = [Two, Three, Add, Two, Three, Add];
1870 assert!(is_same_subexpr(&same, 2));
1871
1872 let diff = [Two, Three, Add, Two, Four, Add];
1874 assert!(!is_same_subexpr(&diff, 2));
1875
1876 let unequal = [Two, Three, Add, Five];
1878 assert!(!is_same_subexpr(&unequal, 2));
1879
1880 let nested = [Two, Sqrt, Neg, Two, Sqrt, Neg];
1882 assert!(is_same_subexpr(&nested, 2));
1883
1884 let short = [Two, Two];
1886 assert!(!is_same_subexpr(&short, 2));
1887
1888 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 let expr = expr_from_postfix("xp^"); assert!(
1950 expression_respects_constraints(&expr, opts),
1951 "x^pi should be allowed with default options"
1952 );
1953
1954 let expr = expr_from_postfix("eS"); 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 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 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 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 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 let expr = expr_from_postfix("eS"); assert!(
2016 !expression_respects_constraints(&expr, opts),
2017 "sinpi(e) should be rejected with rational_trig_args=true"
2018 );
2019
2020 let expr = expr_from_postfix("pS"); 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 let expr = expr_from_postfix("1S"); assert!(
2038 expression_respects_constraints(&expr, opts),
2039 "sinpi(1) should be allowed with rational_trig_args=true"
2040 );
2041
2042 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 let expr = expr_from_postfix("xS"); 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 let expr = expr_from_postfix("xS"); assert!(
2075 expression_respects_constraints(&expr, opts),
2076 "1 trig op should pass with max=2"
2077 );
2078
2079 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 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, ..Default::default()
2101 };
2102
2103 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 let expr = expr_from_postfix("x2^1S+"); assert!(
2124 expression_respects_constraints(&expr, opts),
2125 "x^2 + sinpi(1) should pass all constraints"
2126 );
2127
2128 let expr = expr_from_postfix("xp^");
2130 assert!(
2131 !expression_respects_constraints(&expr, opts),
2132 "x^pi should fail rational_exponents"
2133 );
2134
2135 let expr = expr_from_postfix("xS"); assert!(
2138 !expression_respects_constraints(&expr, opts),
2139 "sinpi(x) should fail rational_trig_args"
2140 );
2141
2142 let expr = expr_from_postfix("1CS"); 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 let expr = Expression::from_symbols(&[crate::symbol::Symbol::Add]); assert!(
2157 !expression_respects_constraints(&expr, opts),
2158 "Malformed expression should return false"
2159 );
2160
2161 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 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 assert_eq!(opts.user_constant_types[0], NumType::Integer);
2186 }
2187}