1
//! A high-level API for interacting with constraints solvers.
2
//!
3
//! This module provides a consistent, solver-independent API for interacting with constraints
4
//! solvers. It also provides incremental solving support, and the returning of run stats from
5
//! solvers.
6
//!
7
//! -----
8
//!
9
//! - [Solver<Adaptor>] provides the API for interacting with constraints solvers.
10
//!
11
//! - The [SolverAdaptor] trait controls how solving actually occurs and handles translation
12
//!   between the [Solver] type and a specific solver.
13
//!
14
//! - [adaptors] contains all implemented solver adaptors.
15
//!
16
//! - The [model_modifier] submodule defines types to help with incremental solving / changing a
17
//!   model during search. The entrypoint for incremental solving is the [Solver<A,ModelLoaded>::solve_mut]
18
//!   function.
19
//!
20
//! # Examples
21
//!
22
//! ## A Successful Minion Model
23
//!
24
//! Note: this example constructs a basic Minion-compatible model instead of using the rewriter.
25
//! For a full end-to-end example, see crates/conjure-cp/examples/solver_hello_minion.rs
26
//!
27
//! ```ignore
28
//! use std::sync::{Arc,Mutex};
29
//! use conjure_cp_core::parse::get_example_model;
30
//! use conjure_cp_core::rule_engine::resolve_rule_sets;
31
//! use conjure_cp_core::rule_engine::rewrite_naive;
32
//! use conjure_cp_core::solver::{adaptors, Solver, SolverAdaptor};
33
//! use conjure_cp_core::solver::states::ModelLoaded;
34
//! use conjure_cp_core::Model;
35
//! use conjure_cp_core::ast::Domain;
36
//! use conjure_cp_core::ast::Declaration;
37
//! use conjure_cp_core::solver::SolverFamily;
38
//! use conjure_cp_core::context::Context;
39
//! use conjure_cp_essence_macros::essence_expr;
40
//!
41
//! // Define a model for minion.
42
//! let context = Context::<'static>::new_ptr_empty(SolverFamily::Minion);
43
//! let mut model = Model::new(context);
44
//! model.as_submodel_mut().add_symbol(Declaration::new_var("x".into(), Domain::Bool));
45
//! model.as_submodel_mut().add_symbol(Declaration::new_var("y".into(), Domain::Bool));
46
//! model.as_submodel_mut().add_constraint(essence_expr!{x != y});
47
//!
48
//! // Solve using Minion.
49
//! let solver = Solver::new(adaptors::Minion::new());
50
//! let solver: Solver<adaptors::Minion,ModelLoaded> = solver.load_model(model).unwrap();
51
//!
52
//! // In this example, we will count solutions.
53
//! //
54
//! // The solver interface is designed to allow adaptors to use multiple-threads / processes if
55
//! // necessary. Therefore, the callback type requires all variables inside it to have a static
56
//! // lifetime and to implement Send (i.e. the variable can be safely shared between theads).
57
//! //
58
//! // We use Arc<Mutex<T>> to create multiple references to a threadsafe mutable
59
//! // variable of type T.
60
//! //
61
//! // Using the move |x| ... closure syntax, we move one of these references into the closure.
62
//! // Note that a normal closure borrow variables from the parent so is not
63
//! // thread-safe.
64
//!
65
//! let counter_ref = Arc::new(Mutex::new(0));
66
//! let counter_ref_2 = counter_ref.clone();
67
//! solver.solve(Box::new(move |_| {
68
//!   let mut counter = (*counter_ref_2).lock().unwrap();
69
//!   *counter += 1;
70
//!   true
71
//!   }));
72
//!
73
//! let mut counter = (*counter_ref).lock().unwrap();
74
//! assert_eq!(*counter,2);
75
//! ```
76
//!
77
//! # The Solver callback function
78
//!
79
//! The callback function given to `solve` is called whenever a solution is found by the solver.
80
//!
81
//! Its return value can be used to control how many solutions the solver finds:
82
//!
83
//! * If the callback function returns `true`, solver execution continues.
84
//! * If the callback function returns `false`, the solver is terminated.
85
//!
86

            
87
// # Implementing Solver interfaces
88
//
89
// Solver interfaces can only be implemented inside this module, due to the SolverAdaptor crate
90
// being sealed.
91
//
92
// To add support for a solver, implement the `SolverAdaptor` trait in a submodule.
93
//
94
// If incremental solving support is required, also implement a new `ModelModifier`. If this is not
95
// required, all `ModelModifier` instances required by the SolverAdaptor trait can be replaced with
96
// NotModifiable.
97
//
98
// For more details, see the docstrings for SolverAdaptor, ModelModifier, and NotModifiable.
99

            
100
#![allow(dead_code)]
101
#![allow(unused)]
102
#![allow(clippy::manual_non_exhaustive)]
103

            
104
use std::any::Any;
105
use std::cell::OnceCell;
106
use std::collections::HashMap;
107
use std::error::Error;
108
use std::fmt::{Debug, Display};
109
use std::io::Write;
110
use std::rc::Rc;
111
use std::str::FromStr;
112
use std::sync::{Arc, RwLock};
113
use std::time::Instant;
114

            
115
use clap::ValueEnum;
116
use schemars::JsonSchema;
117
use serde::{Deserialize, Serialize};
118
use strum_macros::{Display, EnumIter, EnumString};
119
use thiserror::Error;
120

            
121
use crate::Model;
122
use crate::ast::{Literal, Name};
123
use crate::context::Context;
124
use crate::solver::adaptors::smt::{IntTheory, MatrixTheory, TheoryConfig};
125
use crate::stats::SolverStats;
126

            
127
use self::model_modifier::ModelModifier;
128
use self::states::{ExecutionSuccess, Init, ModelLoaded, SolverState};
129

            
130
pub mod adaptors;
131
pub mod model_modifier;
132

            
133
#[doc(hidden)]
134
mod private;
135

            
136
pub mod states;
137

            
138
#[derive(
139
    Debug, EnumIter, Display, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, JsonSchema,
