1use std::{
2 cell::{Cell, RefCell},
3 collections::VecDeque,
4 fmt::Display,
5 io::{self, BufRead, Write},
6 str::FromStr,
7};
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use strum_macros::{Display as StrumDisplay, EnumIter};
12
13use crate::bug;
14use crate::bug_assert;
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18pub enum Heuristic {
19 First,
20 Random,
21 #[default]
22 Compact,
23 Interactive,
25 All,
28}
29
30impl Display for Heuristic {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.write_str(match self {
33 Self::First => "f",
34 Self::Random => "r",
35 Self::Compact => "c",
36 Self::Interactive => "i",
37 Self::All => "x",
38 })
39 }
40}
41
42impl FromStr for Heuristic {
43 type Err = String;
44
45 fn from_str(value: &str) -> Result<Self, Self::Err> {
46 match value.trim().to_ascii_lowercase().as_str() {
47 "f" | "first" => Ok(Self::First),
48 "r" | "random" => Ok(Self::Random),
49 "c" | "compact" => Ok(Self::Compact),
50 "i" | "interactive" => Ok(Self::Interactive),
51 "x" | "all" => Ok(Self::All),
52 other => Err(format!(
53 "unknown heuristic '{other}'; expected one of: f, r, c, i, x"
54 )),
55 }
56 }
57}
58
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
61pub enum Channelling {
62 #[default]
63 No,
64 Yes,
65}
66
67impl Display for Channelling {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.write_str(match self {
70 Self::No => "no",
71 Self::Yes => "yes",
72 })
73 }
74}
75
76impl FromStr for Channelling {
77 type Err = String;
78
79 fn from_str(value: &str) -> Result<Self, Self::Err> {
80 match value.trim().to_ascii_lowercase().as_str() {
81 "no" => Ok(Self::No),
82 "yes" => Ok(Self::Yes),
83 other => Err(format!(
84 "unknown channelling setting '{other}'; expected yes or no"
85 )),
86 }
87 }
88}
89
90pub const DEFAULT_HEURISTIC_SEED: u64 = 0;
91
92thread_local! {
93 static HEURISTIC: Cell<Heuristic> = const { Cell::new(Heuristic::Compact) };
94 static CHANNELLING: Cell<Channelling> = const { Cell::new(Channelling::No) };
95 static HEURISTIC_RANDOM_STATE: Cell<u64> = const { Cell::new(DEFAULT_HEURISTIC_SEED) };
96 static HEURISTIC_ALL_CHOICES: RefCell<AllChoicesState> =
97 const { RefCell::new(AllChoicesState::new()) };
98 static HEURISTIC_RESPONSES: RefCell<VecDeque<usize>> = const { RefCell::new(VecDeque::new()) };
100}
101
102#[derive(Clone, Debug, PartialEq, Eq)]
103pub struct HeuristicChoice {
104 pub selected: usize,
105 pub options: Vec<String>,
106}
107
108#[derive(Debug)]
109struct AllChoicesState {
110 requested: Vec<usize>,
111 decisions: Vec<HeuristicChoice>,
112}
113
114impl AllChoicesState {
115 const fn new() -> Self {
116 Self {
117 requested: Vec::new(),
118 decisions: Vec::new(),
119 }
120 }
121}
122
123pub fn set_heuristic(heuristic: Heuristic) {
124 HEURISTIC.with(|current| current.set(heuristic));
125}
126
127pub fn heuristic() -> Heuristic {
128 HEURISTIC.with(Cell::get)
129}
130
131pub fn set_channelling(channelling: Channelling) {
132 CHANNELLING.with(|current| current.set(channelling));
133}
134
135pub fn channelling() -> Channelling {
136 CHANNELLING.with(Cell::get)
137}
138
139pub fn set_heuristic_seed(seed: u64) {
140 HEURISTIC_RANDOM_STATE.with(|state| state.set(seed));
141}
142
143pub fn set_heuristic_responses(responses: Vec<usize>) {
148 HEURISTIC_RESPONSES.with(|state| {
149 *state.borrow_mut() = VecDeque::from(responses);
150 });
151}
152
153pub fn clear_heuristic_responses() {
155 HEURISTIC_RESPONSES.with(|state| state.borrow_mut().clear());
156}
157
158pub fn next_heuristic_random_index(upper_bound: usize) -> usize {
160 bug_assert!(
161 upper_bound > 0,
162 "random choice requires at least one candidate"
163 );
164 HEURISTIC_RANDOM_STATE.with(|state| {
165 let next = state.get().wrapping_add(0x9e37_79b9_7f4a_7c15);
167 state.set(next);
168 let mut mixed = next;
169 mixed = (mixed ^ (mixed >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
170 mixed = (mixed ^ (mixed >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
171 mixed ^= mixed >> 31;
172 (mixed as usize) % upper_bound
173 })
174}
175
176pub fn begin_heuristic_all_choices(requested: Vec<usize>) {
178 HEURISTIC_ALL_CHOICES.with(|state| {
179 *state.borrow_mut() = AllChoicesState {
180 requested,
181 decisions: Vec::new(),
182 };
183 });
184}
185
186struct HeuristicGuard(Heuristic);
188
189impl Drop for HeuristicGuard {
190 fn drop(&mut self) {
191 set_heuristic(self.0);
192 }
193}
194
195pub fn with_compact_heuristic<T>(f: impl FnOnce() -> T) -> T {
207 let previous = heuristic();
208 set_heuristic(Heuristic::Compact);
209 let _guard = HeuristicGuard(previous);
210 f()
211}
212
213pub fn next_heuristic_all_index(options: &[&str]) -> usize {
215 bug_assert!(
216 !options.is_empty(),
217 "all-choice requires at least one candidate"
218 );
219 HEURISTIC_ALL_CHOICES.with(|state| {
220 let mut state = state.borrow_mut();
221 let decision_index = state.decisions.len();
222 let selected = state.requested.get(decision_index).copied().unwrap_or(0);
223 assert!(
224 selected < options.len(),
225 "requested heuristic branch {selected} but only {} options exist",
226 options.len()
227 );
228 state.decisions.push(HeuristicChoice {
229 selected,
230 options: options.iter().map(|option| (*option).to_string()).collect(),
231 });
232 selected
233 })
234}
235
236pub fn heuristic_all_choices() -> Vec<HeuristicChoice> {
237 HEURISTIC_ALL_CHOICES.with(|state| state.borrow().decisions.clone())
238}
239
240pub fn next_heuristic_interactive_index(options: &[&str]) -> usize {
245 bug_assert!(
246 !options.is_empty(),
247 "interactive choice requires at least one candidate"
248 );
249
250 for (index, option) in options.iter().enumerate() {
251 eprintln!("{}. {}", index + 1, option);
252 }
253
254 let recorded = HEURISTIC_RESPONSES.with(|state| state.borrow_mut().pop_front());
255 let one_based = match recorded {
256 Some(recorded) => {
257 eprintln!("Response: {recorded}");
258 assert!(
259 recorded >= 1 && recorded <= options.len(),
260 "recorded response {recorded} out of range; expected a value between 1 and {}",
261 options.len()
262 );
263 recorded
264 }
265 None => prompt_heuristic_interactive_choice(options.len()),
266 };
267 one_based - 1
268}
269
270fn prompt_heuristic_interactive_choice(option_count: usize) -> usize {
271 let stdin = io::stdin();
272 let mut stdout = io::stderr();
273 loop {
274 eprint!("Pick option: ");
275 let _ = stdout.flush();
276 let mut line = String::new();
277 if stdin.lock().read_line(&mut line).is_err() {
278 eprintln!("Failed to read interactive heuristic response; defaulting to 1");
279 return 1;
280 }
281 let trimmed = line.trim();
282 if trimmed.is_empty() {
283 return 1;
284 }
285 match trimmed.parse::<usize>() {
286 Ok(value) if value >= 1 && value <= option_count => return value,
287 Ok(_) => {
288 eprintln!("Enter a value between 1 and {option_count}");
289 }
290 Err(_) => {
291 eprintln!("Enter an integer value.");
292 }
293 }
294 }
295}
296
297#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
298pub enum Parser {
299 #[default]
300 TreeSitter,
301 ViaConjure,
302}
303
304impl Display for Parser {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 match self {
307 Parser::TreeSitter => write!(f, "tree-sitter"),
308 Parser::ViaConjure => write!(f, "via-conjure"),
309 }
310 }
311}
312
313impl FromStr for Parser {
314 type Err = String;
315
316 fn from_str(s: &str) -> Result<Self, Self::Err> {
317 match s.trim().to_ascii_lowercase().as_str() {
318 "tree-sitter" => Ok(Parser::TreeSitter),
319 "via-conjure" => Ok(Parser::ViaConjure),
320 other => Err(format!(
321 "unknown parser: {other}; expected one of: tree-sitter, via-conjure"
322 )),
323 }
324 }
325}
326
327thread_local! {
328 static CURRENT_PARSER: Cell<Option<Parser>> = const { Cell::new(None) };
332}
333
334pub fn set_current_parser(parser: Parser) {
335 CURRENT_PARSER.with(|current| current.set(Some(parser)));
336}
337
338pub fn current_parser() -> Parser {
339 CURRENT_PARSER.with(|current| {
340 current.get().unwrap_or_else(|| {
341 bug!("current parser not set for this thread; call set_current_parser first")
343 })
344 })
345}
346
347#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
348pub struct RewriteConfig {
349 pub prefilter: bool,
351 pub worklist: bool,
353}
354
355impl RewriteConfig {
356 pub const fn baseline() -> Self {
357 Self {
358 prefilter: false,
359 worklist: false,
360 }
361 }
362
363 pub const fn optimised() -> Self {
364 Self {
365 prefilter: true,
366 worklist: true,
367 }
368 }
369
370 pub const fn is_baseline(self) -> bool {
371 !self.prefilter && !self.worklist
372 }
373
374 pub const fn is_optimised(self) -> bool {
375 self.prefilter && self.worklist
376 }
377}
378
379impl Default for RewriteConfig {
380 fn default() -> Self {
381 Self::optimised()
382 }
383}
384
385impl Display for RewriteConfig {
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 if self.is_optimised() {
388 write!(f, "optimised")
389 } else if self.is_baseline() {
390 write!(f, "baseline")
391 } else {
392 let mut options = vec!["baseline"];
393 if self.prefilter {
394 options.push("prefilter");
395 }
396 if self.worklist {
397 options.push("worklist");
398 }
399 write!(f, "{}", options.join("+"))
400 }
401 }
402}
403
404impl FromStr for RewriteConfig {
405 type Err = String;
406
407 fn from_str(s: &str) -> Result<Self, Self::Err> {
408 let trimmed = s.trim().to_ascii_lowercase();
409 match trimmed.as_str() {
410 "baseline" => return Ok(Self::baseline()),
411 "optimised" => return Ok(Self::optimised()),
412 _ => {}
413 }
414
415 if !trimmed.starts_with("baseline+") {
416 return Err(format!(
417 "unknown rewrite config: {trimmed}; expected baseline, optimised, or baseline plus '+' separated prefilter/worklist options"
418 ));
419 }
420
421 let mut config = Self::baseline();
422 let mut baseline_seen = false;
423 let mut prefilter_seen = false;
424 let mut worklist_seen = false;
425 let mut option_seen = false;
426
427 for token in trimmed.split('+') {
428 match token {
429 "" => {}
430 "baseline" => {
431 if baseline_seen {
432 return Err("duplicate rewrite option 'baseline'".to_string());
433 }
434 baseline_seen = true;
435 }
436 "prefilter" => {
437 if prefilter_seen {
438 return Err("duplicate rewrite option 'prefilter'".to_string());
439 }
440 config.prefilter = true;
441 prefilter_seen = true;
442 option_seen = true;
443 }
444 "worklist" => {
445 if worklist_seen {
446 return Err("duplicate rewrite option 'worklist'".to_string());
447 }
448 config.worklist = true;
449 worklist_seen = true;
450 option_seen = true;
451 }
452 other => {
453 return Err(format!(
454 "unknown rewrite option '{other}'; expected baseline, optimised, or a '+' separated combination of: prefilter, worklist"
455 ));
456 }
457 }
458 }
459
460 if !option_seen {
461 return Err(
462 "rewrite config 'baseline+' must include at least one of: prefilter, worklist"
463 .to_string(),
464 );
465 }
466
467 Ok(config)
468 }
469}
470
471#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
472pub enum Rewriter {
473 Rewrite(RewriteConfig),
474}
475
476impl Default for Rewriter {
477 fn default() -> Self {
478 Self::Rewrite(RewriteConfig::optimised())
479 }
480}
481
482thread_local! {
483 static CURRENT_REWRITER: Cell<Option<Rewriter>> = const { Cell::new(None) };
487}
488
489impl Display for Rewriter {
490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491 match self {
492 Rewriter::Rewrite(config) => write!(f, "{config}"),
493 }
494 }
495}
496
497impl FromStr for Rewriter {
498 type Err = String;
499
500 fn from_str(s: &str) -> Result<Self, Self::Err> {
501 let trimmed = s.trim().to_ascii_lowercase();
502 match trimmed.as_str() {
503 "baseline" | "optimised" => Ok(Rewriter::Rewrite(trimmed.parse()?)),
504 other => {
505 if other.contains('+') {
506 return Ok(Rewriter::Rewrite(other.parse()?));
507 }
508
509 Err(format!(
510 "unknown rewriter: {other}; expected baseline, optimised, or baseline plus '+' separated prefilter/worklist options"
511 ))
512 }
513 }
514 }
515}
516
517pub fn set_current_rewriter(rewriter: Rewriter) {
518 CURRENT_REWRITER.with(|current| current.set(Some(rewriter)));
519}
520
521pub fn current_rewriter() -> Rewriter {
522 CURRENT_REWRITER.with(|current| {
523 current.get().unwrap_or_else(|| {
524 bug!("current rewriter not set for this thread; call set_current_rewriter first")
526 })
527 })
528}
529
530#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
531pub enum QuantifiedExpander {
532 Auto,
533 Native,
534 ViaSolver,
535 ViaSolverAc,
536}
537
538impl Display for QuantifiedExpander {
539 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540 match self {
541 QuantifiedExpander::Auto => write!(f, "auto"),
542 QuantifiedExpander::Native => write!(f, "native"),
543 QuantifiedExpander::ViaSolver => write!(f, "via-solver"),
544 QuantifiedExpander::ViaSolverAc => write!(f, "via-solver-ac"),
545 }
546 }
547}
548
549impl FromStr for QuantifiedExpander {
550 type Err = String;
551
552 fn from_str(s: &str) -> Result<Self, Self::Err> {
553 match s.trim().to_ascii_lowercase().as_str() {
554 "auto" => Ok(QuantifiedExpander::Auto),
555 "native" => Ok(QuantifiedExpander::Native),
556 "via-solver" => Ok(QuantifiedExpander::ViaSolver),
557 "via-solver-ac" => Ok(QuantifiedExpander::ViaSolverAc),
558 _ => Err(format!(
559 "unknown comprehension expander: {s}; expected one of: \
560 auto, native, via-solver, via-solver-ac"
561 )),
562 }
563 }
564}
565
566thread_local! {
567 static COMPREHENSION_EXPANDER: Cell<Option<QuantifiedExpander>> = const { Cell::new(None) };
571}
572
573pub fn set_comprehension_expander(expander: QuantifiedExpander) {
574 COMPREHENSION_EXPANDER.with(|current| current.set(Some(expander)));
575}
576
577pub fn comprehension_expander() -> QuantifiedExpander {
578 COMPREHENSION_EXPANDER.with(|current| {
579 current.get().unwrap_or_else(|| {
580 bug!(
582 "comprehension expander not set for this thread; call set_comprehension_expander first"
583 )
584 })
585 })
586}
587
588#[derive(
589 Debug,
590 EnumIter,
591 StrumDisplay,
592 PartialEq,
593 Eq,
594 Hash,
595 Clone,
596 Copy,
597 Serialize,
598 Deserialize,
599 JsonSchema,
600)]
601pub enum SolverFamily {
602 Minion,
603 Sat,
604 Z3,
605}
606
607thread_local! {
608 static CURRENT_SOLVER_FAMILY: Cell<Option<SolverFamily>> = const { Cell::new(None) };
612}
613
614pub const DEFAULT_MINION_DISCRETE_THRESHOLD: usize = 10;
615
616thread_local! {
617 static MINION_DISCRETE_THRESHOLD: Cell<usize> =
622 const { Cell::new(DEFAULT_MINION_DISCRETE_THRESHOLD) };
623
624 static RULE_TRACE_ENABLED: Cell<bool> = const { Cell::new(false) };
629
630 static DEFAULT_RULE_TRACE_ENABLED: Cell<bool> = const { Cell::new(false) };
632
633 static RULE_ATTEMPT_TRACE_ENABLED: Cell<bool> = const { Cell::new(false) };
635
636 static RULE_TRACE_AGGREGATES_ENABLED: Cell<bool> = const { Cell::new(false) };
638}
639
640pub fn set_current_solver_family(solver_family: SolverFamily) {
641 CURRENT_SOLVER_FAMILY.with(|current| current.set(Some(solver_family)));
642}
643
644struct SolverFamilyGuard(Option<SolverFamily>);
646
647impl Drop for SolverFamilyGuard {
648 fn drop(&mut self) {
649 CURRENT_SOLVER_FAMILY.with(|current| current.set(self.0));
650 }
651}
652
653pub fn with_solver_family<T>(solver_family: SolverFamily, f: impl FnOnce() -> T) -> T {
659 let previous = CURRENT_SOLVER_FAMILY.with(|current| current.replace(Some(solver_family)));
660 let _guard = SolverFamilyGuard(previous);
661 f()
662}
663
664pub fn try_current_solver_family() -> Option<SolverFamily> {
669 CURRENT_SOLVER_FAMILY.with(|current| current.get())
670}
671
672pub fn current_solver_family() -> SolverFamily {
673 CURRENT_SOLVER_FAMILY.with(|current| {
674 current.get().unwrap_or_else(|| {
675 bug!(
677 "current solver family not set for this thread; call set_current_solver_family first"
678 )
679 })
680 })
681}
682
683pub fn ints_need_representation() -> bool {
691 matches!(
692 try_current_solver_family(),
693 Some(SolverFamily::Sat | SolverFamily::Z3)
694 )
695}
696
697pub fn set_minion_discrete_threshold(threshold: usize) {
698 MINION_DISCRETE_THRESHOLD.with(|current| current.set(threshold));
699}
700
701pub fn minion_discrete_threshold() -> usize {
702 MINION_DISCRETE_THRESHOLD.with(|current| current.get())
703}
704
705pub fn set_rule_trace_enabled(enabled: bool) {
706 RULE_TRACE_ENABLED.with(|current| current.set(enabled));
707}
708
709pub fn rule_trace_enabled() -> bool {
710 RULE_TRACE_ENABLED.with(|current| current.get())
711}
712
713pub fn set_default_rule_trace_enabled(enabled: bool) {
714 DEFAULT_RULE_TRACE_ENABLED.with(|current| current.set(enabled));
715}
716
717pub fn default_rule_trace_enabled() -> bool {
718 DEFAULT_RULE_TRACE_ENABLED.with(|current| current.get())
719}
720
721pub fn set_rule_attempt_trace_enabled(enabled: bool) {
722 RULE_ATTEMPT_TRACE_ENABLED.with(|current| current.set(enabled));
723}
724
725pub fn rule_attempt_trace_enabled() -> bool {
726 RULE_ATTEMPT_TRACE_ENABLED.with(|current| current.get())
727}
728
729pub fn set_rule_trace_aggregates_enabled(enabled: bool) {
730 RULE_TRACE_AGGREGATES_ENABLED.with(|current| current.set(enabled));
731}
732
733pub fn rule_trace_aggregates_enabled() -> bool {
734 RULE_TRACE_AGGREGATES_ENABLED.with(|current| current.get())
735}
736
737pub fn configured_rule_trace_enabled() -> bool {
738 default_rule_trace_enabled() || rule_attempt_trace_enabled() || rule_trace_aggregates_enabled()
739}
740
741impl FromStr for SolverFamily {
742 type Err = String;
743
744 fn from_str(s: &str) -> Result<Self, Self::Err> {
745 match s.trim().to_ascii_lowercase().as_str() {
746 "minion" => Ok(SolverFamily::Minion),
747 "sat" => Ok(SolverFamily::Sat),
748 "z3" => Ok(SolverFamily::Z3),
749 other => Err(format!(
750 "unknown solver '{other}', expected one of: minion, sat, z3"
751 )),
752 }
753 }
754}
755
756impl SolverFamily {
757 pub fn as_str(&self) -> String {
758 match self {
759 SolverFamily::Minion => "minion".to_owned(),
760 SolverFamily::Sat => "sat".to_owned(),
761 SolverFamily::Z3 => "z3".to_owned(),
762 }
763 }
764}
765
766#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
767pub struct SolverArgs {
768 pub timeout_ms: Option<u64>,
769}
770
771#[cfg(test)]
772mod tests {
773 use super::{
774 Channelling, Heuristic, QuantifiedExpander, RewriteConfig, Rewriter, SolverFamily,
775 begin_heuristic_all_choices, clear_heuristic_responses, heuristic_all_choices,
776 next_heuristic_all_index, next_heuristic_interactive_index, next_heuristic_random_index,
777 set_current_solver_family, set_heuristic_responses, set_heuristic_seed,
778 try_current_solver_family, with_solver_family,
779 };
780 use std::str::FromStr;
781
782 #[test]
783 fn compact_is_the_default_heuristic() {
784 assert_eq!(Heuristic::default(), Heuristic::Compact);
785 }
786
787 #[test]
788 fn scoped_solver_family_restores_the_outer_solver() {
789 set_current_solver_family(SolverFamily::Z3);
790
791 with_solver_family(SolverFamily::Minion, || {
792 assert_eq!(try_current_solver_family(), Some(SolverFamily::Minion));
793 });
794
795 assert_eq!(try_current_solver_family(), Some(SolverFamily::Z3));
796 }
797
798 #[test]
799 fn parses_answer_heuristics_and_channelling() {
800 assert_eq!(Heuristic::from_str("f"), Ok(Heuristic::First));
801 assert_eq!(Heuristic::from_str("random"), Ok(Heuristic::Random));
802 assert_eq!(Heuristic::from_str("c"), Ok(Heuristic::Compact));
803 assert_eq!(Heuristic::from_str("i"), Ok(Heuristic::Interactive));
804 assert_eq!(
805 Heuristic::from_str("interactive"),
806 Ok(Heuristic::Interactive)
807 );
808 assert_eq!(Heuristic::from_str("x"), Ok(Heuristic::All));
809 assert_eq!(Channelling::from_str("no"), Ok(Channelling::No));
810 assert_eq!(Channelling::from_str("yes"), Ok(Channelling::Yes));
811 }
812
813 #[test]
814 fn parses_auto_comprehension_expander() {
815 assert_eq!(
816 QuantifiedExpander::from_str("auto"),
817 Ok(QuantifiedExpander::Auto)
818 );
819 assert_eq!(QuantifiedExpander::Auto.to_string(), "auto");
820 }
821
822 #[test]
823 fn interactive_heuristic_consumes_one_based_responses() {
824 set_heuristic_responses(vec![2, 1]);
825 assert_eq!(next_heuristic_interactive_index(&["left", "right"]), 1);
826 assert_eq!(next_heuristic_interactive_index(&["a", "b", "c"]), 0);
827 clear_heuristic_responses();
828 }
829
830 #[test]
831 fn random_heuristic_is_reproducible_from_seed() {
832 set_heuristic_seed(42);
833 let first: Vec<_> = (0..8).map(|_| next_heuristic_random_index(7)).collect();
834 set_heuristic_seed(42);
835 let second: Vec<_> = (0..8).map(|_| next_heuristic_random_index(7)).collect();
836 assert_eq!(first, second);
837 }
838
839 #[test]
840 fn all_heuristic_replays_and_records_choices() {
841 begin_heuristic_all_choices(vec![1]);
842 assert_eq!(next_heuristic_all_index(&["left", "right"]), 1);
843 assert_eq!(
844 heuristic_all_choices()[0].options,
845 vec!["left".to_string(), "right".to_string()]
846 );
847 }
848
849 #[test]
850 fn parses_rewrite_option_combinations() {
851 assert_eq!(
852 RewriteConfig::from_str("baseline").unwrap(),
853 RewriteConfig {
854 prefilter: false,
855 worklist: false,
856 }
857 );
858 assert_eq!(
859 RewriteConfig::from_str("baseline+prefilter").unwrap(),
860 RewriteConfig {
861 prefilter: true,
862 worklist: false,
863 }
864 );
865 assert_eq!(
866 RewriteConfig::from_str("baseline+worklist").unwrap(),
867 RewriteConfig {
868 prefilter: false,
869 worklist: true,
870 }
871 );
872 assert_eq!(
873 RewriteConfig::from_str("baseline+prefilter+worklist").unwrap(),
874 RewriteConfig::optimised()
875 );
876 assert!(RewriteConfig::from_str("+dirty").is_err());
877 assert!(RewriteConfig::from_str("baseline+dirty").is_err());
878 assert!(RewriteConfig::from_str("baseline+cache").is_err());
879 assert!(RewriteConfig::from_str("baseline+rulememo").is_err());
880 assert!(RewriteConfig::from_str("baseline+candidateindex").is_err());
881 assert!(RewriteConfig::from_str("baseline+dirtyqueues").is_err());
882 assert!(RewriteConfig::from_str("baseline+rule-memo").is_err());
883 assert!(RewriteConfig::from_str("baseline+candidate-index").is_err());
884 assert!(RewriteConfig::from_str("baseline+candidate-node-index").is_err());
885 assert!(RewriteConfig::from_str("baseline+dirty-node-queues").is_err());
886 }
887
888 #[test]
889 fn optimised_rewrite_config_enables_fast_default_options() {
890 assert_eq!(
891 RewriteConfig::from_str("optimised").unwrap(),
892 RewriteConfig {
893 prefilter: true,
894 worklist: true,
895 }
896 );
897 }
898
899 #[test]
900 fn displays_rewrite_option_combinations() {
901 assert_eq!(RewriteConfig::baseline().to_string(), "baseline");
902 assert_eq!(
903 RewriteConfig {
904 prefilter: true,
905 worklist: false,
906 }
907 .to_string(),
908 "baseline+prefilter"
909 );
910 assert_eq!(
911 RewriteConfig {
912 prefilter: false,
913 worklist: true,
914 }
915 .to_string(),
916 "baseline+worklist"
917 );
918 assert_eq!(RewriteConfig::optimised().to_string(), "optimised");
919 assert_eq!(
920 RewriteConfig::from_str("baseline+prefilter+worklist")
921 .unwrap()
922 .to_string(),
923 "optimised"
924 );
925 }
926
927 #[test]
928 fn parses_rewriter_option_combinations() {
929 assert_eq!(
930 Rewriter::from_str("baseline+prefilter").unwrap(),
931 Rewriter::Rewrite(RewriteConfig {
932 prefilter: true,
933 worklist: false,
934 })
935 );
936 assert_eq!(
937 Rewriter::from_str("baseline+prefilter+worklist").unwrap(),
938 Rewriter::Rewrite(RewriteConfig::optimised())
939 );
940 assert!(Rewriter::from_str("+dirty").is_err());
941 assert!(Rewriter::from_str("dirty").is_err());
942 assert!(Rewriter::from_str("baseline+dirty").is_err());
943 }
944}