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
//! ```rust
25
//! use std::sync::{Arc,Mutex};
26
//! use conjure_core::parse::get_example_model;
27
//! use conjure_core::rule_engine::resolve_rule_sets;
28
//! use conjure_core::rule_engine::rewrite_model;
29
//! use conjure_core::solver::{adaptors, Solver, SolverAdaptor};
30
//! use conjure_core::solver::states::ModelLoaded;
31
//! use conjure_core::solver::SolverFamily;
32
//!
33
//! // Define and rewrite a model for minion.
34
//! let model = get_example_model("bool-03").unwrap();
35
//! let rule_sets = resolve_rule_sets(SolverFamily::Minion, &vec!["Constant".to_string()]).unwrap();
36
//! let model = rewrite_model(&model,&rule_sets).unwrap();
37
//!
38
//!
39
//! // Solve using Minion.
40
//! let solver = Solver::new(adaptors::Minion::new());
41
//! let solver: Solver<adaptors::Minion,ModelLoaded> = solver.load_model(model).unwrap();
42
//!
43
//! // In this example, we will count solutions.
44
//! //
45
//! // The solver interface is designed to allow adaptors to use multiple-threads / processes if
46
//! // necessary. Therefore, the callback type requires all variables inside it to have a static
47
//! // lifetime and to implement Send (i.e. the variable can be safely shared between theads).
48
//! //
49
//! // We use Arc<Mutex<T>> to create multiple references to a threadsafe mutable
50
//! // variable of type T.
51
//! //
52
//! // Using the move |x| ... closure syntax, we move one of these references into the closure.
53
//! // Note that a normal closure borrow variables from the parent so is not
54
//! // thread-safe.
55
//!
56
//! let counter_ref = Arc::new(Mutex::new(0));
57
//! let counter_ref_2 = counter_ref.clone();
58
//! solver.solve(Box::new(move |_| {
59
//!   let mut counter = (*counter_ref_2).lock().unwrap();
60
//!   *counter += 1;
61
//!   true
62
//!   }));
63
//!
64
//! let mut counter = (*counter_ref).lock().unwrap();
65
//! assert_eq!(*counter,2);
66
//! ```
67
//!
68
//!
69

            
70
// # Implementing Solver interfaces
71
//
72
// Solver interfaces can only be implemented inside this module, due to the SolverAdaptor crate
73
// being sealed.
74
//
75
// To add support for a solver, implement the `SolverAdaptor` trait in a submodule.
76
//
77
// If incremental solving support is required, also implement a new `ModelModifier`. If this is not
78
// required, all `ModelModifier` instances required by the SolverAdaptor trait can be replaced with
79
// NotModifiable.
80
//
81
// For more details, see the docstrings for SolverAdaptor, ModelModifier, and NotModifiable.
82

            
83
#![allow(dead_code)]
84
#![allow(unused)]
85
#![allow(clippy::manual_non_exhaustive)]
86

            
87
use std::any::Any;
88
use std::cell::OnceCell;
89
use std::collections::HashMap;
90
use std::error::Error;
91
use std::fmt::{Debug, Display};
92
use std::rc::Rc;
93
use std::sync::{Arc, RwLock};
94
use std::time::Instant;
95

            
96
use schemars::JsonSchema;
97
use serde::{Deserialize, Serialize};
98
use strum_macros::{Display, EnumIter, EnumString};
99
use thiserror::Error;
100

            
101
use crate::ast::{Constant, Name};
102
use crate::context::Context;
103
use crate::stats::SolverStats;
104
use crate::Model;
105

            
106
use self::model_modifier::ModelModifier;
107
use self::states::*;
108

            
109
pub mod adaptors;
110
pub mod model_modifier;
111

            
112
#[doc(hidden)]
113
mod private;
114

            
115
pub mod states;
116

            
117
#[derive(
118
    Debug,
119
    EnumString,
120
    EnumIter,
121
    Display,
122
    PartialEq,
123
    Eq,
124
    Hash,
125
    Clone,
