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
720#[cfg(feature = "parallel")]
729pub fn generate_all_preserving_lhs_with_limit_and_context(
730 config: &GenConfig,
731 target: f64,
732 eval_context: &EvalContext<'_>,
733 max_expressions: usize,
734) -> Option<GeneratedExprs> {
735 use std::sync::atomic::{AtomicUsize, Ordering};
736 use std::sync::Arc;
737
738 let count = Arc::new(AtomicUsize::new(0));
739 let limit = max_expressions;
740
741 let mut lhs_raw = Vec::new();
742 let mut rhs_raw = Vec::new();
743
744 let mut callbacks = StreamingCallbacks {
745 on_lhs: &mut |expr| {
746 let current = count.fetch_add(1, Ordering::Relaxed) + 1;
747 if current > limit {
748 return false;
749 }
750 lhs_raw.push(expr.clone());
751 true
752 },
753 on_rhs: &mut |expr| {
754 let current = count.fetch_add(1, Ordering::Relaxed) + 1;
755 if current > limit {
756 return false;
757 }
758 rhs_raw.push(expr.clone());
759 true
760 },
761 };
762
763 generate_streaming_with_context(config, target, eval_context, &mut callbacks);
764
765 let final_count = count.load(Ordering::Relaxed);
766 if final_count > limit {
767 return None;
768 }
769
770 Some(GeneratedExprs {
771 lhs: lhs_raw,
772 rhs: dedupe_rhs_expressions(rhs_raw),
773 })
774}
775
776pub fn generate_streaming(config: &GenConfig, target: f64, callbacks: &mut StreamingCallbacks) {
818 generate_streaming_with_context(
819 config,
820 target,
821 &EvalContext::from_slices(&config.user_constants, &config.user_functions),
822 callbacks,
823 );
824}
825
826pub fn generate_streaming_with_context(
828 config: &GenConfig,
829 target: f64,
830 eval_context: &EvalContext<'_>,
831 callbacks: &mut StreamingCallbacks,
832) {
833 if config.generate_lhs && config.generate_rhs && has_rhs_symbol_overrides(config) {
834 let mut lhs_config = config.clone();
835 lhs_config.generate_lhs = true;
836 lhs_config.generate_rhs = false;
837 if !generate_recursive_streaming(
838 &lhs_config,
839 target,
840 *eval_context,
841 &mut Expression::new(),
842 0,
843 callbacks,
844 ) {
845 return;
846 }
847
848 let rhs_config = rhs_only_config(config);
849 generate_recursive_streaming(
850 &rhs_config,
851 target,
852 *eval_context,
853 &mut Expression::new(),
854 0,
855 callbacks,
856 );
857 } else {
858 generate_recursive_streaming(
859 config,
860 target,
861 *eval_context,
862 &mut Expression::new(),
863 0, callbacks,
865 );
866 }
867}
868
869pub fn count_expressions_with_limit_and_context(
874 config: &GenConfig,
875 target: f64,
876 eval_context: &EvalContext<'_>,
877 max_expressions: usize,
878) -> Option<usize> {
879 let count = std::cell::Cell::new(0usize);
880 let mut callbacks = StreamingCallbacks {
881 on_lhs: &mut |_expr| {
882 let next = count.get() + 1;
883 count.set(next);
884 next <= max_expressions
885 },
886 on_rhs: &mut |_expr| {
887 let next = count.get() + 1;
888 count.set(next);
889 next <= max_expressions
890 },
891 };
892
893 generate_streaming_with_context(config, target, eval_context, &mut callbacks);
894
895 if count.get() > max_expressions {
896 None
897 } else {
898 Some(count.get())
899 }
900}
901
902#[inline]
903fn has_rhs_symbol_overrides(config: &GenConfig) -> bool {
904 config.rhs_constants.is_some()
905 || config.rhs_unary_ops.is_some()
906 || config.rhs_binary_ops.is_some()
907 || config.rhs_symbol_max_counts.is_some()
908}
909
910#[inline]
915fn should_include_expression(
916 result: &crate::eval::EvalResult,
917 config: &GenConfig,
918 complexity: u32,
919 contains_x: bool,
920) -> bool {
921 result.value.is_finite()
922 && result.value.abs() <= MAX_GENERATED_VALUE
923 && result.num_type >= config.min_num_type
924 && if contains_x {
925 config.generate_lhs && complexity <= config.max_lhs_complexity
926 } else {
927 config.generate_rhs && complexity <= config.max_rhs_complexity
928 }
929}
930
931#[inline]
937fn get_max_complexity(config: &GenConfig, contains_x: bool) -> u32 {
938 if contains_x {
939 config.max_lhs_complexity
940 } else {
941 std::cmp::max(config.max_lhs_complexity, config.max_rhs_complexity)
944 }
945}
946
947fn rhs_only_config(config: &GenConfig) -> GenConfig {
948 let mut rhs_config = config.clone();
949 rhs_config.generate_lhs = false;
950 rhs_config.generate_rhs = true;
951 if let Some(constants) = &config.rhs_constants {
952 rhs_config.constants = constants.clone();
953 }
954 if let Some(unary_ops) = &config.rhs_unary_ops {
955 rhs_config.unary_ops = unary_ops.clone();
956 }
957 if let Some(binary_ops) = &config.rhs_binary_ops {
958 rhs_config.binary_ops = binary_ops.clone();
959 }
960 if let Some(rhs_symbol_max_counts) = &config.rhs_symbol_max_counts {
961 rhs_config.symbol_max_counts = rhs_symbol_max_counts.clone();
962 }
963 rhs_config
964}
965
966#[inline]
967fn exceeds_symbol_limit(config: &GenConfig, current: &Expression, sym: Symbol) -> bool {
968 config
969 .symbol_max_counts
970 .get(&sym)
971 .is_some_and(|&max| current.count_symbol(sym) >= max)
972}
973
974fn generate_recursive_streaming(
979 config: &GenConfig,
980 target: f64,
981 eval_context: EvalContext<'_>,
982 current: &mut Expression,
983 stack_depth: usize,
984 callbacks: &mut StreamingCallbacks,
985) -> bool {
986 if stack_depth == 1 && !current.is_empty() {
988 match evaluate_fast_with_context(current, target, &eval_context) {
990 Ok(result) => {
991 if should_include_expression(
993 &result,
994 config,
995 current.complexity(),
996 current.contains_x(),
997 ) {
998 let expr = current.clone();
999 let eval_expr =
1000 EvaluatedExpr::new(expr, result.value, result.derivative, result.num_type);
1001
1002 let should_continue = if current.contains_x() {
1004 (callbacks.on_lhs)(&eval_expr)
1005 } else {
1006 (callbacks.on_rhs)(&eval_expr)
1007 };
1008 if !should_continue {
1009 return false;
1010 }
1011 }
1012 }
1013 Err(e) => {
1014 if config.show_pruned_arith {
1016 eprintln!(
1017 " [pruned arith] expression=\"{}\" reason={:?}",
1018 current.to_postfix(),
1019 e
1020 );
1021 }
1022 }
1023 }
1024 }
1025
1026 if current.len() >= config.max_length {
1028 return true;
1029 }
1030
1031 let max_complexity = get_max_complexity(config, current.contains_x());
1033
1034 if current.complexity() >= max_complexity {
1035 return true;
1036 }
1037
1038 let min_remaining = min_complexity_to_complete(stack_depth, config);
1040 if current.complexity() + min_remaining > max_complexity {
1041 return true;
1042 }
1043
1044 for &sym in &config.constants {
1048 let sym_weight = config.symbol_table.weight(sym);
1049 if current.complexity() + sym_weight > max_complexity {
1050 continue;
1051 }
1052 if exceeds_symbol_limit(config, current, sym) {
1053 continue;
1054 }
1055
1056 if sym == Symbol::X && !config.generate_lhs {
1058 continue;
1059 }
1060
1061 current.push_with_table(sym, &config.symbol_table);
1062 if !generate_recursive_streaming(
1063 config,
1064 target,
1065 eval_context,
1066 current,
1067 stack_depth + 1,
1068 callbacks,
1069 ) {
1070 current.pop_with_table(&config.symbol_table);
1071 return false;
1072 }
1073 current.pop_with_table(&config.symbol_table);
1074 }
1075
1076 if config.generate_lhs && !config.constants.contains(&Symbol::X) {
1078 let sym = Symbol::X;
1079 let sym_weight = config.symbol_table.weight(sym);
1080 if current.complexity() + sym_weight <= max_complexity
1081 && !exceeds_symbol_limit(config, current, sym)
1082 {
1083 current.push_with_table(sym, &config.symbol_table);
1084 if !generate_recursive_streaming(
1085 config,
1086 target,
1087 eval_context,
1088 current,
1089 stack_depth + 1,
1090 callbacks,
1091 ) {
1092 current.pop_with_table(&config.symbol_table);
1093 return false;
1094 }
1095 current.pop_with_table(&config.symbol_table);
1096 }
1097 }
1098
1099 if stack_depth >= 1 {
1101 for &sym in &config.unary_ops {
1102 let sym_weight = config.symbol_table.weight(sym);
1103 if current.complexity() + sym_weight > max_complexity {
1104 continue;
1105 }
1106 if exceeds_symbol_limit(config, current, sym) {
1107 continue;
1108 }
1109
1110 if should_prune_unary(current, sym) {
1112 continue;
1113 }
1114
1115 current.push_with_table(sym, &config.symbol_table);
1116 if !generate_recursive_streaming(
1117 config,
1118 target,
1119 eval_context,
1120 current,
1121 stack_depth,
1122 callbacks,
1123 ) {
1124 current.pop_with_table(&config.symbol_table);
1125 return false;
1126 }
1127 current.pop_with_table(&config.symbol_table);
1128 }
1129 }
1130
1131 if stack_depth >= 2 {
1133 for &sym in &config.binary_ops {
1134 let sym_weight = config.symbol_table.weight(sym);
1135 if current.complexity() + sym_weight > max_complexity {
1136 continue;
1137 }
1138 if exceeds_symbol_limit(config, current, sym) {
1139 continue;
1140 }
1141
1142 if should_prune_binary(current, sym) {
1144 continue;
1145 }
1146
1147 current.push_with_table(sym, &config.symbol_table);
1148 if !generate_recursive_streaming(
1149 config,
1150 target,
1151 eval_context,
1152 current,
1153 stack_depth - 1,
1154 callbacks,
1155 ) {
1156 current.pop_with_table(&config.symbol_table);
1157 return false;
1158 }
1159 current.pop_with_table(&config.symbol_table);
1160 }
1161 }
1162
1163 true
1164}
1165
1166fn generate_recursive(
1168 config: &GenConfig,
1169 target: f64,
1170 eval_context: EvalContext<'_>,
1171 current: &mut Expression,
1172 stack_depth: usize,
1173 lhs_out: &mut Vec<EvaluatedExpr>,
1174 rhs_out: &mut Vec<EvaluatedExpr>,
1175) {
1176 if stack_depth == 1 && !current.is_empty() {
1178 match evaluate_fast_with_context(current, target, &eval_context) {
1180 Ok(result) => {
1181 if should_include_expression(
1183 &result,
1184 config,
1185 current.complexity(),
1186 current.contains_x(),
1187 ) {
1188 let expr = current.clone();
1189 let eval_expr =
1190 EvaluatedExpr::new(expr, result.value, result.derivative, result.num_type);
1191
1192 if current.contains_x() {
1194 lhs_out.push(eval_expr);
1195 } else {
1196 rhs_out.push(eval_expr);
1197 }
1198 }
1199 }
1200 Err(e) => {
1201 if config.show_pruned_arith {
1203 eprintln!(
1204 " [pruned arith] expression=\"{}\" reason={:?}",
1205 current.to_postfix(),
1206 e
1207 );
1208 }
1209 }
1210 }
1211 }
1212
1213 if current.len() >= config.max_length {
1215 return;
1216 }
1217
1218 let max_complexity = get_max_complexity(config, current.contains_x());
1220
1221 if current.complexity() >= max_complexity {
1222 return;
1223 }
1224
1225 let min_remaining = min_complexity_to_complete(stack_depth, config);
1227 if current.complexity() + min_remaining > max_complexity {
1228 return;
1229 }
1230
1231 for &sym in &config.constants {
1235 let sym_weight = config.symbol_table.weight(sym);
1236 if current.complexity() + sym_weight > max_complexity {
1237 continue;
1238 }
1239 if exceeds_symbol_limit(config, current, sym) {
1240 continue;
1241 }
1242
1243 if sym == Symbol::X && !config.generate_lhs {
1245 continue;
1246 }
1247
1248 current.push_with_table(sym, &config.symbol_table);
1249 generate_recursive(
1250 config,
1251 target,
1252 eval_context,
1253 current,
1254 stack_depth + 1,
1255 lhs_out,
1256 rhs_out,
1257 );
1258 current.pop_with_table(&config.symbol_table);
1259 }
1260
1261 if config.generate_lhs && !config.constants.contains(&Symbol::X) {
1263 let sym = Symbol::X;
1264 let sym_weight = config.symbol_table.weight(sym);
1265 if current.complexity() + sym_weight <= max_complexity
1266 && !exceeds_symbol_limit(config, current, sym)
1267 {
1268 current.push_with_table(sym, &config.symbol_table);
1269 generate_recursive(
1270 config,
1271 target,
1272 eval_context,
1273 current,
1274 stack_depth + 1,
1275 lhs_out,
1276 rhs_out,
1277 );
1278 current.pop_with_table(&config.symbol_table);
1279 }
1280 }
1281
1282 if stack_depth >= 1 {
1284 for &sym in &config.unary_ops {
1285 let sym_weight = config.symbol_table.weight(sym);
1286 if current.complexity() + sym_weight > max_complexity {
1287 continue;
1288 }
1289 if exceeds_symbol_limit(config, current, sym) {
1290 continue;
1291 }
1292
1293 if should_prune_unary(current, sym) {
1295 continue;
1296 }
1297
1298 current.push_with_table(sym, &config.symbol_table);
1299 generate_recursive(
1300 config,
1301 target,
1302 eval_context,
1303 current,
1304 stack_depth,
1305 lhs_out,
1306 rhs_out,
1307 );
1308 current.pop_with_table(&config.symbol_table);
1309 }
1310 }
1311
1312 if stack_depth >= 2 {
1314 for &sym in &config.binary_ops {
1315 let sym_weight = config.symbol_table.weight(sym);
1316 if current.complexity() + sym_weight > max_complexity {
1317 continue;
1318 }
1319 if exceeds_symbol_limit(config, current, sym) {
1320 continue;
1321 }
1322
1323 if should_prune_binary(current, sym) {
1325 continue;
1326 }
1327
1328 current.push_with_table(sym, &config.symbol_table);
1329 generate_recursive(
1330 config,
1331 target,
1332 eval_context,
1333 current,
1334 stack_depth - 1,
1335 lhs_out,
1336 rhs_out,
1337 );
1338 current.pop_with_table(&config.symbol_table);
1339 }
1340 }
1341}
1342
1343fn min_complexity_to_complete(stack_depth: usize, config: &GenConfig) -> u32 {
1345 if stack_depth <= 1 {
1346 return 0;
1347 }
1348
1349 let min_binary_weight = config
1351 .binary_ops
1352 .iter()
1353 .map(|s| config.symbol_table.weight(*s))
1354 .min()
1355 .unwrap_or(4);
1356
1357 ((stack_depth - 1) as u32) * min_binary_weight
1358}
1359
1360fn should_prune_unary(expr: &Expression, sym: Symbol) -> bool {
1362 let symbols = expr.symbols();
1363 if symbols.is_empty() {
1364 return false;
1365 }
1366
1367 let last = symbols[symbols.len() - 1];
1368
1369 use Symbol::*;
1370
1371 match (last, sym) {
1372 (Neg, Neg) => true,
1374 (Recip, Recip) => true,
1376 (Square, Sqrt) => true,
1378 (Sqrt, Square) => true,
1380 (Exp, Ln) => true,
1382 (Ln, Exp) => true,
1384
1385 (Sqrt, Recip) => true,
1388 (Square, Recip) => true,
1389 (Ln, Recip) => true,
1391 (Square, Square) => true,
1393 (Sqrt, Sqrt) => true,
1395 (Sub, Neg) => true,
1398
1399 (SinPi, SinPi) => true,
1403 (CosPi, CosPi) => true,
1404 (Exp, Exp) => true,
1410
1411 (Exp, LambertW) => true,
1413
1414 (Recip, LambertW) => true,
1417
1418 _ => false,
1419 }
1420}
1421
1422fn should_prune_binary(expr: &Expression, sym: Symbol) -> bool {
1424 let symbols = expr.symbols();
1425 if symbols.len() < 2 {
1426 return false;
1427 }
1428
1429 let last = symbols[symbols.len() - 1];
1430 let prev = symbols[symbols.len() - 2];
1431
1432 use Symbol::*;
1433
1434 match sym {
1435 Sub if is_same_subexpr(symbols, 2) => true,
1437 Sub if last == X && prev == X => true,
1439
1440 Div if is_same_subexpr(symbols, 2) => true,
1442 Div if last == X && prev == X => true,
1444 Div if last == One => true,
1446
1447 Add if is_same_subexpr(symbols, 2) => true,
1449 Add if last == Neg
1451 && symbols.len() >= 3
1452 && symbols[symbols.len() - 2] == X
1453 && prev == X =>
1454 {
1455 true
1456 }
1457
1458 Pow if prev == One => true,
1461 Pow if last == One => true,
1463
1464 Mul if last == One || prev == One => true,
1466
1467 Root if prev == One => true,
1470 Root if last == One => true,
1472 Root if last == Two => true,
1474
1475 Log if last == X && prev == X => true,
1477 Log if prev == One || last == One => true,
1479 Log if prev == E => true,
1481
1482 Add | Mul if prev > last && is_constant(last) && is_constant(prev) => true,
1484
1485 _ => false,
1486 }
1487}
1488
1489fn is_same_subexpr(symbols: &[Symbol], n: usize) -> bool {
1502 if n != 2 || symbols.len() < n * 2 {
1508 return false;
1509 }
1510
1511 fn subexpr_start(symbols: &[Symbol], end: usize) -> Option<usize> {
1514 let mut needed: i32 = 1;
1515 for i in (0..end).rev() {
1516 needed += match symbols[i].seft() {
1517 Seft::A => -1,
1518 Seft::B => 0,
1519 Seft::C => 1,
1520 };
1521 if needed == 0 {
1522 return Some(i);
1523 }
1524 }
1525 None
1526 }
1527
1528 let end = symbols.len();
1529 let Some(top_start) = subexpr_start(symbols, end) else {
1530 return false;
1531 };
1532 let Some(prev_start) = subexpr_start(symbols, top_start) else {
1533 return false;
1534 };
1535
1536 symbols[prev_start..top_start] == symbols[top_start..end]
1539}
1540
1541fn is_constant(sym: Symbol) -> bool {
1543 matches!(sym.seft(), Seft::A) && sym != Symbol::X
1544}
1545
1546#[cfg(feature = "parallel")]
1548pub fn generate_all_parallel(config: &GenConfig, target: f64) -> GeneratedExprs {
1549 generate_all_parallel_with_context(
1550 config,
1551 target,
1552 &EvalContext::from_slices(&config.user_constants, &config.user_functions),
1553 )
1554}
1555
1556#[cfg(feature = "parallel")]
1558pub fn generate_all_parallel_with_context(
1559 config: &GenConfig,
1560 target: f64,
1561 eval_context: &EvalContext<'_>,
1562) -> GeneratedExprs {
1563 use rayon::prelude::*;
1564
1565 if has_rhs_symbol_overrides(config) {
1567 return generate_all_with_context(config, target, eval_context);
1568 }
1569
1570 let mut prefixes: Vec<(Expression, usize)> = Vec::new();
1573 let mut immediate_results_lhs = Vec::new();
1574 let mut immediate_results_rhs = Vec::new();
1575
1576 let first_symbols: Vec<Symbol> = config
1577 .constants
1578 .iter()
1579 .copied()
1580 .chain(
1581 if config.generate_lhs && !config.constants.contains(&Symbol::X) {
1582 Some(Symbol::X)
1583 } else {
1584 None
1585 },
1586 )
1587 .filter(|&sym| {
1588 config
1589 .symbol_max_counts
1590 .get(&sym)
1591 .is_none_or(|&max| max > 0)
1592 })
1593 .collect();
1594
1595 for sym1 in first_symbols {
1596 let mut expr1 = Expression::new();
1597 expr1.push_with_table(sym1, &config.symbol_table);
1598
1599 let max_complexity = if expr1.contains_x() {
1600 config.max_lhs_complexity
1601 } else {
1602 std::cmp::max(config.max_lhs_complexity, config.max_rhs_complexity)
1603 };
1604
1605 if expr1.complexity() > max_complexity {
1606 continue;
1607 }
1608
1609 if let Ok(result) = evaluate_fast_with_context(&expr1, target, eval_context) {
1611 if result.value.is_finite()
1612 && result.value.abs() <= MAX_GENERATED_VALUE
1613 && result.num_type >= config.min_num_type
1614 {
1615 let eval_expr = EvaluatedExpr::new(
1616 expr1.clone(),
1617 result.value,
1618 result.derivative,
1619 result.num_type,
1620 );
1621
1622 if expr1.contains_x() {
1623 if config.generate_lhs && expr1.complexity() <= config.max_lhs_complexity {
1624 immediate_results_lhs.push(eval_expr);
1625 }
1626 } else if config.generate_rhs && expr1.complexity() <= config.max_rhs_complexity {
1627 immediate_results_rhs.push(eval_expr);
1628 }
1629 }
1630 }
1631
1632 if expr1.len() >= config.max_length {
1633 continue;
1634 }
1635
1636 let mut next_constants = config.constants.clone();
1640 if config.generate_lhs && !next_constants.contains(&Symbol::X) {
1641 next_constants.push(Symbol::X);
1642 }
1643
1644 for &sym2 in &next_constants {
1645 let sym2_weight = config.symbol_table.weight(sym2);
1646 let next_max = if expr1.contains_x() || sym2 == Symbol::X {
1647 config.max_lhs_complexity
1648 } else {
1649 std::cmp::max(config.max_lhs_complexity, config.max_rhs_complexity)
1650 };
1651
1652 if expr1.complexity() + sym2_weight <= next_max
1653 && !exceeds_symbol_limit(config, &expr1, sym2)
1654 {
1655 let mut expr2 = expr1.clone();
1656 expr2.push_with_table(sym2, &config.symbol_table);
1657 let min_remaining = min_complexity_to_complete(2, config);
1659 if expr2.complexity() + min_remaining <= next_max {
1660 prefixes.push((expr2, 2));
1661 }
1662 }
1663 }
1664
1665 for &sym2 in &config.unary_ops {
1667 let sym2_weight = config.symbol_table.weight(sym2);
1668 if expr1.complexity() + sym2_weight <= max_complexity
1669 && !exceeds_symbol_limit(config, &expr1, sym2)
1670 && !should_prune_unary(&expr1, sym2)
1671 {
1672 let mut expr2 = expr1.clone();
1673 expr2.push_with_table(sym2, &config.symbol_table);
1674 let min_remaining = min_complexity_to_complete(1, config);
1675 if expr2.complexity() + min_remaining <= max_complexity {
1676 prefixes.push((expr2, 1));
1677 }
1678 }
1679 }
1680 }
1681
1682 let results: Vec<(Vec<EvaluatedExpr>, Vec<EvaluatedExpr>)> = prefixes
1683 .into_par_iter()
1684 .map(|(mut expr, depth)| {
1685 let mut lhs = Vec::new();
1686 let mut rhs = Vec::new();
1687 generate_recursive(
1688 config,
1689 target,
1690 *eval_context,
1691 &mut expr,
1692 depth,
1693 &mut lhs,
1694 &mut rhs,
1695 );
1696 (lhs, rhs)
1697 })
1698 .collect();
1699
1700 let mut lhs_raw = immediate_results_lhs;
1702 let mut rhs_raw = immediate_results_rhs;
1703 for (lhs, rhs) in results {
1704 lhs_raw.extend(lhs);
1705 rhs_raw.extend(rhs);
1706 }
1707
1708 GeneratedExprs {
1710 lhs: dedupe_lhs_expressions(lhs_raw),
1711 rhs: dedupe_rhs_expressions(rhs_raw),
1712 }
1713}
1714
1715#[cfg(test)]
1716mod tests {
1717 use super::*;
1718 #[cfg(not(target_arch = "wasm32"))]
1719 use proptest::prelude::*;
1720
1721 fn fast_test_config() -> GenConfig {
1723 GenConfig {
1724 max_lhs_complexity: 20,
1725 max_rhs_complexity: 20,
1726 max_length: 8,
1727 constants: vec![
1728 Symbol::One,
1729 Symbol::Two,
1730 Symbol::Three,
1731 Symbol::Four,
1732 Symbol::Five,
1733 Symbol::Pi,
1734 Symbol::E,
1735 ],
1736 unary_ops: vec![Symbol::Neg, Symbol::Recip, Symbol::Square, Symbol::Sqrt],
1737 binary_ops: vec![Symbol::Add, Symbol::Sub, Symbol::Mul, Symbol::Div],
1738 rhs_constants: None,
1739 rhs_unary_ops: None,
1740 rhs_binary_ops: None,
1741 symbol_max_counts: HashMap::new(),
1742 rhs_symbol_max_counts: None,
1743 min_num_type: NumType::Transcendental,
1744 generate_lhs: true,
1745 generate_rhs: true,
1746 user_constants: Vec::new(),
1747 user_functions: Vec::new(),
1748 show_pruned_arith: false,
1749 symbol_table: Arc::new(SymbolTable::new()),
1750 }
1751 }
1752
1753 fn expr_from_postfix(s: &str) -> Expression {
1754 Expression::parse(s).expect("valid expression")
1755 }
1756
1757 fn stub_eval_expr(postfix: &str, value: f64, derivative: f64) -> EvaluatedExpr {
1758 EvaluatedExpr {
1759 expr: expr_from_postfix(postfix),
1760 value,
1761 derivative,
1762 num_type: NumType::Transcendental,
1763 }
1764 }
1765
1766 #[test]
1767 fn test_generate_simple() {
1768 let mut config = fast_test_config();
1769 config.generate_lhs = false; let result = generate_all(&config, 1.0);
1772
1773 assert!(!result.rhs.is_empty());
1775
1776 for expr in &result.rhs {
1778 assert!(!expr.expr.contains_x());
1779 }
1780 }
1781
1782 #[test]
1783 fn test_generate_lhs() {
1784 let mut config = fast_test_config();
1785 config.generate_rhs = false;
1786
1787 let result = generate_all(&config, 2.0);
1788
1789 assert!(!result.lhs.is_empty());
1791 for expr in &result.lhs {
1792 assert!(expr.expr.contains_x());
1793 }
1794 }
1795
1796 #[test]
1797 fn test_complexity_limit() {
1798 let config = fast_test_config();
1799
1800 let result = generate_all(&config, 1.0);
1801
1802 for expr in &result.rhs {
1803 assert!(expr.expr.complexity() <= config.max_rhs_complexity);
1804 }
1805 for expr in &result.lhs {
1806 assert!(expr.expr.complexity() <= config.max_lhs_complexity);
1807 }
1808 }
1809
1810 #[test]
1811 fn test_generate_all_with_limit_aborts_when_exceeded() {
1812 let mut config = fast_test_config();
1815 config.max_lhs_complexity = 30;
1816 config.max_rhs_complexity = 30;
1817
1818 let unlimited = generate_all(&config, 2.5);
1820 let total_unlimited = unlimited.lhs.len() + unlimited.rhs.len();
1821
1822 assert!(
1824 total_unlimited > 10,
1825 "Test config should generate >10 expressions"
1826 );
1827
1828 let limit = total_unlimited / 2; let result = generate_all_with_limit(&config, 2.5, limit);
1831
1832 assert!(
1833 result.is_none(),
1834 "generate_all_with_limit should return None when limit ({}) is exceeded (actual: {})",
1835 limit,
1836 total_unlimited
1837 );
1838 }
1839
1840 #[test]
1841 fn test_generate_all_with_limit_succeeds_when_within_limit() {
1842 let mut config = fast_test_config();
1844 config.max_lhs_complexity = 30;
1845 config.max_rhs_complexity = 30;
1846
1847 let result = generate_all_with_limit(&config, 2.5, 10_000);
1849
1850 assert!(
1851 result.is_some(),
1852 "generate_all_with_limit should return Some when limit is not exceeded"
1853 );
1854
1855 let generated = result.unwrap();
1856 assert!(!generated.lhs.is_empty() || !generated.rhs.is_empty());
1858 }
1859
1860 #[test]
1861 fn test_count_expressions_with_limit_matches_generated_total() {
1862 let mut config = fast_test_config();
1863 config.max_lhs_complexity = 30;
1864 config.max_rhs_complexity = 30;
1865
1866 let context = EvalContext::from_slices(&config.user_constants, &config.user_functions);
1867 let total_generated = std::cell::Cell::new(0usize);
1868 let mut callbacks = StreamingCallbacks {
1869 on_lhs: &mut |_expr| {
1870 total_generated.set(total_generated.get() + 1);
1871 true
1872 },
1873 on_rhs: &mut |_expr| {
1874 total_generated.set(total_generated.get() + 1);
1875 true
1876 },
1877 };
1878 generate_streaming_with_context(&config, 2.5, &context, &mut callbacks);
1879 let counted = count_expressions_with_limit_and_context(
1880 &config,
1881 2.5,
1882 &context,
1883 total_generated.get() + 1,
1884 );
1885
1886 assert_eq!(counted, Some(total_generated.get()));
1887 }
1888
1889 #[test]
1890 fn test_count_expressions_with_limit_returns_none_when_exceeded() {
1891 let mut config = fast_test_config();
1892 config.max_lhs_complexity = 30;
1893 config.max_rhs_complexity = 30;
1894
1895 let context = EvalContext::from_slices(&config.user_constants, &config.user_functions);
1896 let total_generated = std::cell::Cell::new(0usize);
1897 let mut callbacks = StreamingCallbacks {
1898 on_lhs: &mut |_expr| {
1899 total_generated.set(total_generated.get() + 1);
1900 true
1901 },
1902 on_rhs: &mut |_expr| {
1903 total_generated.set(total_generated.get() + 1);
1904 true
1905 },
1906 };
1907 generate_streaming_with_context(&config, 2.5, &context, &mut callbacks);
1908 let counted = count_expressions_with_limit_and_context(
1909 &config,
1910 2.5,
1911 &context,
1912 total_generated.get() / 2,
1913 );
1914
1915 assert_eq!(counted, None);
1916 }
1917
1918 #[test]
1921 fn test_is_same_subexpr_detects_identical_operands() {
1922 use crate::symbol::Symbol::*;
1923
1924 let same = [Two, Three, Add, Two, Three, Add];
1926 assert!(is_same_subexpr(&same, 2));
1927
1928 let diff = [Two, Three, Add, Two, Four, Add];
1930 assert!(!is_same_subexpr(&diff, 2));
1931
1932 let unequal = [Two, Three, Add, Five];
1934 assert!(!is_same_subexpr(&unequal, 2));
1935
1936 let nested = [Two, Sqrt, Neg, Two, Sqrt, Neg];
1938 assert!(is_same_subexpr(&nested, 2));
1939
1940 let short = [Two, Two];
1942 assert!(!is_same_subexpr(&short, 2));
1943
1944 let triple = [Two, Three, Add, Two, Three, Add, Two, Three, Add];
1946 assert!(!is_same_subexpr(&triple, 3));
1947 }
1948
1949 #[test]
1950 fn test_quantize_value_signed_boundary_symmetry_around_zero() {
1951 let below = 0.49 / QUANTIZE_SCALE;
1952 let above = 0.51 / QUANTIZE_SCALE;
1953
1954 assert_eq!(quantize_value(below), 0);
1955 assert_eq!(quantize_value(-below), 0);
1956 assert_eq!(quantize_value(above), 1);
1957 assert_eq!(quantize_value(-above), -1);
1958 }
1959
1960 #[test]
1961 fn test_dedupe_rhs_keeps_simplest_expression_within_quantized_bucket() {
1962 let base = 2.0;
1963 let rhs = dedupe_rhs_expressions(vec![
1964 stub_eval_expr("11+", base + 0.49 / QUANTIZE_SCALE, 0.0),
1965 stub_eval_expr("2", base, 0.0),
1966 ]);
1967
1968 assert_eq!(rhs.len(), 1);
1969 assert_eq!(rhs[0].expr.to_postfix(), "2");
1970 }
1971
1972 #[test]
1973 fn test_dedupe_lhs_preserves_adjacent_derivative_buckets() {
1974 let lhs = dedupe_lhs_expressions(vec![
1975 stub_eval_expr("x", 1.0, 1.0),
1976 stub_eval_expr("x1+", 1.0, 1.0 + 0.51 / QUANTIZE_SCALE),
1977 ]);
1978
1979 assert_eq!(lhs.len(), 2);
1980 }
1981
1982 #[cfg(not(target_arch = "wasm32"))]
1983 proptest! {
1984 #[test]
1985 fn test_quantize_value_collapses_values_within_same_bucket(bucket in -1_000_000i64..1_000_000i64) {
1986 let base = bucket as f64 / QUANTIZE_SCALE;
1987 prop_assert_eq!(quantize_value(base + 0.49 / QUANTIZE_SCALE), bucket);
1988 prop_assert_eq!(quantize_value(base - 0.49 / QUANTIZE_SCALE), bucket);
1989 }
1990
1991 #[test]
1992 fn test_quantize_value_separates_adjacent_buckets(bucket in -1_000_000i64..1_000_000i64) {
1993 let base = bucket as f64 / QUANTIZE_SCALE;
1994 prop_assert_eq!(quantize_value(base + 0.51 / QUANTIZE_SCALE), bucket + 1);
1995 prop_assert_eq!(quantize_value(base - 0.51 / QUANTIZE_SCALE), bucket - 1);
1996 }
1997 }
1998
1999 #[test]
2000 fn test_constraints_default_allows_all() {
2001 let opts = ExpressionConstraintOptions::default();
2002
2003 let expr = expr_from_postfix("xp^"); assert!(
2006 expression_respects_constraints(&expr, opts),
2007 "x^pi should be allowed with default options"
2008 );
2009
2010 let expr = expr_from_postfix("eS"); assert!(
2013 expression_respects_constraints(&expr, opts),
2014 "sinpi(e) should be allowed with default options"
2015 );
2016 }
2017
2018 #[test]
2019 fn test_constraints_rational_exponents_rejects_transcendental() {
2020 let opts = ExpressionConstraintOptions {
2021 rational_exponents: true,
2022 ..Default::default()
2023 };
2024
2025 let expr = expr_from_postfix("xp^");
2027 assert!(
2028 !expression_respects_constraints(&expr, opts),
2029 "x^pi should be rejected with rational_exponents=true"
2030 );
2031
2032 let expr = expr_from_postfix("xe^");
2034 assert!(
2035 !expression_respects_constraints(&expr, opts),
2036 "x^e should be rejected with rational_exponents=true"
2037 );
2038 }
2039
2040 #[test]
2041 fn test_constraints_rational_exponents_allows_integer() {
2042 let opts = ExpressionConstraintOptions {
2043 rational_exponents: true,
2044 ..Default::default()
2045 };
2046
2047 let expr = expr_from_postfix("x2^");
2049 assert!(
2050 expression_respects_constraints(&expr, opts),
2051 "x^2 should be allowed with rational_exponents=true"
2052 );
2053
2054 let expr = expr_from_postfix("x1^");
2056 assert!(
2057 expression_respects_constraints(&expr, opts),
2058 "x^1 should be allowed with rational_exponents=true"
2059 );
2060 }
2061
2062 #[test]
2063 fn test_constraints_rational_trig_args_rejects_irrational() {
2064 let opts = ExpressionConstraintOptions {
2065 rational_trig_args: true,
2066 ..Default::default()
2067 };
2068
2069 let expr = expr_from_postfix("eS"); assert!(
2072 !expression_respects_constraints(&expr, opts),
2073 "sinpi(e) should be rejected with rational_trig_args=true"
2074 );
2075
2076 let expr = expr_from_postfix("pS"); assert!(
2079 !expression_respects_constraints(&expr, opts),
2080 "sinpi(pi) should be rejected with rational_trig_args=true"
2081 );
2082 }
2083
2084 #[test]
2085 fn test_constraints_rational_trig_args_allows_rational() {
2086 let opts = ExpressionConstraintOptions {
2087 rational_trig_args: true,
2088 ..Default::default()
2089 };
2090
2091 let expr = expr_from_postfix("1S"); assert!(
2094 expression_respects_constraints(&expr, opts),
2095 "sinpi(1) should be allowed with rational_trig_args=true"
2096 );
2097
2098 let expr = expr_from_postfix("2S");
2100 assert!(
2101 expression_respects_constraints(&expr, opts),
2102 "sinpi(2) should be allowed with rational_trig_args=true"
2103 );
2104 }
2105
2106 #[test]
2107 fn test_constraints_rational_trig_args_rejects_x() {
2108 let opts = ExpressionConstraintOptions {
2109 rational_trig_args: true,
2110 ..Default::default()
2111 };
2112
2113 let expr = expr_from_postfix("xS"); assert!(
2116 !expression_respects_constraints(&expr, opts),
2117 "sinpi(x) should be rejected with rational_trig_args=true"
2118 );
2119 }
2120
2121 #[test]
2122 fn test_constraints_max_trig_cycles() {
2123 let opts = ExpressionConstraintOptions {
2124 max_trig_cycles: Some(2),
2125 ..Default::default()
2126 };
2127
2128 let expr = expr_from_postfix("xS"); assert!(
2131 expression_respects_constraints(&expr, opts),
2132 "1 trig op should pass with max=2"
2133 );
2134
2135 let expr = expr_from_postfix("xCS");
2138 assert!(
2139 expression_respects_constraints(&expr, opts),
2140 "2 trig ops should pass with max=2"
2141 );
2142
2143 let expr = expr_from_postfix("xTCS");
2146 assert!(
2147 !expression_respects_constraints(&expr, opts),
2148 "3 trig ops should fail with max=2"
2149 );
2150 }
2151
2152 #[test]
2153 fn test_constraints_max_trig_cycles_none_unlimited() {
2154 let opts = ExpressionConstraintOptions {
2155 max_trig_cycles: None, ..Default::default()
2157 };
2158
2159 let expr = expr_from_postfix("xTCSTCS");
2162 assert!(
2163 expression_respects_constraints(&expr, opts),
2164 "Unlimited trig should pass any depth"
2165 );
2166 }
2167
2168 #[test]
2169 fn test_constraints_combined() {
2170 let opts = ExpressionConstraintOptions {
2171 rational_exponents: true,
2172 rational_trig_args: true,
2173 max_trig_cycles: Some(1),
2174 ..Default::default()
2175 };
2176
2177 let expr = expr_from_postfix("x2^1S+"); assert!(
2180 expression_respects_constraints(&expr, opts),
2181 "x^2 + sinpi(1) should pass all constraints"
2182 );
2183
2184 let expr = expr_from_postfix("xp^");
2186 assert!(
2187 !expression_respects_constraints(&expr, opts),
2188 "x^pi should fail rational_exponents"
2189 );
2190
2191 let expr = expr_from_postfix("xS"); assert!(
2194 !expression_respects_constraints(&expr, opts),
2195 "sinpi(x) should fail rational_trig_args"
2196 );
2197
2198 let expr = expr_from_postfix("1CS"); assert!(
2201 !expression_respects_constraints(&expr, opts),
2202 "double trig should fail max_trig_cycles=1"
2203 );
2204 }
2205
2206 #[test]
2207 fn test_constraints_malformed_expression() {
2208 let opts = ExpressionConstraintOptions::default();
2209
2210 let expr = Expression::from_symbols(&[crate::symbol::Symbol::Add]); assert!(
2213 !expression_respects_constraints(&expr, opts),
2214 "Malformed expression should return false"
2215 );
2216
2217 let expr =
2219 Expression::from_symbols(&[crate::symbol::Symbol::One, crate::symbol::Symbol::Two]);
2220 assert!(
2221 !expression_respects_constraints(&expr, opts),
2222 "Incomplete expression should return false"
2223 );
2224 }
2225
2226 #[test]
2227 fn test_constraints_user_constant_types() {
2228 let mut user_types = [NumType::Transcendental; 16];
2230 user_types[0] = NumType::Integer;
2231
2232 let opts = ExpressionConstraintOptions {
2233 rational_exponents: true,
2234 user_constant_types: user_types,
2235 ..Default::default()
2236 };
2237
2238 assert_eq!(opts.user_constant_types[0], NumType::Integer);
2242 }
2243}