#![cfg(not(target_arch = "wasm32"))]
#![allow(clippy::field_reassign_with_default)]
use ries_rs::eval;
use ries_rs::expr::Expression;
use ries_rs::gen::{generate_all, GenConfig};
use ries_rs::profile::UserConstant;
use ries_rs::search::{
search_adaptive, search_streaming_with_config, search_with_stats_and_config,
search_with_stats_and_options, ExprDatabase, SearchConfig,
};
use ries_rs::symbol::{NumType, Symbol};
use ries_rs::udf::UserFunction;
use ries_rs::SymbolTable;
use std::collections::HashMap;
use std::sync::Arc;
mod common;
fn fast_config() -> GenConfig {
GenConfig {
max_lhs_complexity: 40,
max_rhs_complexity: 40,
max_length: 10,
constants: vec![
Symbol::One,
Symbol::Two,
Symbol::Three,
Symbol::Four,
Symbol::Five,
Symbol::Six,
Symbol::Seven,
Symbol::Eight,
Symbol::Nine,
Symbol::Pi,
Symbol::E,
],
unary_ops: vec![Symbol::Neg, Symbol::Recip, Symbol::Square, Symbol::Sqrt],
binary_ops: vec![Symbol::Add, Symbol::Sub, Symbol::Mul, Symbol::Div],
rhs_constants: None,
rhs_unary_ops: None,
rhs_binary_ops: None,
symbol_max_counts: HashMap::new(),
rhs_symbol_max_counts: None,
min_num_type: NumType::Transcendental,
generate_lhs: true,
generate_rhs: true,
user_constants: Vec::new(),
user_functions: Vec::new(),
show_pruned_arith: false,
symbol_table: Arc::new(SymbolTable::new()),
}
}
#[test]
fn test_basic_generation() {
let config = fast_config();
let generated = generate_all(&config, 2.5);
assert!(!generated.lhs.is_empty(), "Should generate LHS expressions");
assert!(!generated.rhs.is_empty(), "Should generate RHS expressions");
for lhs in &generated.lhs {
assert!(lhs.expr.contains_x(), "LHS should contain x");
}
for rhs in &generated.rhs {
assert!(!rhs.expr.contains_x(), "RHS should not contain x");
}
}
#[test]
fn test_complexity_limits() {
let config = fast_config();
let generated = generate_all(&config, 1.0);
for lhs in &generated.lhs {
assert!(
lhs.expr.complexity() <= 40,
"LHS complexity {} exceeds limit",
lhs.expr.complexity()
);
}
for rhs in &generated.rhs {
assert!(
rhs.expr.complexity() <= 40,
"RHS complexity {} exceeds limit",
rhs.expr.complexity()
);
}
}
#[test]
fn test_symbol_count_limits_are_enforced() {
let mut config = fast_config();
config.symbol_max_counts.insert(Symbol::X, 1);
config.symbol_max_counts.insert(Symbol::Add, 1);
let generated = generate_all(&config, 2.5);
for lhs in &generated.lhs {
let x_count = lhs
.expr
.symbols()
.iter()
.filter(|&&s| s == Symbol::X)
.count();
let add_count = lhs
.expr
.symbols()
.iter()
.filter(|&&s| s == Symbol::Add)
.count();
assert!(
x_count <= 1,
"expected at most one x in LHS, got {}",
x_count
);
assert!(
add_count <= 1,
"expected at most one + in LHS, got {}",
add_count
);
}
}
#[test]
fn test_expr_database() {
let config = fast_config();
let generated = generate_all(&config, 2.5);
assert!(!generated.rhs.is_empty(), "Should generate RHS expressions");
let mut db = ExprDatabase::new();
db.insert_rhs(generated.rhs);
let results = db.range(2.4, 2.6);
assert!(!results.is_empty(), "Should find expressions near 2.5");
}
#[test]
fn test_pi_generation() {
let config = fast_config();
let generated = generate_all(&config, std::f64::consts::PI);
let has_pi = generated.rhs.iter().any(|e| e.expr.to_postfix() == "p");
assert!(has_pi, "Should generate π as RHS");
}
#[test]
fn test_basic_operators() {
let config = fast_config();
let generated = generate_all(&config, 5.0);
let postfixes: Vec<_> = generated.rhs.iter().map(|e| e.expr.to_postfix()).collect();
assert!(postfixes.iter().any(|p| p == "1"), "Should have 1");
assert!(postfixes.iter().any(|p| p == "2"), "Should have 2");
assert!(postfixes.iter().any(|p| p == "5"), "Should have 5");
}
#[test]
fn test_expression_evaluation() {
let expr = Expression::parse("xs").unwrap();
let result = eval::evaluate(&expr, 3.0).unwrap();
assert!((result.value - 9.0).abs() < 1e-10);
assert!((result.derivative - 6.0).abs() < 1e-10);
let expr = Expression::parse("xq").unwrap();
let result = eval::evaluate(&expr, 4.0).unwrap();
assert!((result.value - 2.0).abs() < 1e-10);
assert!((result.derivative - 0.25).abs() < 1e-10);
let expr = Expression::parse("2x*").unwrap();
let result = eval::evaluate(&expr, 3.0).unwrap();
assert!((result.value - 6.0).abs() < 1e-10);
assert!((result.derivative - 2.0).abs() < 1e-10);
}
#[test]
fn test_exact_match_finding() {
let config = fast_config();
let generated = generate_all(&config, 2.0);
let has_x_equals_2 = generated.lhs.iter().any(|e| {
if let Ok(result) = eval::evaluate(&e.expr, 2.0) {
(result.value - 2.0).abs() < 1e-10
} else {
false
}
});
assert!(has_x_equals_2, "Should find x = 2 for target 2");
}
#[test]
fn test_find_2x_equals_5() {
let config = fast_config();
let (matches, _stats) = search_with_stats_and_options(2.5, &config, 50, false, None);
let has_2x_5 = matches.iter().any(|m| {
let lhs_is_2x = m.lhs.expr.to_postfix() == "2x*";
let rhs_is_5 = m.rhs.expr.to_postfix() == "5";
let is_exact = m.error.abs() < 1e-14;
lhs_is_2x && rhs_is_5 && is_exact
});
assert!(has_2x_5, "Should find 2x = 5 as exact match for target 2.5");
}
#[test]
fn test_find_reciprocal_relations() {
let config = fast_config();
let (matches, _stats) = search_with_stats_and_options(2.5, &config, 50, false, None);
assert!(!matches.is_empty(), "Should find matches for target 2.5");
let has_near_exact = matches.iter().any(|m| m.error.abs() < 1e-10);
assert!(
has_near_exact,
"Should find at least one near-exact match for target 2.5"
);
}
#[test]
fn test_batch_and_streaming_agree_on_top_match_for_pi() {
let config = fast_config();
let search_config = SearchConfig {
target: std::f64::consts::PI,
max_matches: 10,
user_constants: config.user_constants.clone(),
user_functions: config.user_functions.clone(),
..Default::default()
};
let (batch_matches, _batch_stats) = search_with_stats_and_config(&config, &search_config);
let (streaming_matches, _streaming_stats) =
search_streaming_with_config(&config, &search_config);
assert!(
!batch_matches.is_empty(),
"batch search should find matches"
);
assert!(
!streaming_matches.is_empty(),
"streaming search should find matches"
);
let batch_top = (
batch_matches[0].lhs.expr.to_postfix(),
batch_matches[0].rhs.expr.to_postfix(),
);
let streaming_top = (
streaming_matches[0].lhs.expr.to_postfix(),
streaming_matches[0].rhs.expr.to_postfix(),
);
assert_eq!(
batch_top, streaming_top,
"batch and streaming should agree on the top-ranked pi match"
);
}
#[test]
fn test_batch_and_streaming_agree_on_first_five_matches_for_pi() {
let config = fast_config();
let search_config = SearchConfig {
target: std::f64::consts::PI,
max_matches: 10,
user_constants: config.user_constants.clone(),
user_functions: config.user_functions.clone(),
..Default::default()
};
let (batch_matches, _batch_stats) = search_with_stats_and_config(&config, &search_config);
let (streaming_matches, _streaming_stats) =
search_streaming_with_config(&config, &search_config);
let batch_pairs: Vec<_> = batch_matches
.iter()
.take(5)
.map(|m| (m.lhs.expr.to_postfix(), m.rhs.expr.to_postfix()))
.collect();
let streaming_pairs: Vec<_> = streaming_matches
.iter()
.take(5)
.map(|m| (m.lhs.expr.to_postfix(), m.rhs.expr.to_postfix()))
.collect();
assert_eq!(
batch_pairs.len(),
5,
"expected at least five batch matches for pi"
);
assert_eq!(
streaming_pairs.len(),
5,
"expected at least five streaming matches for pi"
);
assert_eq!(
batch_pairs, streaming_pairs,
"batch and streaming should agree on the first five pi matches"
);
}
#[test]
fn test_search_stats_report_window_and_acceptance_metrics() {
let config = fast_config();
let search_config = SearchConfig {
target: std::f64::consts::PI,
max_matches: 10,
user_constants: config.user_constants.clone(),
user_functions: config.user_functions.clone(),
..Default::default()
};
let (_batch_matches, batch_stats) = search_with_stats_and_config(&config, &search_config);
let (_streaming_matches, streaming_stats) =
search_streaming_with_config(&config, &search_config);
for stats in [&batch_stats, &streaming_stats] {
assert!(
stats.lhs_tested > 0,
"expected LHS expressions to be tested"
);
assert!(
stats.candidate_window_total >= stats.candidates_tested,
"candidate windows should cover all tested pairs"
);
assert!(
stats.candidate_window_max > 0,
"expected at least one non-empty candidate window"
);
assert!(
stats.candidate_window_max_lhs_postfix.is_some(),
"expected max-window LHS details to be recorded"
);
assert!(
stats.candidate_window_max_lhs_value.is_some()
&& stats.candidate_window_max_lhs_derivative.is_some()
&& stats.candidate_window_max_lhs_complexity.is_some(),
"expected max-window numeric context to be recorded"
);
assert!(
stats.candidate_window_avg() > 0.0,
"expected a positive average candidate window width"
);
assert!(
(0.0..=1.0).contains(&stats.newton_success_rate()),
"newton success rate should be normalized"
);
assert!(
(0.0..=1.0).contains(&stats.pool_acceptance_rate()),
"pool acceptance rate should be normalized"
);
}
}
#[test]
fn test_find_golden_ratio() {
let mut config = fast_config();
config.constants.push(Symbol::Phi);
let phi = 1.618_033_988_749_895;
let (matches, _stats) = search_with_stats_and_options(phi, &config, 50, false, None);
assert!(
!matches.is_empty(),
"Should find some matches for golden ratio"
);
let best_error = matches
.iter()
.map(|m| m.error.abs())
.fold(f64::INFINITY, |a, b| a.min(b));
assert!(
best_error < 0.1,
"Best match error should be < 0.1 for phi, got {}",
best_error
);
}
#[test]
fn test_user_constant_in_search() {
let user_constants = vec![UserConstant {
weight: 4, name: "g".to_string(),
description: "test constant".to_string(),
value: 0.57721,
num_type: NumType::Transcendental,
}];
let mut config = fast_config();
config.user_constants = user_constants;
config.constants.push(Symbol::UserConstant0);
let (matches, _stats) = search_with_stats_and_options(0.57721, &config, 50, false, None);
let has_user_constant_match = matches.iter().any(|m| {
let lhs_is_x = m.lhs.expr.to_postfix() == "x";
let is_exact = m.error.abs() < 1e-10;
lhs_is_x
&& is_exact
&& m.rhs
.expr
.symbols()
.iter()
.any(|s| matches!(s, Symbol::UserConstant0))
});
assert!(
has_user_constant_match,
"Should find x = u0 as match for user constant value"
);
}
#[test]
fn test_batch_and_streaming_agree_on_user_constant_top_match() {
let user_constants = vec![UserConstant {
weight: 4,
name: "g".to_string(),
description: "test constant".to_string(),
value: 0.57721,
num_type: NumType::Transcendental,
}];
let mut config = fast_config();
config.user_constants = user_constants.clone();
config.constants.push(Symbol::UserConstant0);
let search_config = SearchConfig {
target: 0.57721,
max_matches: 10,
user_constants,
user_functions: config.user_functions.clone(),
..Default::default()
};
let (batch_matches, _batch_stats) = search_with_stats_and_config(&config, &search_config);
let (streaming_matches, _streaming_stats) =
search_streaming_with_config(&config, &search_config);
assert!(
!batch_matches.is_empty() && !streaming_matches.is_empty(),
"expected both search paths to find user-constant matches"
);
let batch_top = (
batch_matches[0].lhs.expr.to_postfix(),
batch_matches[0].rhs.expr.to_postfix(),
);
let streaming_top = (
streaming_matches[0].lhs.expr.to_postfix(),
streaming_matches[0].rhs.expr.to_postfix(),
);
assert_eq!(
batch_top, streaming_top,
"batch and streaming should agree on the top user-constant match"
);
}
#[test]
fn test_multiple_user_constants() {
let user_constants = vec![
UserConstant {
weight: 4,
name: "a".to_string(),
description: "constant a".to_string(),
value: 2.0,
num_type: NumType::Integer,
},
UserConstant {
weight: 4,
name: "b".to_string(),
description: "constant b".to_string(),
value: 3.0,
num_type: NumType::Integer,
},
];
let mut config = fast_config();
config.user_constants = user_constants.clone();
config.constants.push(Symbol::UserConstant0);
config.constants.push(Symbol::UserConstant1);
let generated = generate_all(&config, 2.5);
let has_value_2 = generated.rhs.iter().any(|e| (e.value - 2.0).abs() < 1e-10);
let has_value_3 = generated.rhs.iter().any(|e| (e.value - 3.0).abs() < 1e-10);
assert!(
has_value_2 || has_value_3,
"Should have RHS with values from user constants or matching standard constants"
);
}
#[test]
fn test_user_function_in_search() {
let udf = UserFunction::parse("4:sinh:hyperbolic sine:E|r-2/").unwrap();
let uc = UserConstant {
weight: 4,
name: "sinh2".to_string(),
description: "sinh(2)".to_string(),
value: 3.626_860_407_847_019,
num_type: NumType::Transcendental,
};
let config = GenConfig {
max_lhs_complexity: 20,
max_rhs_complexity: 20,
max_length: 6,
constants: vec![Symbol::One, Symbol::UserConstant0],
unary_ops: vec![Symbol::UserFunction0],
binary_ops: vec![],
rhs_constants: None,
rhs_unary_ops: None,
rhs_binary_ops: None,
symbol_max_counts: HashMap::new(),
rhs_symbol_max_counts: None,
min_num_type: NumType::Transcendental,
generate_lhs: true,
generate_rhs: true,
user_constants: vec![uc.clone()],
user_functions: vec![udf.clone()],
show_pruned_arith: false,
symbol_table: Arc::new(SymbolTable::from_parts(
&ries_rs::profile::Profile::new(),
&[uc],
&[udf],
)),
};
let (matches, _stats) = search_with_stats_and_options(2.0, &config, 50, false, None);
let has_udf_match = matches.iter().any(|m| {
m.error.abs() < 1e-10
&& m.lhs
.expr
.symbols()
.iter()
.any(|s| matches!(s, Symbol::UserFunction0))
&& m.rhs
.expr
.symbols()
.iter()
.any(|s| matches!(s, Symbol::UserConstant0))
});
assert!(
has_udf_match,
"Should find an exact match using UserFunction0 and UserConstant0"
);
}
#[test]
fn test_streaming_rhs_dedup_key_safe_for_large_values() {
use ries_rs::gen::quantize_value;
let large = 1e11_f64;
let safe_key = quantize_value(large);
assert_ne!(
safe_key,
i64::MAX,
"quantize_value must not overflow to i64::MAX for large finite values"
);
assert_eq!(
safe_key,
i64::MAX - 1,
"quantize_value returns i64::MAX-1 for large positive values"
);
let nan_key = quantize_value(f64::NAN);
assert_eq!(nan_key, i64::MAX, "NaN must map to i64::MAX sentinel");
assert_ne!(
safe_key, nan_key,
"large finite values must be distinct from NaN sentinel"
);
let buggy_key = (large * 1e8_f64).round() as i64;
assert_eq!(
buggy_key,
i64::MAX,
"demonstrates the bug: inline computation overflows to i64::MAX (same as NaN sentinel)"
);
}
#[test]
fn test_streaming_search_with_udf_produces_results() {
let udf = UserFunction::parse("4:sinh:hyperbolic sine:E|r-2/").unwrap();
let uc = UserConstant {
weight: 4,
name: "sinh2".to_string(),
description: "sinh(2)".to_string(),
value: 3.626_860_407_847_019,
num_type: NumType::Transcendental,
};
let config = GenConfig {
max_lhs_complexity: 20,
max_rhs_complexity: 20,
max_length: 6,
constants: vec![Symbol::One, Symbol::UserConstant0],
unary_ops: vec![Symbol::UserFunction0],
binary_ops: vec![],
rhs_constants: None,
rhs_unary_ops: None,
rhs_binary_ops: None,
symbol_max_counts: HashMap::new(),
rhs_symbol_max_counts: None,
min_num_type: NumType::Transcendental,
generate_lhs: true,
generate_rhs: true,
user_constants: vec![uc.clone()],
user_functions: vec![udf.clone()],
show_pruned_arith: false,
symbol_table: Arc::new(SymbolTable::from_parts(
&ries_rs::profile::Profile::new(),
&[uc],
&[udf],
)),
};
let (matches, _stats) = ries_rs::search::search_streaming(2.0, &config, 50, false, None);
assert!(
!matches.is_empty(),
"streaming search with UDF should produce at least one match"
);
}
#[test]
fn test_adaptive_search_produces_valid_matches() {
let config = fast_config();
let (matches, _stats) = search_with_stats_and_options(2.0, &config, 10, false, None);
assert!(
!matches.is_empty(),
"search should find matches for target 2.0"
);
let min_complexity = matches
.iter()
.map(|m| m.complexity)
.min()
.unwrap_or(u32::MAX);
assert!(
min_complexity <= 28,
"simplest match complexity should be low for target 2.0, got {min_complexity}"
);
}
#[test]
fn test_search_adaptive_calls_iterative_algorithm() {
use ries_rs::search::SearchConfig;
let gen_config = fast_config();
let search_config = SearchConfig {
target: 2.0,
max_matches: 10,
user_constants: gen_config.user_constants.clone(),
user_functions: gen_config.user_functions.clone(),
..Default::default()
};
let (adaptive_matches, adaptive_stats) = search_adaptive(&gen_config, &search_config, 0);
assert!(
!adaptive_matches.is_empty(),
"search_adaptive should find matches for target 2.0"
);
assert!(
adaptive_stats.lhs_count > 0,
"must report generated LHS expressions"
);
assert!(
adaptive_stats.rhs_count > 0,
"must report generated RHS expressions"
);
let target_level0: usize = 2000 * 4usize.pow(2);
let total = adaptive_stats.lhs_count + adaptive_stats.rhs_count;
assert!(
total <= target_level0 * 8,
"adaptive at level 0 generated {total} expressions but the target is ~{target_level0}; \
a large overshoot indicates the bounds-clamp bug is still present"
);
assert!(
total >= target_level0 / 8,
"adaptive at level 0 generated only {total} expressions (target ~{target_level0}); \
the iterative growth may not be running at all"
);
}
#[cfg(feature = "parallel")]
fn match_signatures(matches: &[ries_rs::search::Match]) -> Vec<(String, String, i64)> {
let mut signatures: Vec<(String, String, i64)> = matches
.iter()
.map(|m| {
(
m.lhs.expr.to_postfix(),
m.rhs.expr.to_postfix(),
(m.error / 1e-12).round() as i64,
)
})
.collect();
signatures.sort();
signatures
}
#[cfg(feature = "parallel")]
#[test]
fn test_parallel_matches_sequential_results() {
use ries_rs::search::search_parallel_with_stats_and_config;
let gen_config = fast_config();
let search_config = SearchConfig {
target: 2.0,
max_matches: 20,
user_constants: gen_config.user_constants.clone(),
user_functions: gen_config.user_functions.clone(),
..Default::default()
};
let (sequential, _) = search_with_stats_and_config(&gen_config, &search_config);
let (parallel, _) = search_parallel_with_stats_and_config(&gen_config, &search_config);
assert_eq!(
match_signatures(&sequential),
match_signatures(¶llel),
"parallel and sequential search should return the same match set"
);
}
#[cfg(feature = "parallel")]
#[test]
fn test_turbo_best_match_equals_sequential() {
use ries_rs::search::search_turbo_with_stats_and_config;
for target in [
std::f64::consts::SQRT_2,
2.0 * std::f64::consts::PI,
std::f64::consts::FRAC_PI_2,
3.301,
] {
let gen_config = fast_config();
let search_config = SearchConfig {
target,
max_matches: 20,
user_constants: gen_config.user_constants.clone(),
user_functions: gen_config.user_functions.clone(),
..Default::default()
};
let (sequential, _) = search_with_stats_and_config(&gen_config, &search_config);
let (turbo, _) = search_turbo_with_stats_and_config(&gen_config, &search_config);
assert!(
!sequential.is_empty() && !turbo.is_empty(),
"target {target}"
);
let seq_best = (
sequential[0].lhs.expr.to_postfix(),
sequential[0].rhs.expr.to_postfix(),
);
let turbo_best = (
turbo[0].lhs.expr.to_postfix(),
turbo[0].rhs.expr.to_postfix(),
);
assert_eq!(
seq_best, turbo_best,
"turbo and serial must agree on the best match for {target}"
);
}
}
#[cfg(feature = "parallel")]
#[test]
fn test_turbo_stop_at_exact_preserves_sequential_best_match() {
use ries_rs::search::search_turbo_with_stats_and_config;
let gen_config = fast_config();
let search_config = SearchConfig {
target: std::f64::consts::FRAC_PI_2,
max_matches: 20,
stop_at_exact: true,
user_constants: gen_config.user_constants.clone(),
user_functions: gen_config.user_functions.clone(),
..Default::default()
};
let (sequential, sequential_stats) = search_with_stats_and_config(&gen_config, &search_config);
let (turbo, turbo_stats) = search_turbo_with_stats_and_config(&gen_config, &search_config);
assert_eq!(
match_signatures(&sequential),
match_signatures(&turbo),
"turbo must preserve serial stopping semantics and results"
);
assert_eq!(sequential_stats.early_exit, turbo_stats.early_exit);
}
#[cfg(feature = "parallel")]
#[test]
fn test_turbo_stop_below_preserves_sequential_best_match() {
use ries_rs::search::search_turbo_with_stats_and_config;
let gen_config = fast_config();
let search_config = SearchConfig {
target: std::f64::consts::FRAC_PI_2,
max_matches: 20,
stop_below: Some(1e-10),
user_constants: gen_config.user_constants.clone(),
user_functions: gen_config.user_functions.clone(),
..Default::default()
};
let (sequential, sequential_stats) = search_with_stats_and_config(&gen_config, &search_config);
let (turbo, turbo_stats) = search_turbo_with_stats_and_config(&gen_config, &search_config);
assert_eq!(
match_signatures(&sequential),
match_signatures(&turbo),
"turbo must preserve serial threshold-stopping semantics and results"
);
assert_eq!(sequential_stats.early_exit, turbo_stats.early_exit);
}
#[cfg(feature = "parallel")]
#[test]
fn test_turbo_results_are_valid_refined_matches() {
use ries_rs::search::search_turbo_with_stats_and_config;
let gen_config = fast_config();
let search_config = SearchConfig {
target: 2.5,
max_matches: 20,
user_constants: gen_config.user_constants.clone(),
user_functions: gen_config.user_functions.clone(),
..Default::default()
};
let (turbo, _) = search_turbo_with_stats_and_config(&gen_config, &search_config);
assert!(!turbo.is_empty());
for m in &turbo {
assert!(
(m.error - (m.x_value - search_config.target)).abs() < 1e-9,
"turbo match error must be consistent with x_value"
);
assert!(m.error.abs() <= search_config.max_error + 1e-12);
}
}
#[cfg(feature = "parallel")]
#[test]
fn test_turbo_finds_known_exact_match() {
use ries_rs::search::search_turbo_with_stats_and_config;
let gen_config = fast_config();
let search_config = SearchConfig {
target: 2.0 * std::f64::consts::PI,
max_matches: 20,
user_constants: gen_config.user_constants.clone(),
user_functions: gen_config.user_functions.clone(),
..Default::default()
};
let (turbo, _) = search_turbo_with_stats_and_config(&gen_config, &search_config);
assert!(
turbo.iter().any(|m| m.error.abs() < 1e-14),
"turbo should find at least one exact match for 2*pi"
);
}
#[cfg(feature = "parallel")]
#[test]
fn test_parallel_rhs_override_matches_sequential() {
use ries_rs::search::search_parallel_with_stats_and_config;
let mut gen_config = fast_config();
gen_config.rhs_constants = Some(vec![
Symbol::One,
Symbol::Two,
Symbol::Three,
Symbol::Pi,
Symbol::E,
]);
let search_config = SearchConfig {
target: 2.0,
max_matches: 20,
user_constants: gen_config.user_constants.clone(),
user_functions: gen_config.user_functions.clone(),
..Default::default()
};
let (sequential, _) = search_with_stats_and_config(&gen_config, &search_config);
let (parallel, _) = search_parallel_with_stats_and_config(&gen_config, &search_config);
assert_eq!(
match_signatures(&sequential),
match_signatures(¶llel),
"parallel RHS-override fallback should match sequential results"
);
}