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 exhaustive 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    /// One representation kind per type family throughout a model, without channelling.
66    Uniform,
67}
68
69impl Display for Channelling {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.write_str(match self {
72            Self::No => "no",
73            Self::Yes => "yes",
74            Self::Uniform => "uniform",
75        })
76    }
77}
78
79impl FromStr for Channelling {
80    type Err = String;
81
82    fn from_str(value: &str) -> Result<Self, Self::Err> {
83        match value.trim().to_ascii_lowercase().as_str() {
84            "no" => Ok(Self::No),
85            "yes" => Ok(Self::Yes),
86            "uniform" => Ok(Self::Uniform),
87            other => Err(format!(
88                "unknown channelling setting '{other}'; expected yes, no, or uniform"
89            )),
90        }
91    }
92}
93
94pub const DEFAULT_HEURISTIC_SEED: u64 = 0;
95
96thread_local! {
97    static ELEMENT_ENCODING: Cell<Option<crate::ast::sat_decision::ElementEncoding>> = const { Cell::new(None) };
98    static TABLE_ENCODING: Cell<Option<crate::ast::sat_decision::TableEncoding>> = const { Cell::new(None) };
99    static ALLDIFFERENT_ENCODING: Cell<Option<crate::ast::sat_decision::AllDifferentEncoding>> = const { Cell::new(None) };
100    static PB_ENCODING: Cell<Option<crate::ast::sat_decision::PbEncoding>> = const { Cell::new(None) };
101    static CARDINALITY_ENCODING: Cell<Option<crate::ast::sat_decision::CardinalityEncoding>> = const { Cell::new(None) };
102    static AMO_ENCODING: Cell<Option<crate::ast::sat_decision::AmoEncoding>> = const { Cell::new(None) };
103    static HEURISTIC: Cell<Heuristic> = const { Cell::new(Heuristic::Compact) };
104    static CHANNELLING: Cell<Channelling> = const { Cell::new(Channelling::No) };
105    static HEURISTIC_RANDOM_STATE: Cell<u64> = const { Cell::new(DEFAULT_HEURISTIC_SEED) };
106    static HEURISTIC_ALL_CHOICES: RefCell<AllChoicesState> =
107        const { RefCell::new(AllChoicesState::new()) };
108    /// 1-based answers for the interactive heuristic, matching Conjure's `--responses`.
109    static HEURISTIC_RESPONSES: RefCell<VecDeque<usize>> = const { RefCell::new(VecDeque::new()) };
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
113pub struct HeuristicChoice {
114    pub selected: usize,
115    pub options: Vec<String>,
116}
117
118#[derive(Debug)]
119struct AllChoicesState {
120    requested: Vec<usize>,
121    decisions: Vec<HeuristicChoice>,
122}
123
124impl AllChoicesState {
125    const fn new() -> Self {
126        Self {
127            requested: Vec::new(),
128            decisions: Vec::new(),
129        }
130    }
131}
132
133pub fn set_heuristic(heuristic: Heuristic) {
134    HEURISTIC.with(|current| current.set(heuristic));
135}
136
137pub fn heuristic() -> Heuristic {
138    HEURISTIC.with(Cell::get)
139}
140
141pub fn set_channelling(channelling: Channelling) {
142    CHANNELLING.with(|current| current.set(channelling));
143    crate::representation::uniform::reset();
144}
145
146pub fn channelling() -> Channelling {
147    CHANNELLING.with(Cell::get)
148}
149
150pub fn set_heuristic_seed(seed: u64) {
151    HEURISTIC_RANDOM_STATE.with(|state| state.set(seed));
152}
153
154/// Sets the 1-based answers consumed by the interactive heuristic (`i`).
155///
156/// When provided (e.g. via `--responses`), these are used instead of prompting on stdin.
157/// Values must be in `1..=n` for a decision with `n` options, matching Conjure.
158pub fn set_heuristic_responses(responses: Vec<usize>) {
159    HEURISTIC_RESPONSES.with(|state| {
160        *state.borrow_mut() = VecDeque::from(responses);
161    });
162}
163
164/// Clears any remaining interactive responses.
165pub fn clear_heuristic_responses() {
166    HEURISTIC_RESPONSES.with(|state| state.borrow_mut().clear());
167}
168
169/// Returns a deterministic pseudo-random index and advances the per-thread heuristic RNG.
170pub fn next_heuristic_random_index(upper_bound: usize) -> usize {
171    bug_assert!(
172        upper_bound > 0,
173        "random choice requires at least one candidate"
174    );
175    HEURISTIC_RANDOM_STATE.with(|state| {
176        // SplitMix64: small, reproducible, and sufficient for choice ordering.
177        let next = state.get().wrapping_add(0x9e37_79b9_7f4a_7c15);
178        state.set(next);
179        let mut mixed = next;
180        mixed = (mixed ^ (mixed >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
181        mixed = (mixed ^ (mixed >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
182        mixed ^= mixed >> 31;
183        (mixed as usize) % upper_bound
184    })
185}
186
187/// Starts one replay of the `x` heuristic using the requested branch indices.
188pub fn begin_heuristic_all_choices(requested: Vec<usize>) {
189    HEURISTIC_ALL_CHOICES.with(|state| {
190        *state.borrow_mut() = AllChoicesState {
191            requested,
192            decisions: Vec::new(),
193        };
194    });
195}
196
197/// Restores the previous heuristic when a scoped override ends.
198struct HeuristicGuard(Heuristic);
199
200impl Drop for HeuristicGuard {
201    fn drop(&mut self) {
202        set_heuristic(self.0);
203    }
204}
205
206/// Runs `f` with the compact heuristic in force, whatever the configured one is.
207///
208/// Rewriting a temporary model -- a comprehension generator submodel, say -- picks representations
209/// too, but those are not modelling choices about the user's problem: a representation is
210/// semantics-preserving, so whichever one a submodel gets, it enumerates the same values. Compact
211/// takes the smallest and moves on, leaving the configured heuristic's state for the decisions that
212/// do matter: `x` keeps enumerating combinations of the real model only, the random stream is not
213/// advanced, and the interactive heuristic does not ask the user about throwaway variables.
214///
215/// A representation named on the domain itself (`set (representation packed)`) still wins -- representation
216/// selection honours that before it consults the heuristic at all.
217pub fn with_compact_heuristic<T>(f: impl FnOnce() -> T) -> T {
218    let previous = heuristic();
219    set_heuristic(Heuristic::Compact);
220    let _guard = HeuristicGuard(previous);
221    f()
222}
223
224/// Selects the requested branch at the next `x` decision and records all available options.
225pub fn next_heuristic_all_index(options: &[&str]) -> usize {
226    bug_assert!(
227        !options.is_empty(),
228        "all-choice requires at least one candidate"
229    );
230    HEURISTIC_ALL_CHOICES.with(|state| {
231        let mut state = state.borrow_mut();
232        let decision_index = state.decisions.len();
233        let selected = state.requested.get(decision_index).copied().unwrap_or(0);
234        assert!(
235            selected < options.len(),
236            "requested heuristic branch {selected} but only {} options exist",
237            options.len()
238        );
239        state.decisions.push(HeuristicChoice {
240            selected,
241            options: options.iter().map(|option| (*option).to_string()).collect(),
242        });
243        selected
244    })
245}
246
247pub fn heuristic_all_choices() -> Vec<HeuristicChoice> {
248    HEURISTIC_ALL_CHOICES.with(|state| state.borrow().decisions.clone())
249}
250
251/// Selects an option for the interactive heuristic (`i`).
252///
253/// Options are printed 1-based. Prefers the next value from [`set_heuristic_responses`]; otherwise
254/// prompts on stdin (empty input picks option 1), matching Conjure.
255pub fn next_heuristic_interactive_index(options: &[&str]) -> usize {
256    bug_assert!(
257        !options.is_empty(),
258        "interactive choice requires at least one candidate"
259    );
260
261    for (index, option) in options.iter().enumerate() {
262        eprintln!("{}. {}", index + 1, option);
263    }
264
265    let recorded = HEURISTIC_RESPONSES.with(|state| state.borrow_mut().pop_front());
266    let one_based = match recorded {
267        Some(recorded) => {
268            eprintln!("Response: {recorded}");
269            assert!(
270                recorded >= 1 && recorded <= options.len(),
271                "recorded response {recorded} out of range; expected a value between 1 and {}",
272                options.len()
273            );
274            recorded
275        }
276        None => prompt_heuristic_interactive_choice(options.len()),
277    };
278    one_based - 1
279}
280
281fn prompt_heuristic_interactive_choice(option_count: usize) -> usize {
282    let stdin = io::stdin();
283    let mut stdout = io::stderr();
284    loop {
285        eprint!("Pick option: ");
286        let _ = stdout.flush();
287        let mut line = String::new();
288        if stdin.lock().read_line(&mut line).is_err() {
289            eprintln!("Failed to read interactive heuristic response; defaulting to 1");
290            return 1;
291        }
292        let trimmed = line.trim();
293        if trimmed.is_empty() {
294            return 1;
295        }
296        match trimmed.parse::<usize>() {
297            Ok(value) if value >= 1 && value <= option_count => return value,
298            Ok(_) => {
299                eprintln!("Enter a value between 1 and {option_count}");
300            }
301            Err(_) => {
302                eprintln!("Enter an integer value.");
303            }
304        }
305    }
306}
307
308#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
309pub enum Parser {
310    #[default]
311    TreeSitter,
312    ViaConjure,
313}
314
315impl Display for Parser {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        match self {
318            Parser::TreeSitter => write!(f, "tree-sitter"),
319            Parser::ViaConjure => write!(f, "via-conjure"),
320        }
321    }
322}
323
324impl FromStr for Parser {
325    type Err = String;
326
327    fn from_str(s: &str) -> Result<Self, Self::Err> {
328        match s.trim().to_ascii_lowercase().as_str() {
329            "tree-sitter" => Ok(Parser::TreeSitter),
330            "via-conjure" => Ok(Parser::ViaConjure),
331            other => Err(format!(
332                "unknown parser: {other}; expected one of: tree-sitter, via-conjure"
333            )),
334        }
335    }
336}
337
338thread_local! {
339    /// Thread-local setting for which parser is currently active.
340    ///
341    /// Must be explicitly set before use.
342    static CURRENT_PARSER: Cell<Option<Parser>> = const { Cell::new(None) };
343}
344
345pub fn set_current_parser(parser: Parser) {
346    CURRENT_PARSER.with(|current| current.set(Some(parser)));
347}
348
349pub fn current_parser() -> Parser {
350    CURRENT_PARSER.with(|current| {
351        current.get().unwrap_or_else(|| {
352            // loud failure on purpose, so we don't end up using the default
353            bug!("current parser not set for this thread; call set_current_parser first")
354        })
355    })
356}
357
358#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
359pub struct RewriteConfig {
360    /// Skip rules whose declared expression discriminants cannot match.
361    pub prefilter: bool,
362    /// Drive rewriting from persistent dirty node queues instead of repeated full scans.
363    pub worklist: bool,
364}
365
366impl RewriteConfig {
367    pub const fn baseline() -> Self {
368        Self {
369            prefilter: false,
370            worklist: false,
371        }
372    }
373
374    pub const fn optimised() -> Self {
375        Self {
376            prefilter: true,
377            worklist: true,
378        }
379    }
380
381    pub const fn is_baseline(self) -> bool {
382        !self.prefilter && !self.worklist
383    }
384
385    pub const fn is_optimised(self) -> bool {
386        self.prefilter && self.worklist
387    }
388}
389
390impl Default for RewriteConfig {
391    fn default() -> Self {
392        Self::optimised()
393    }
394}
395
396impl Display for RewriteConfig {
397    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398        if self.is_optimised() {
399            write!(f, "optimised")
400        } else if self.is_baseline() {
401            write!(f, "baseline")
402        } else {
403            let mut options = vec!["baseline"];
404            if self.prefilter {
405                options.push("prefilter");
406            }
407            if self.worklist {
408                options.push("worklist");
409            }
410            write!(f, "{}", options.join("+"))
411        }
412    }
413}
414
415impl FromStr for RewriteConfig {
416    type Err = String;
417
418    fn from_str(s: &str) -> Result<Self, Self::Err> {
419        let trimmed = s.trim().to_ascii_lowercase();
420        match trimmed.as_str() {
421            "baseline" => return Ok(Self::baseline()),
422            "optimised" => return Ok(Self::optimised()),
423            _ => {}
424        }
425
426        if !trimmed.starts_with("baseline+") {
427            return Err(format!(
428                "unknown rewrite config: {trimmed}; expected baseline, optimised, or baseline plus '+' separated prefilter/worklist options"
429            ));
430        }
431
432        let mut config = Self::baseline();
433        let mut baseline_seen = false;
434        let mut prefilter_seen = false;
435        let mut worklist_seen = false;
436        let mut option_seen = false;
437
438        for token in trimmed.split('+') {
439            match token {
440                "" => {}
441                "baseline" => {
442                    if baseline_seen {
443                        return Err("duplicate rewrite option 'baseline'".to_string());
444                    }
445                    baseline_seen = true;
446                }
447                "prefilter" => {
448                    if prefilter_seen {
449                        return Err("duplicate rewrite option 'prefilter'".to_string());
450                    }
451                    config.prefilter = true;
452                    prefilter_seen = true;
453                    option_seen = true;
454                }
455                "worklist" => {
456                    if worklist_seen {
457                        return Err("duplicate rewrite option 'worklist'".to_string());
458                    }
459                    config.worklist = true;
460                    worklist_seen = true;
461                    option_seen = true;
462                }
463                other => {
464                    return Err(format!(
465                        "unknown rewrite option '{other}'; expected baseline, optimised, or a '+' separated combination of: prefilter, worklist"
466                    ));
467                }
468            }
469        }
470
471        if !option_seen {
472            return Err(
473                "rewrite config 'baseline+' must include at least one of: prefilter, worklist"
474                    .to_string(),
475            );
476        }
477
478        Ok(config)
479    }
480}
481
482#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
483pub enum Rewriter {
484    Rewrite(RewriteConfig),
485}
486
487impl Default for Rewriter {
488    fn default() -> Self {
489        Self::Rewrite(RewriteConfig::optimised())
490    }
491}
492
493thread_local! {
494    /// Thread-local setting for which rewriter is currently active.
495    ///
496    /// Must be explicitly set before use.
497    static CURRENT_REWRITER: Cell<Option<Rewriter>> = const { Cell::new(None) };
498}
499
500impl Display for Rewriter {
501    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
502        match self {
503            Rewriter::Rewrite(config) => write!(f, "{config}"),
504        }
505    }
506}
507
508impl FromStr for Rewriter {
509    type Err = String;
510
511    fn from_str(s: &str) -> Result<Self, Self::Err> {
512        let trimmed = s.trim().to_ascii_lowercase();
513        match trimmed.as_str() {
514            "baseline" | "optimised" => Ok(Rewriter::Rewrite(trimmed.parse()?)),
515            other => {
516                if other.contains('+') {
517                    return Ok(Rewriter::Rewrite(other.parse()?));
518                }
519
520                Err(format!(
521                    "unknown rewriter: {other}; expected baseline, optimised, or baseline plus '+' separated prefilter/worklist options"
522                ))
523            }
524        }
525    }
526}
527
528pub fn set_current_rewriter(rewriter: Rewriter) {
529    CURRENT_REWRITER.with(|current| current.set(Some(rewriter)));
530}
531
532pub fn current_rewriter() -> Rewriter {
533    CURRENT_REWRITER.with(|current| {
534        current.get().unwrap_or_else(|| {
535            // loud failure on purpose, so we don't end up using the default
536            bug!("current rewriter not set for this thread; call set_current_rewriter first")
537        })
538    })
539}
540
541#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
542pub enum QuantifiedExpander {
543    Auto,
544    Native,
545    ViaSolver,
546    ViaSolverAc,
547}
548
549impl Display for QuantifiedExpander {
550    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
551        match self {
552            QuantifiedExpander::Auto => write!(f, "auto"),
553            QuantifiedExpander::Native => write!(f, "native"),
554            QuantifiedExpander::ViaSolver => write!(f, "via-solver"),
555            QuantifiedExpander::ViaSolverAc => write!(f, "via-solver-ac"),
556        }
557    }
558}
559
560impl FromStr for QuantifiedExpander {
561    type Err = String;
562
563    fn from_str(s: &str) -> Result<Self, Self::Err> {
564        match s.trim().to_ascii_lowercase().as_str() {
565            "auto" => Ok(QuantifiedExpander::Auto),
566            "native" => Ok(QuantifiedExpander::Native),
567            "via-solver" => Ok(QuantifiedExpander::ViaSolver),
568            "via-solver-ac" => Ok(QuantifiedExpander::ViaSolverAc),
569            _ => Err(format!(
570                "unknown comprehension expander: {s}; expected one of: \
571                 auto, native, via-solver, via-solver-ac"
572            )),
573        }
574    }
575}
576
577thread_local! {
578    /// Thread-local setting for which comprehension expansion strategy is currently active.
579    ///
580    /// Must be explicitly set before use.
581    static COMPREHENSION_EXPANDER: Cell<Option<QuantifiedExpander>> = const { Cell::new(None) };
582}
583
584pub fn set_comprehension_expander(expander: QuantifiedExpander) {
585    COMPREHENSION_EXPANDER.with(|current| current.set(Some(expander)));
586}
587
588pub fn comprehension_expander() -> QuantifiedExpander {
589    COMPREHENSION_EXPANDER.with(|current| {
590        current.get().unwrap_or_else(|| {
591            // loud failure on purpose, so we don't end up using the default
592            bug!(
593                "comprehension expander not set for this thread; call set_comprehension_expander first"
594            )
595        })
596    })
597}
598
599#[derive(
600    Debug,
601    EnumIter,
602    StrumDisplay,
603    PartialEq,
604    Eq,
605    Hash,
606    Clone,
607    Copy,
608    Serialize,
609    Deserialize,
610    JsonSchema,
611)]
612pub enum SolverFamily {
613    Minion,
614    Sat,
615    Z3,
616}
617
618thread_local! {
619    /// Thread-local setting for which solver family is currently active.
620    ///
621    /// Must be explicitly set before use.
622    static CURRENT_SOLVER_FAMILY: Cell<Option<SolverFamily>> = const { Cell::new(None) };
623}
624
625pub const DEFAULT_MINION_DISCRETE_THRESHOLD: usize = 10;
626
627thread_local! {
628    /// Thread-local setting controlling when Minion int domains are emitted as `DISCRETE`.
629    ///
630    /// If an int domain size is <= this threshold, the Minion adaptor uses `DISCRETE`; otherwise
631    /// it uses `BOUND`, unless another constraint requires `DISCRETE`.
632    static MINION_DISCRETE_THRESHOLD: Cell<Option<usize>> = const { Cell::new(None) };
633
634    /// Thread-local setting controlling whether rule-trace outputs are active in this phase.
635    ///
636    /// This is intentionally off by default and can be disabled before solver-time rewrites so
637    /// follow-up dominance-blocking rewrites do not pollute the initial rewrite trace.
638    static RULE_TRACE_ENABLED: Cell<bool> = const { Cell::new(false) };
639
640    /// Thread-local setting controlling whether default rule traces are configured.
641    static DEFAULT_RULE_TRACE_ENABLED: Cell<bool> = const { Cell::new(false) };
642
643    /// Thread-local setting controlling whether verbose rule-attempt traces are configured.
644    static RULE_ATTEMPT_TRACE_ENABLED: Cell<bool> = const { Cell::new(false) };
645
646    /// Thread-local setting controlling whether aggregate rule-application traces are configured.
647    static RULE_TRACE_AGGREGATES_ENABLED: Cell<bool> = const { Cell::new(false) };
648}
649
650pub fn set_current_solver_family(solver_family: SolverFamily) {
651    CURRENT_SOLVER_FAMILY.with(|current| current.set(Some(solver_family)));
652}
653
654/// Restores the previous solver family when a scoped override ends.
655struct SolverFamilyGuard(Option<SolverFamily>);
656
657impl Drop for SolverFamilyGuard {
658    fn drop(&mut self) {
659        CURRENT_SOLVER_FAMILY.with(|current| current.set(self.0));
660    }
661}
662
663/// Runs `f` as a rewrite for `solver_family`, then restores the surrounding solver context.
664///
665/// Some rewrites build and solve a temporary model with a different backend from the outer model.
666/// Representation applicability must follow the backend that will actually consume that temporary
667/// model, not the user's outer solver setting.
668pub fn with_solver_family<T>(solver_family: SolverFamily, f: impl FnOnce() -> T) -> T {
669    let previous = CURRENT_SOLVER_FAMILY.with(|current| current.replace(Some(solver_family)));
670    let _guard = SolverFamilyGuard(previous);
671    f()
672}
673
674/// The active solver family, or `None` if this thread has not set one.
675///
676/// Unlike [`current_solver_family`] this does not panic. Solver-family gating asks with this so
677/// that unit tests exercising a single component do not have to stand up a solver context.
678pub fn try_current_solver_family() -> Option<SolverFamily> {
679    CURRENT_SOLVER_FAMILY.with(|current| current.get())
680}
681
682pub fn current_solver_family() -> SolverFamily {
683    CURRENT_SOLVER_FAMILY.with(|current| {
684        current.get().unwrap_or_else(|| {
685            // loud failure on purpose, so we don't end up using the default
686            bug!(
687                "current solver family not set for this thread; call set_current_solver_family first"
688            )
689        })
690    })
691}
692
693/// Whether integers carry a representation choice for the solver being targeted.
694///
695/// SAT has no integers at all: an int variable is encoded as a vector of Booleans, and which
696/// encoding it gets is a representation choice like any other. The SMT backend does have integers,
697/// but has two ways to express them -- linear arithmetic and bit-vectors -- so the choice is a
698/// representation there too. Minion has one native integer and needs none of this. With no solver
699/// family set -- a unit test exercising one component, say -- integers stay concrete.
700pub fn ints_need_representation() -> bool {
701    matches!(
702        try_current_solver_family(),
703        Some(SolverFamily::Sat | SolverFamily::Z3)
704    )
705}
706
707/// Pins Minion's domain threshold, or leaves it to the modelling heuristic.
708pub fn set_minion_discrete_threshold(threshold: Option<usize>) {
709    MINION_DISCRETE_THRESHOLD.with(|current| current.set(threshold));
710}
711
712/// Selects one threshold for a Minion model: the default, all bound, or all discrete.
713/// Constraints requiring discrete variables still override the threshold.
714pub fn select_minion_discrete_threshold() -> usize {
715    if let Some(threshold) = MINION_DISCRETE_THRESHOLD.with(Cell::get) {
716        return threshold;
717    }
718    let thresholds = [DEFAULT_MINION_DISCRETE_THRESHOLD, 0, usize::MAX];
719    let labels = [
720        "minion-discrete-threshold=10",
721        "minion-discrete-threshold=0",
722        "minion-discrete-threshold=unlimited",
723    ];
724    let index = match heuristic() {
725        Heuristic::First | Heuristic::Compact => 0,
726        Heuristic::Random => next_heuristic_random_index(thresholds.len()),
727        Heuristic::Interactive => next_heuristic_interactive_index(&labels),
728        Heuristic::All => next_heuristic_all_index(&labels),
729    };
730    thresholds[index]
731}
732
733pub fn set_rule_trace_enabled(enabled: bool) {
734    RULE_TRACE_ENABLED.with(|current| current.set(enabled));
735}
736
737pub fn rule_trace_enabled() -> bool {
738    RULE_TRACE_ENABLED.with(|current| current.get())
739}
740
741pub fn set_default_rule_trace_enabled(enabled: bool) {
742    DEFAULT_RULE_TRACE_ENABLED.with(|current| current.set(enabled));
743}
744
745pub fn default_rule_trace_enabled() -> bool {
746    DEFAULT_RULE_TRACE_ENABLED.with(|current| current.get())
747}
748
749pub fn set_rule_attempt_trace_enabled(enabled: bool) {
750    RULE_ATTEMPT_TRACE_ENABLED.with(|current| current.set(enabled));
751}
752
753pub fn rule_attempt_trace_enabled() -> bool {
754    RULE_ATTEMPT_TRACE_ENABLED.with(|current| current.get())
755}
756
757pub fn set_rule_trace_aggregates_enabled(enabled: bool) {
758    RULE_TRACE_AGGREGATES_ENABLED.with(|current| current.set(enabled));
759}
760
761pub fn rule_trace_aggregates_enabled() -> bool {
762    RULE_TRACE_AGGREGATES_ENABLED.with(|current| current.get())
763}
764
765pub fn configured_rule_trace_enabled() -> bool {
766    default_rule_trace_enabled() || rule_attempt_trace_enabled() || rule_trace_aggregates_enabled()
767}
768
769impl FromStr for SolverFamily {
770    type Err = String;
771
772    fn from_str(s: &str) -> Result<Self, Self::Err> {
773        match s.trim().to_ascii_lowercase().as_str() {
774            "minion" => Ok(SolverFamily::Minion),
775            "sat" => Ok(SolverFamily::Sat),
776            "z3" => Ok(SolverFamily::Z3),
777            other => Err(format!(
778                "unknown solver '{other}', expected one of: minion, sat, z3"
779            )),
780        }
781    }
782}
783
784impl SolverFamily {
785    pub fn as_str(&self) -> String {
786        match self {
787            SolverFamily::Minion => "minion".to_owned(),
788            SolverFamily::Sat => "sat".to_owned(),
789            SolverFamily::Z3 => "z3".to_owned(),
790        }
791    }
792}
793
794#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
795pub struct SolverArgs {
796    pub timeout_ms: Option<u64>,
797}
798
799#[cfg(test)]
800mod tests {
801    use super::{
802        Channelling, Heuristic, QuantifiedExpander, RewriteConfig, Rewriter, SolverFamily,
803        begin_heuristic_all_choices, clear_heuristic_responses, heuristic_all_choices,
804        next_heuristic_all_index, next_heuristic_interactive_index, next_heuristic_random_index,
805        set_current_solver_family, set_heuristic_responses, set_heuristic_seed,
806        try_current_solver_family, with_solver_family,
807    };
808    use std::str::FromStr;
809
810    #[test]
811    fn compact_is_the_default_heuristic() {
812        assert_eq!(Heuristic::default(), Heuristic::Compact);
813    }
814
815    #[test]
816    fn scoped_solver_family_restores_the_outer_solver() {
817        set_current_solver_family(SolverFamily::Z3);
818
819        with_solver_family(SolverFamily::Minion, || {
820            assert_eq!(try_current_solver_family(), Some(SolverFamily::Minion));
821        });
822
823        assert_eq!(try_current_solver_family(), Some(SolverFamily::Z3));
824    }
825
826    #[test]
827    fn parses_answer_heuristics_and_channelling() {
828        assert_eq!(Heuristic::from_str("f"), Ok(Heuristic::First));
829        assert_eq!(Heuristic::from_str("random"), Ok(Heuristic::Random));
830        assert_eq!(Heuristic::from_str("c"), Ok(Heuristic::Compact));
831        assert_eq!(Heuristic::from_str("i"), Ok(Heuristic::Interactive));
832        assert_eq!(
833            Heuristic::from_str("interactive"),
834            Ok(Heuristic::Interactive)
835        );
836        assert_eq!(Heuristic::from_str("x"), Ok(Heuristic::All));
837        assert_eq!(Channelling::from_str("no"), Ok(Channelling::No));
838        assert_eq!(Channelling::from_str("yes"), Ok(Channelling::Yes));
839        assert_eq!(Channelling::from_str("uniform"), Ok(Channelling::Uniform));
840        assert_eq!(Channelling::Uniform.to_string(), "uniform");
841    }
842
843    #[test]
844    fn parses_auto_comprehension_expander() {
845        assert_eq!(
846            QuantifiedExpander::from_str("auto"),
847            Ok(QuantifiedExpander::Auto)
848        );
849        assert_eq!(QuantifiedExpander::Auto.to_string(), "auto");
850    }
851
852    #[test]
853    fn interactive_heuristic_consumes_one_based_responses() {
854        set_heuristic_responses(vec![2, 1]);
855        assert_eq!(next_heuristic_interactive_index(&["left", "right"]), 1);
856        assert_eq!(next_heuristic_interactive_index(&["a", "b", "c"]), 0);
857        clear_heuristic_responses();
858    }
859
860    #[test]
861    fn random_heuristic_is_reproducible_from_seed() {
862        set_heuristic_seed(42);
863        let first: Vec<_> = (0..8).map(|_| next_heuristic_random_index(7)).collect();
864        set_heuristic_seed(42);
865        let second: Vec<_> = (0..8).map(|_| next_heuristic_random_index(7)).collect();
866        assert_eq!(first, second);
867    }
868
869    #[test]
870    fn all_heuristic_replays_and_records_choices() {
871        begin_heuristic_all_choices(vec![1]);
872        assert_eq!(next_heuristic_all_index(&["left", "right"]), 1);
873        assert_eq!(
874            heuristic_all_choices()[0].options,
875            vec!["left".to_string(), "right".to_string()]
876        );
877    }
878
879    #[test]
880    fn parses_rewrite_option_combinations() {
881        assert_eq!(
882            RewriteConfig::from_str("baseline").unwrap(),
883            RewriteConfig {
884                prefilter: false,
885                worklist: false,
886            }
887        );
888        assert_eq!(
889            RewriteConfig::from_str("baseline+prefilter").unwrap(),
890            RewriteConfig {
891                prefilter: true,
892                worklist: false,
893            }
894        );
895        assert_eq!(
896            RewriteConfig::from_str("baseline+worklist").unwrap(),
897            RewriteConfig {
898                prefilter: false,
899                worklist: true,
900            }
901        );
902        assert_eq!(
903            RewriteConfig::from_str("baseline+prefilter+worklist").unwrap(),
904            RewriteConfig::optimised()
905        );
906        assert!(RewriteConfig::from_str("+dirty").is_err());
907        assert!(RewriteConfig::from_str("baseline+dirty").is_err());
908        assert!(RewriteConfig::from_str("baseline+cache").is_err());
909        assert!(RewriteConfig::from_str("baseline+rulememo").is_err());
910        assert!(RewriteConfig::from_str("baseline+candidateindex").is_err());
911        assert!(RewriteConfig::from_str("baseline+dirtyqueues").is_err());
912        assert!(RewriteConfig::from_str("baseline+rule-memo").is_err());
913        assert!(RewriteConfig::from_str("baseline+candidate-index").is_err());
914        assert!(RewriteConfig::from_str("baseline+candidate-node-index").is_err());
915        assert!(RewriteConfig::from_str("baseline+dirty-node-queues").is_err());
916    }
917
918    #[test]
919    fn optimised_rewrite_config_enables_fast_default_options() {
920        assert_eq!(
921            RewriteConfig::from_str("optimised").unwrap(),
922            RewriteConfig {
923                prefilter: true,
924                worklist: true,
925            }
926        );
927    }
928
929    #[test]
930    fn displays_rewrite_option_combinations() {
931        assert_eq!(RewriteConfig::baseline().to_string(), "baseline");
932        assert_eq!(
933            RewriteConfig {
934                prefilter: true,
935                worklist: false,
936            }
937            .to_string(),
938            "baseline+prefilter"
939        );
940        assert_eq!(
941            RewriteConfig {
942                prefilter: false,
943                worklist: true,
944            }
945            .to_string(),
946            "baseline+worklist"
947        );
948        assert_eq!(RewriteConfig::optimised().to_string(), "optimised");
949        assert_eq!(
950            RewriteConfig::from_str("baseline+prefilter+worklist")
951                .unwrap()
952                .to_string(),
953            "optimised"
954        );
955    }
956
957    #[test]
958    fn parses_rewriter_option_combinations() {
959        assert_eq!(
960            Rewriter::from_str("baseline+prefilter").unwrap(),
961            Rewriter::Rewrite(RewriteConfig {
962                prefilter: true,
963                worklist: false,
964            })
965        );
966        assert_eq!(
967            Rewriter::from_str("baseline+prefilter+worklist").unwrap(),
968            Rewriter::Rewrite(RewriteConfig::optimised())
969        );
970        assert!(Rewriter::from_str("+dirty").is_err());
971        assert!(Rewriter::from_str("dirty").is_err());
972        assert!(Rewriter::from_str("baseline+dirty").is_err());
973    }
974}
975
976/// Pin the AMO algorithm, or allow the modelling heuristic to choose.
977pub fn set_amo_encoding(encoding: Option<crate::ast::sat_decision::AmoEncoding>) {
978    AMO_ENCODING.with(|current| current.set(encoding));
979}
980/// Explicit AMO algorithm override for this compilation thread.
981pub fn amo_encoding() -> Option<crate::ast::sat_decision::AmoEncoding> {
982    AMO_ENCODING.with(Cell::get)
983}
984
985/// Pin a cardinality provider/algorithm, or let the heuristic select it.
986pub fn set_cardinality_encoding(encoding: Option<crate::ast::sat_decision::CardinalityEncoding>) {
987    CARDINALITY_ENCODING.with(|current| current.set(encoding));
988}
989/// Explicit cardinality encoder override for this compilation thread.
990pub fn cardinality_encoding() -> Option<crate::ast::sat_decision::CardinalityEncoding> {
991    CARDINALITY_ENCODING.with(Cell::get)
992}
993
994/// Pin a pseudo-Boolean provider/algorithm, or let the heuristic select it.
995pub fn set_pb_encoding(encoding: Option<crate::ast::sat_decision::PbEncoding>) {
996    PB_ENCODING.with(|current| current.set(encoding));
997}
998/// Explicit pseudo-Boolean encoder override for this compilation thread.
999pub fn pb_encoding() -> Option<crate::ast::sat_decision::PbEncoding> {
1000    PB_ENCODING.with(Cell::get)
1001}
1002
1003/// Pin the allDifferent strategy, or let the heuristic choose an applicable composition.
1004pub fn set_alldifferent_encoding(encoding: Option<crate::ast::sat_decision::AllDifferentEncoding>) {
1005    ALLDIFFERENT_ENCODING.with(|current| current.set(encoding));
1006}
1007/// Explicit allDifferent strategy override for this compilation thread.
1008pub fn alldifferent_encoding() -> Option<crate::ast::sat_decision::AllDifferentEncoding> {
1009    ALLDIFFERENT_ENCODING.with(Cell::get)
1010}
1011
1012/// Pin the table composition, or let the modelling heuristic select it.
1013pub fn set_table_encoding(encoding: Option<crate::ast::sat_decision::TableEncoding>) {
1014    TABLE_ENCODING.with(|current| current.set(encoding));
1015}
1016/// Explicit table strategy override for this compilation thread.
1017pub fn table_encoding() -> Option<crate::ast::sat_decision::TableEncoding> {
1018    TABLE_ENCODING.with(Cell::get)
1019}
1020
1021/// Pin the element composition, or let the modelling heuristic select it.
1022pub fn set_element_encoding(encoding: Option<crate::ast::sat_decision::ElementEncoding>) {
1023    ELEMENT_ENCODING.with(|current| current.set(encoding));
1024}
1025/// Explicit element strategy override for this compilation thread.
1026pub fn element_encoding() -> Option<crate::ast::sat_decision::ElementEncoding> {
1027    ELEMENT_ENCODING.with(Cell::get)
1028}