140
)]
141
pub enum SolverFamily {
142
    Sat,
143
    Smt(TheoryConfig),
144
    Minion,
145
}
146

            
147
impl FromStr for SolverFamily {
148
    type Err = String;
149

            
150
    fn from_str(s: &str) -> Result<Self, Self::Err> {
151
        let s = s.trim().to_ascii_lowercase();
152

            
153
        match s.as_str() {
154
            "minion" => Ok(SolverFamily::Minion),
155
            "sat" => Ok(SolverFamily::Sat),
156
            "smt" => Ok(SolverFamily::Smt(TheoryConfig::default())),
157
            other => {
158
                // allow forms like `smt-bv-atomic` or `smt-lia-arrays`
159
                if other.starts_with("smt-") {
160
                    let parts = other.split('-').skip(1);
161
                    let mut ints = IntTheory::default();
162
                    let mut matrices = MatrixTheory::default();
163

            
164
                    for token in parts {
165
                        match token {
166
                            "lia" => ints = IntTheory::Lia,
167
                            "bv" => ints = IntTheory::Bv,
168
                            "arrays" => matrices = MatrixTheory::Arrays,
169
                            "atomic" => matrices = MatrixTheory::Atomic,
170
                            other_token => {
171
                                return Err(format!(
172
                                    "unknown SMT theory option '{other_token}', must be one of bv|lia|arrays|atomic"
173
                                ));
174
                            }
175
                        }
176
                    }
177

            
178
                    Ok(SolverFamily::Smt(TheoryConfig { ints, matrices }))
179
                } else {
180
                    Err(format!(
181
                        "unknown solver family '{other}', expected 'minion', 'sat' or 'smt[(bv|lia)-(arrays|atomic)]'"
182
                    ))
183
                }
184
            }
185
        }
186
    }
187
}
188

            
189
/// The type for user-defined callbacks for use with [Solver].
190
///
191
/// Note that this enforces thread safety
192
pub type SolverCallback = Box<dyn Fn(HashMap<Name, Literal>) -> bool + Send>;
193
pub type SolverMutCallback =
194
    Box<dyn Fn(HashMap<Name, Literal>, Box<dyn ModelModifier>) -> bool + Send>;
