Skip to main content

conjure_cp_core/
settings.rs

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/// Strategy used when a modelling decision has more than one applicable answer.
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18pub enum Heuristic {
19    First,
20    Random,
21    #[default]
22    Compact,
23    /// Prompt on stdin, or consume values from [`set_heuristic_responses`].
24    Interactive,
25    /// Enumerate every applicable choice. This is supported by the integration model generator,
26    /// but is not accepted by the command-line interface yet.
27    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/// Whether a declaration may have more than one channelled representation.
60#[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    /// 1-based answers for the interactive heuristic, matching Conjure's `--responses`.
99    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
143/// Sets the 1-based answers consumed by the interactive heuristic (`i`).
144///
145/// When provided (e.g. via `--responses`), these are used instead of prompting on stdin.
146/// Values must be in `1..=n` for a decision with `n` options, matching Conjure.
147pub fn set_heuristic_responses(responses: Vec<usize>) {
148    HEURISTIC_RESPONSES.with(|state| {
149        *state.borrow_mut() = VecDeque::from(responses);
150    });
151}
152
153/// Clears any remaining interactive responses.
154pub fn clear_heuristic_responses() {
155    HEURISTIC_RESPONSES.with(|state| state.borrow_mut().clear());
156}
157
158/// Returns a deterministic pseudo-random index and advances the per-thread heuristic RNG.
159pub 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        // SplitMix64: small, reproducible, and sufficient for choice ordering.
166        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
176/// Starts one replay of the `x` heuristic using the requested branch indices.
177pub 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
186/// Restores the previous heuristic when a scoped override ends.
187struct HeuristicGuard(Heuristic);
188
189impl Drop for HeuristicGuard {
190    fn drop(&mut self) {
191        set_heuristic(self.0);
192    }
193}
194
195/// Runs `f` with the compact heuristic in force, whatever the configured one is.
196///
197/// Rewriting a temporary model -- a comprehension generator submodel, say -- picks representations
198/// too, but those are not modelling choices about the user's problem: a representation is
199/// semantics-preserving, so whichever one a submodel gets, it enumerates the same values. Compact
200/// takes the smallest and moves on, leaving the configured heuristic's state for the decisions that
201/// do matter: `x` keeps enumerating combinations of the real model only, the random stream is not
202/// advanced, and the interactive heuristic does not ask the user about throwaway variables.
203///
204/// A representation named on the domain itself (`set (representation packed)`) still wins -- representation
205/// selection honours that before it consults the heuristic at all.
206pub 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
213/// Selects the requested branch at the next `x` decision and records all available options.
214pub 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
240/// Selects an option for the interactive heuristic (`i`).
241///
242/// Options are printed 1-based. Prefers the next value from [`set_heuristic_responses`]; otherwise
243/// prompts on stdin (empty input picks option 1), matching Conjure.
244pub 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    /// Thread-local setting for which parser is currently active.
329    ///
330    /// Must be explicitly set before use.
331    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            // loud failure on purpose, so we don't end up using the default
342            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    /// Skip rules whose declared expression discriminants cannot match.
350    pub prefilter: bool,
351    /// Drive rewriting from persistent dirty node queues instead of repeated full scans.
352    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    /// Thread-local setting for which rewriter is currently active.
484    ///
485    /// Must be explicitly set before use.
486    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            // loud failure on purpose, so we don't end up using the default
525            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    /// Thread-local setting for which comprehension expansion strategy is currently active.
568    ///
569    /// Must be explicitly set before use.
570    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            // loud failure on purpose, so we don't end up using the default
581            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    /// Thread-local setting for which solver family is currently active.
609    ///
610    /// Must be explicitly set before use.
611    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    /// Thread-local setting controlling when Minion int domains are emitted as `DISCRETE`.
618    ///
619    /// If an int domain size is <= this threshold, the Minion adaptor uses `DISCRETE`; otherwise
620    /// it uses `BOUND`, unless another constraint requires `DISCRETE`.
621    static MINION_DISCRETE_THRESHOLD: Cell<usize> =
622        const { Cell::new(DEFAULT_MINION_DISCRETE_THRESHOLD) };
623
624    /// Thread-local setting controlling whether rule-trace outputs are active in this phase.
625    ///
626    /// This is intentionally off by default and can be disabled before solver-time rewrites so
627    /// follow-up dominance-blocking rewrites do not pollute the initial rewrite trace.
628    static RULE_TRACE_ENABLED: Cell<bool> = const { Cell::new(false) };
629
630    /// Thread-local setting controlling whether default rule traces are configured.
631    static DEFAULT_RULE_TRACE_ENABLED: Cell<bool> = const { Cell::new(false) };
632
633    /// Thread-local setting controlling whether verbose rule-attempt traces are configured.
634    static RULE_ATTEMPT_TRACE_ENABLED: Cell<bool> = const { Cell::new(false) };
635
636    /// Thread-local setting controlling whether aggregate rule-application traces are configured.
637    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
644/// Restores the previous solver family when a scoped override ends.
645struct 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
653/// Runs `f` as a rewrite for `solver_family`, then restores the surrounding solver context.
654///
655/// Some rewrites build and solve a temporary model with a different backend from the outer model.
656/// Representation applicability must follow the backend that will actually consume that temporary
657/// model, not the user's outer solver setting.
658pub 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
664/// The active solver family, or `None` if this thread has not set one.
665///
666/// Unlike [`current_solver_family`] this does not panic. Solver-family gating asks with this so
667/// that unit tests exercising a single component do not have to stand up a solver context.
668pub 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            // loud failure on purpose, so we don't end up using the default
676            bug!(
677                "current solver family not set for this thread; call set_current_solver_family first"
678            )
679        })
680    })
681}
682
683/// Whether integers carry a representation choice for the solver being targeted.
684///
685/// SAT has no integers at all: an int variable is encoded as a vector of Booleans, and which
686/// encoding it gets is a representation choice like any other. The SMT backend does have integers,
687/// but has two ways to express them -- linear arithmetic and bit-vectors -- so the choice is a
688/// representation there too. Minion has one native integer and needs none of this. With no solver
689/// family set -- a unit test exercising one component, say -- integers stay concrete.
690pub 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}