126
    Copy,
127
    Serialize,
128
    Deserialize,
129
    JsonSchema,
130
)]
131
pub enum SolverFamily {
132
    SAT,
133
    Minion,
134
}
135

            
136
/// The type for user-defined callbacks for use with [Solver].
137
///
138
/// Note that this enforces thread safety
139
pub type SolverCallback = Box<dyn Fn(HashMap<Name, Constant>) -> bool + Send>;
140
pub type SolverMutCallback =
141
    Box<dyn Fn(HashMap<Name, Constant>, Box<dyn ModelModifier>) -> bool + Send>;
142

            
143
/// A common interface for calling underlying solver APIs inside a [`Solver`].
144
///
145
/// Implementations of this trait aren't directly callable and should be used through [`Solver`] .
146
///
147
/// The below documentation lists the formal requirements that all implementations of
148
/// [`SolverAdaptor`] should follow - **see the top level module documentation and [`Solver`] for
149
/// usage details.**
150
///
151
/// # Encapsulation
152
///  
153
///  The [`SolverAdaptor`] trait **must** only be implemented inside a submodule of this one,
154
///  and **should** only be called through [`Solver`].
155
///
156
/// The `private::Sealed` trait and `private::Internal` type enforce these requirements by only
157
/// allowing trait implementations and calling of methods of SolverAdaptor to occur inside this
158
/// module.
159
///
160
/// # Thread Safety
161
///
162
/// Multiple instances of [`Solver`] can be run in parallel across multiple threads.
163
///
164
/// [`Solver`] provides no concurrency control or thread-safety; therefore, adaptors **must**
165
/// ensure that multiple instances of themselves can be ran in parallel. This applies to all
166
/// stages of solving including having two active `solve()` calls happening at a time, loading
167
/// a model while another is mid-solve, loading two models at once, etc.
168
///
169
/// A [SolverAdaptor] **may** use whatever threading or process model it likes underneath the hood,
170
/// as long as it obeys the above.
171
///
172
/// Method calls **should** block instead of erroring where possible.
173
///
174
/// Underlying solvers that only have one instance per process (such as Minion) **should** block
175
/// (eg. using a [`Mutex<()>`](`std::sync::Mutex`)) to run calls to
176
/// [`Solver<A,ModelLoaded>::solve()`] and [`Solver<A,ModelLoaded>::solve_mut()`] sequentially.
177
pub trait SolverAdaptor: private::Sealed + Any {
178
    /// Runs the solver on the given model.
179
    ///
180
    /// Implementations of this function **must** call the user provided callback whenever a solution
181
    /// is found. If the user callback returns `true`, search should continue, if the user callback
182
    /// returns `false`, search should terminate.
183
    ///
184
    /// # Returns
185
    ///
186
    /// If the solver terminates without crashing a [SolveSuccess] struct **must** returned. The
187
    /// value of [SearchStatus] can be used to denote whether the underlying solver completed its
188
    /// search or not. The latter case covers most non-crashing "failure" cases including user
189
    /// termination, timeouts, etc.
190
    ///
191
    /// To help populate [SearchStatus], it may be helpful to implement counters that track if the
192
    /// user callback has been called yet, and its return value. This information makes it is
193
    /// possible to distinguish between the most common search statuses:
194
    /// [SearchComplete::HasSolutions], [SearchComplete::NoSolutions], and
195
    /// [SearchIncomplete::UserTerminated].
196
    fn solve(
197
        &mut self,
198
        callback: SolverCallback,
199
        _: private::Internal,
200
    ) -> Result<SolveSuccess, SolverError>;
201

            
202
    /// Runs the solver on the given model, allowing modification of the model through a
203
    /// [`ModelModifier`].
204
    ///
205
    /// Implementations of this function **must** return [`OpNotSupported`](`ModificationFailure::OpNotSupported`)
206
    /// if modifying the model mid-search is not supported.
207
    ///
208
    /// Otherwise, this should work in the same way as [`solve`](SolverAdaptor::solve).
209
    fn solve_mut(
210
        &mut self,
211
        callback: SolverMutCallback,
212
        _: private::Internal,
213
    ) -> Result<SolveSuccess, SolverError>;
214
    fn load_model(&mut self, model: Model, _: private::Internal) -> Result<(), SolverError>;
215
    fn init_solver(&mut self, _: private::Internal) {}
216

            
217
    /// Get the solver family that this solver adaptor belongs to
218
    fn get_family(&self) -> SolverFamily;
219

            
220
    /// Gets the name of the solver adaptor for pretty printing.
221
    fn get_name(&self) -> Option<String> {
222
        None
223
    }
224

            
225
    /// Adds the solver adaptor name and family (if they exist) to the given stats object.
226
    fn add_adaptor_info_to_stats(&self, stats: SolverStats) -> SolverStats {
227
        SolverStats {
228
            solver_adaptor: self.get_name(),
229
            solver_family: Some(self.get_family()),
230
            ..stats
231
        }
232
    }
233
}
234

            
235
/// An abstract representation of a constraints solver.
236
///
237
/// [Solver] provides a common interface for interacting with a constraint solver. It also
238
/// abstracts over solver-specific datatypes, handling the translation to/from [conjure_core::ast]
239
/// types for a model and its solutions.
240
///
241
/// Details of how a model is solved is specified by the [SolverAdaptor]. This includes: the
242
/// underlying solver used, the translation of the model to a solver compatible form, how solutions
243
/// are translated back to [conjure_core::ast] types, and how incremental solving is implemented.
244
/// As such, there may be multiple [SolverAdaptor] implementations for a single underlying solver:
245
/// e.g. one adaptor may give solutions in a representation close to the solvers, while another may
246
/// attempt to rewrite it back into Essence.
247
///
248
#[derive(Clone)]
249
pub struct Solver<A: SolverAdaptor, State: SolverState = Init> {
250
    state: State,
251
    adaptor: A,
252
    context: Option<Arc<RwLock<Context<'static>>>>,
253
}
254

            
255
impl<Adaptor: SolverAdaptor> Solver<Adaptor> {
256
    pub fn new(solver_adaptor: Adaptor) -> Solver<Adaptor> {
257
        let mut solver = Solver {
258
            state: Init,
259
            adaptor: solver_adaptor,
260
            context: None,
261
        };
262

            
263
        solver.adaptor.init_solver(private::Internal);
264
        solver
265
    }
266

            
267
    pub fn get_family(&self) -> SolverFamily {
268
        self.adaptor.get_family()
269
    }
270
}
271

            
272
impl<A: SolverAdaptor> Solver<A, Init> {
273
    pub fn load_model(mut self, model: Model) -> Result<Solver<A, ModelLoaded>, SolverError> {
274
        let solver_model = &mut self.adaptor.load_model(model.clone(), private::Internal)?;
275
        Ok(Solver {
276
            state: ModelLoaded,
277
            adaptor: self.adaptor,
278
            context: Some(model.context.clone()),
279
        })
280
    }
281
}
282

            
283
impl<A: SolverAdaptor> Solver<A, ModelLoaded> {
284
    pub fn solve(
285
        mut self,
286
        callback: SolverCallback,
287
    ) -> Result<Solver<A, ExecutionSuccess>, SolverError> {
288
        #[allow(clippy::unwrap_used)]
289
        let start_time = Instant::now();
290

            
291
        #[allow(clippy::unwrap_used)]
292
        let result = self.adaptor.solve(callback, private::Internal);
293

            
294
        let duration = start_time.elapsed();
295

            
296
        match result {
297
            Ok(x) => {
298
                let stats = self
299
                    .adaptor
300
                    .add_adaptor_info_to_stats(x.stats)
301
                    .with_timings(duration.as_secs_f64());
302

            
303
                Ok(Solver {
304
                    adaptor: self.adaptor,
305
                    state: ExecutionSuccess {
306
                        stats,
307
                        status: x.status,
308
                        _sealed: private::Internal,
309
                    },
310
                    context: self.context,
311
                })
312
            }
313
            Err(x) => Err(x),
314
        }
315
    }
316

            
317
    pub fn solve_mut(
318
        mut self,
319
        callback: SolverMutCallback,
320
    ) -> Result<Solver<A, ExecutionSuccess>, SolverError> {
321
        #[allow(clippy::unwrap_used)]
322
        let start_time = Instant::now();
323

            
324
        #[allow(clippy::unwrap_used)]
325
        let result = self.adaptor.solve_mut(callback, private::Internal);
326

            
327
        let duration = start_time.elapsed();
328

            
329
        match result {
330
            Ok(x) => {
331
                let stats = self
332
                    .adaptor
333
                    .add_adaptor_info_to_stats(x.stats)
334
                    .with_timings(duration.as_secs_f64());
335

            
336
                Ok(Solver {
337
                    adaptor: self.adaptor,
338
                    state: ExecutionSuccess {
339
                        stats,
340
                        status: x.status,
341
                        _sealed: private::Internal,
342
                    },
343
                    context: self.context,
344
                })
345
            }
346
            Err(x) => Err(x),
347
        }
348
    }
349
}
350

            
351
impl<A: SolverAdaptor> Solver<A, ExecutionSuccess> {
352
    pub fn stats(&self) -> SolverStats {
353
        self.state.stats.clone()
354
    }
355

            
356
    // Saves this solvers stats to the global context as a "solver run"
357
    pub fn save_stats_to_context(&self) {
358
        #[allow(clippy::unwrap_used)]
359
        #[allow(clippy::expect_used)]
360
        self.context
361
            .as_ref()
362
            .expect("")
363
            .write()
364
            .unwrap()
365
            .stats
366
            .add_solver_run(self.stats());
367
    }
368

            
369
    pub fn wall_time_s(&self) -> f64 {
370
        self.stats().conjure_solver_wall_time_s
371
    }
372
}
373

            
374
/// Errors returned by [Solver] on failure.
375
#[non_exhaustive]
376
#[derive(Debug, Error, Clone)]
377
pub enum SolverError {
378
    #[error("operation not implemented yet: {0}")]
379
    OpNotImplemented(String),
380

            
381
    #[error("operation not supported: {0}")]
382
    OpNotSupported(String),
383

            
384
    #[error("model feature not supported: {0}")]
385
    ModelFeatureNotSupported(String),
386

            
387
    #[error("model feature not implemented yet: {0}")]
388
    ModelFeatureNotImplemented(String),
389

            
390
    // use for semantics / type errors, use the above for syntax
391
    #[error("model invalid: {0}")]
392
    ModelInvalid(String),
393

            
394
    #[error("error during solver execution: not implemented: {0}")]
395
    RuntimeNotImplemented(String),
396

            
397
    #[error("error during solver execution: {0}")]
398
    Runtime(String),
399
}
400

            
401
/// Returned from [SolverAdaptor] when solving is successful.
402
pub struct SolveSuccess {
403
    stats: SolverStats,
404
    status: SearchStatus,
405
}
406

            
407
pub enum SearchStatus {
408
    /// The search was complete (i.e. the solver found all possible solutions)
409
    Complete(SearchComplete),
410
    /// The search was incomplete (i.e. it was terminated before all solutions were found)
411
    Incomplete(SearchIncomplete),
412
}
413

            
414
#[non_exhaustive]
415
pub enum SearchIncomplete {
416
    Timeout,
417
    UserTerminated,
418
    #[doc(hidden)]
419
    /// This variant should not be matched - it exists to simulate non-exhaustiveness of this enum.
420
    __NonExhaustive,
421
}
422

            
423
#[non_exhaustive]
424
pub enum SearchComplete {
425
    HasSolutions,
426
    NoSolutions,
427
    #[doc(hidden)]
428
    /// This variant should not be matched - it exists to simulate non-exhaustiveness of this enum.
429
    __NonExhaustive,
430
}