195

            
196
/// A common interface for calling underlying solver APIs inside a [`Solver`].
197
///
198
/// Implementations of this trait aren't directly callable and should be used through [`Solver`] .
199
///
200
/// The below documentation lists the formal requirements that all implementations of
201
/// [`SolverAdaptor`] should follow - **see the top level module documentation and [`Solver`] for
202
/// usage details.**
203
///
204
/// # Encapsulation
205
///
206
///  The [`SolverAdaptor`] trait **must** only be implemented inside a submodule of this one,
207
///  and **should** only be called through [`Solver`].
208
///
209
/// The `private::Sealed` trait and `private::Internal` type enforce these requirements by only
210
/// allowing trait implementations and calling of methods of SolverAdaptor to occur inside this
211
/// module.
212
///
213
/// # Thread Safety
214
///
215
/// Multiple instances of [`Solver`] can be run in parallel across multiple threads.
216
///
217
/// [`Solver`] provides no concurrency control or thread-safety; therefore, adaptors **must**
218
/// ensure that multiple instances of themselves can be ran in parallel. This applies to all
219
/// stages of solving including having two active `solve()` calls happening at a time, loading
220
/// a model while another is mid-solve, loading two models at once, etc.
221
///
222
/// A [SolverAdaptor] **may** use whatever threading or process model it likes underneath the hood,
223
/// as long as it obeys the above.
224
///
225
/// Method calls **should** block instead of erroring where possible.
226
///
227
/// Underlying solvers that only have one instance per process (such as Minion) **should** block
228
/// (eg. using a [`Mutex<()>`](`std::sync::Mutex`)) to run calls to
229
/// [`Solver<A,ModelLoaded>::solve()`] and [`Solver<A,ModelLoaded>::solve_mut()`] sequentially.
230
pub trait SolverAdaptor: private::Sealed + Any {
231
    /// Runs the solver on the given model.
232
    ///
233
    /// Implementations of this function **must** call the user provided callback whenever a solution
234
    /// is found. If the user callback returns `true`, search should continue, if the user callback
235
    /// returns `false`, search should terminate.
236
    ///
237
    /// # Returns
238
    ///
239
    /// If the solver terminates without crashing a [SolveSuccess] struct **must** returned. The
240
    /// value of [SearchStatus] can be used to denote whether the underlying solver completed its
241
    /// search or not. The latter case covers most non-crashing "failure" cases including user
242
    /// termination, timeouts, etc.
243
    ///
244
    /// To help populate [SearchStatus], it may be helpful to implement counters that track if the
245
    /// user callback has been called yet, and its return value. This information makes it is
246
    /// possible to distinguish between the most common search statuses:
247
    /// [SearchComplete::HasSolutions], [SearchComplete::NoSolutions], and
248
    /// [SearchIncomplete::UserTerminated].
249
    fn solve(
250
        &mut self,
251
        callback: SolverCallback,
252
        _: private::Internal,
253
    ) -> Result<SolveSuccess, SolverError>;
254

            
255
    /// Runs the solver on the given model, allowing modification of the model through a
256
    /// [`ModelModifier`].
257
    ///
258
    /// Implementations of this function **must** return [`OpNotSupported`](`ModificationFailure::OpNotSupported`)
259
    /// if modifying the model mid-search is not supported.
260
    ///
261
    /// Otherwise, this should work in the same way as [`solve`](SolverAdaptor::solve).
262
    fn solve_mut(
263
        &mut self,
264
        callback: SolverMutCallback,
265
        _: private::Internal,
266
    ) -> Result<SolveSuccess, SolverError>;
267
    fn load_model(&mut self, model: Model, _: private::Internal) -> Result<(), SolverError>;
268
    fn init_solver(&mut self, _: private::Internal) {}
269

            
270
    /// Get the solver family that this solver adaptor belongs to
271
    fn get_family(&self) -> SolverFamily;
272

            
273
    /// Gets the name of the solver adaptor for pretty printing.
274
    fn get_name(&self) -> &'static str;
275

            
276
    /// Adds the solver adaptor name and family (if they exist) to the given stats object.
277
    fn add_adaptor_info_to_stats(&self, stats: SolverStats) -> SolverStats {
278
        SolverStats {
279
            solver_adaptor: Some(String::from(self.get_name())),
280
            solver_family: Some(self.get_family()),
281
            ..stats
282
        }
283
    }
284

            
285
    /// Writes a solver input file to the given writer.
286
    ///
287
    /// This method is for debugging use only, and there are no plans to make the solutions
288
    /// obtained by running this file through the solver translatable back into high-level Essence.
289
    ///
290
    /// This file is runnable using the solvers command line interface. E.g. for Minion, this
291
    /// outputs a valid .minion file.
292
    ///
293
    ///
294
    /// # Implementation
295
    /// + It can be helpful for this file to contain comments linking constraints and variables to
296
    ///   their original essence, but this is not required.
297
    ///
298
    /// + This function is ran after model loading but before solving - therefore, it is safe for
299
    ///   solving to mutate the model object.
300
    fn write_solver_input_file(&self, writer: &mut Box<dyn Write>) -> Result<(), std::io::Error>;
301
}
302

            
303
/// An abstract representation of a constraints solver.
304
///
305
/// [Solver] provides a common interface for interacting with a constraint solver. It also
306
/// abstracts over solver-specific datatypes, handling the translation to/from [conjure_cp_core::ast]
307
/// types for a model and its solutions.
308
///
309
/// Details of how a model is solved is specified by the [SolverAdaptor]. This includes: the
310
/// underlying solver used, the translation of the model to a solver compatible form, how solutions
311
/// are translated back to [conjure_cp_core::ast] types, and how incremental solving is implemented.
312
/// As such, there may be multiple [SolverAdaptor] implementations for a single underlying solver:
313
/// e.g. one adaptor may give solutions in a representation close to the solvers, while another may
314
/// attempt to rewrite it back into Essence.
315
///
316
pub struct Solver<State: SolverState = Init> {
317
    state: State,
318
    adaptor: Box<dyn SolverAdaptor>,
319
    context: Option<Arc<RwLock<Context<'static>>>>,
320
}
321

            
322
impl Solver {
323
    pub fn new<A: SolverAdaptor>(solver_adaptor: A) -> Solver {
324
        let mut solver = Solver {
325
            state: Init,
326
            adaptor: Box::new(solver_adaptor),
327
            context: None,
328
        };
329

            
330
        solver.adaptor.init_solver(private::Internal);
331
        solver
332
    }
333

            
334
    pub fn get_family(&self) -> SolverFamily {
335
        self.adaptor.get_family()
336
    }
337

            
338
    pub fn get_name(&self) -> &'static str {
339
        self.adaptor.get_name()
340
    }
341
}
342

            
343
impl Solver<Init> {
344
    pub fn load_model(mut self, model: Model) -> Result<Solver<ModelLoaded>, SolverError> {
345
        let solver_model = &mut self.adaptor.load_model(model.clone(), private::Internal)?;
346
        Ok(Solver {
347
            state: ModelLoaded,
348
            adaptor: self.adaptor,
349
            context: Some(model.context.clone()),
350
        })
351
    }
352
}
353

            
354
impl Solver<ModelLoaded> {
355
    pub fn solve(
356
        mut self,
357
        callback: SolverCallback,
358
    ) -> Result<Solver<ExecutionSuccess>, SolverError> {
359
        #[allow(clippy::unwrap_used)]
360
        let start_time = Instant::now();
361

            
362
        #[allow(clippy::unwrap_used)]
363
        let result = self.adaptor.solve(callback, private::Internal);
364

            
365
        let duration = start_time.elapsed();
366

            
367
        match result {
368
            Ok(x) => {
369
                let stats = self
370
                    .adaptor
371
                    .add_adaptor_info_to_stats(x.stats)
372
                    .with_timings(duration.as_secs_f64());
373

            
374
                Ok(Solver {
375
                    adaptor: self.adaptor,
376
                    state: ExecutionSuccess {
377
                        stats,
378
                        status: x.status,
379
                        _sealed: private::Internal,
380
                    },
381
                    context: self.context,
382
                })
383
            }
384
            Err(x) => Err(x),
385
        }
386
    }
387

            
388
    pub fn solve_mut(
389
        mut self,
390
        callback: SolverMutCallback,
391
    ) -> Result<Solver<ExecutionSuccess>, SolverError> {
392
        #[allow(clippy::unwrap_used)]
393
        let start_time = Instant::now();
394

            
395
        #[allow(clippy::unwrap_used)]
396
        let result = self.adaptor.solve_mut(callback, private::Internal);
397

            
398
        let duration = start_time.elapsed();
399

            
400
        match result {
401
            Ok(x) => {
402
                let stats = self
403
                    .adaptor
404
                    .add_adaptor_info_to_stats(x.stats)
405
                    .with_timings(duration.as_secs_f64());
406

            
407
                Ok(Solver {
408
                    adaptor: self.adaptor,
409
                    state: ExecutionSuccess {
410
                        stats,
411
                        status: x.status,
412
                        _sealed: private::Internal,
413
                    },
414
                    context: self.context,
415
                })
416
            }
417
            Err(x) => Err(x),
418
        }
419
    }
420

            
421
    /// Writes a solver input file to the given writer.
422
    ///
423
    /// This method is for debugging use only, and there are no plans to make the solutions
424
    /// obtained by running this file through the solver translatable back into high-level Essence.
425
    ///
426
    /// This file is runnable using the solvers command line interface. E.g. for Minion, this
427
    /// outputs a valid .minion file.
428
    ///
429
    /// This function is only available in the `ModelLoaded` state as solvers are allowed to edit
430
    /// the model in place.
431
    pub fn write_solver_input_file(
432
        &self,
433
        writer: &mut Box<dyn Write>,
434
    ) -> Result<(), std::io::Error> {
435
        self.adaptor.write_solver_input_file(writer)
436
    }
437
}
438

            
439
impl Solver<ExecutionSuccess> {
440
    pub fn stats(&self) -> SolverStats {
441
        self.state.stats.clone()
442
    }
443

            
444
    // Saves this solvers stats to the global context as a "solver run"
445
    pub fn save_stats_to_context(&self) {
446
        #[allow(clippy::unwrap_used)]
447
        #[allow(clippy::expect_used)]
448
        self.context
449
            .as_ref()
450
            .expect("")
451
            .write()
452
            .unwrap()
453
            .stats
454
            .add_solver_run(self.stats());
455
    }
456

            
457
    pub fn wall_time_s(&self) -> f64 {
458
        self.stats().conjure_solver_wall_time_s
459
    }
460
}
461

            
462
/// Errors returned by [Solver] on failure.
463
#[non_exhaustive]
464
#[derive(Debug, Error, Clone)]
465
pub enum SolverError {
466
    #[error("operation not implemented yet: {0}")]
467
    OpNotImplemented(String),
468

            
469
    #[error("operation not supported: {0}")]
470
    OpNotSupported(String),
471

            
472
    #[error("model feature not supported: {0}")]
473
    ModelFeatureNotSupported(String),
474

            
475
    #[error("model feature not implemented yet: {0}")]
476
    ModelFeatureNotImplemented(String),
477

            
478
    // use for semantics / type errors, use the above for syntax
479
    #[error("model invalid: {0}")]
480
    ModelInvalid(String),
481

            
482
    #[error("error during solver execution: not implemented: {0}")]
483
    RuntimeNotImplemented(String),
484

            
485
    #[error("error during solver execution: {0}")]
486
    Runtime(String),
487
}
488

            
489
pub type SolverResult<T> = Result<T, SolverError>;
490

            
491
/// Returned from [SolverAdaptor] when solving is successful.
492
pub struct SolveSuccess {
493
    stats: SolverStats,
494
    status: SearchStatus,
495
}
496

            
497
pub enum SearchStatus {
498
    /// The search was complete (i.e. the solver found all possible solutions)
499
    Complete(SearchComplete),
500
    /// The search was incomplete (i.e. it was terminated before all solutions were found)
501
    Incomplete(SearchIncomplete),
502
}
503

            
504
#[non_exhaustive]
505
pub enum SearchIncomplete {
506
    Timeout,
507
    UserTerminated,
508
    #[doc(hidden)]
509
    /// This variant should not be matched - it exists to simulate non-exhaustiveness of this enum.
510
    __NonExhaustive,
511
}
512

            
513
#[non_exhaustive]
514
pub enum SearchComplete {
515
    HasSolutions,
516
    NoSolutions,
517
    #[doc(hidden)]
518
    /// This variant should not be matched - it exists to simulate non-exhaustiveness of this enum.
519
    __NonExhaustive,
520
}