Skip to main content

minion_sys/
run.rs

1#![allow(unreachable_patterns)]
2#![allow(unsafe_op_in_unsafe_fn)]
3
4use std::{
5    collections::HashMap,
6    ffi::{CStr, CString, c_char, c_void},
7};
8
9use anyhow::anyhow;
10
11use crate::{
12    ast::{Constant, Constraint, Model, Var, VarDomain, VarName},
13    error::{MinionError, check_minion_result},
14    scoped_ptr::Scoped,
15};
16use crate::{
17    ast::{ShortTuple, Tuple},
18    ffi::{self},
19};
20
21/// The callback type used by [`run_minion`].
22///
23/// Called by Minion whenever a solution is found. The input is
24/// a `HashMap` of all named variables along with their value.
25///
26/// Return `true` to continue searching, `false` to stop.
27///
28/// Since this is a boxed closure, it can capture state from its environment,
29/// eliminating the need for global or thread-local state in callers.
30///
31/// # Examples
32///
33/// ```
34///   use minion_sys::ast::*;
35///   use minion_sys::run_minion;
36///   use std::collections::HashMap;
37///
38///   let mut all_solutions: Vec<HashMap<VarName,Constant>> = vec![];
39///
40///   let callback = Box::new(|solutions: HashMap<VarName,Constant>| -> bool {
41///       all_solutions.push(solutions);
42///       true
43///   });
44///
45///   // Build and run the model.
46///   let mut model = Model::new();
47///
48///   // ... omitted for brevity ...
49/// # model
50/// #     .named_variables
51/// #     .add_var("x".to_owned(), VarDomain::Bound(1, 3));
52/// # model
53/// #     .named_variables
54/// #     .add_var("y".to_owned(), VarDomain::Bound(2, 4));
55/// # model
56/// #     .named_variables
57/// #     .add_var("z".to_owned(), VarDomain::Bound(1, 5));
58/// #
59/// # let leq = Constraint::SumLeq(
60/// #     vec![
61/// #         Var::NameRef("x".to_owned()),
62/// #         Var::NameRef("y".to_owned()),
63/// #         Var::NameRef("z".to_owned()),
64/// #     ],
65/// #     Var::ConstantAsVar(4),
66/// # );
67/// #
68/// # let geq = Constraint::SumGeq(
69/// #     vec![
70/// #         Var::NameRef("x".to_owned()),
71/// #         Var::NameRef("y".to_owned()),
72/// #         Var::NameRef("z".to_owned()),
73/// #     ],
74/// #     Var::ConstantAsVar(4),
75/// # );
76/// #
77/// # let ineq = Constraint::Ineq(
78/// #     Var::NameRef("x".to_owned()),
79/// #     Var::NameRef("y".to_owned()),
80/// #     Constant::Integer(-1),
81/// # );
82/// #
83/// # model.constraints.push(leq);
84/// # model.constraints.push(geq);
85/// # model.constraints.push(ineq);
86///
87///   let _solver_ctx = run_minion(model, callback).expect("Error occurred");
88///
89///   let solution_set_1 = &all_solutions[0];
90///   let x1 = solution_set_1.get("x").unwrap();
91///   let y1 = solution_set_1.get("y").unwrap();
92///   let z1 = solution_set_1.get("z").unwrap();
93/// #
94/// # assert_eq!(all_solutions.len(),1);
95/// # assert_eq!(*x1,Constant::Integer(1));
96/// # assert_eq!(*y1,Constant::Integer(2));
97/// # assert_eq!(*z1,Constant::Integer(1));
98/// ```
99pub type Callback<'a> = Box<dyn FnMut(HashMap<VarName, Constant>) -> bool + 'a>;
100
101/// Richer callback that also receives a [`MidSearchContext`] handle so
102/// the caller can add variables or constraints from inside a solution
103/// callback. See [`run_minion_midsearch`].
104pub type MidSearchCallback<'a> =
105    Box<dyn FnMut(&mut MidSearchContext<'_>, HashMap<VarName, Constant>) -> bool + 'a>;
106
107/// Handle for mutating the current search from inside a solution callback.
108///
109/// Callers receive a `&mut MidSearchContext` for the duration of a single
110/// callback invocation and can use it to add fresh variables or new
111/// constraints via the underlying `minion_newVarMidsearch` and
112/// `minion_addConstraintMidsearch` FFI calls. Any variables added this
113/// way are tracked so subsequent callbacks' solution maps include them.
114///
115/// The handle is not `Send` or `Sync` — it is only valid for the current
116/// callback invocation on the current thread.
117pub struct MidSearchContext<'a> {
118    ctx: *mut ffi::MinionContext,
119    instance: *mut ffi::ProbSpec_CSPInstance,
120    midsearch_vars: &'a mut Vec<VarName>,
121    _not_send: std::marker::PhantomData<*mut ()>,
122}
123
124impl MidSearchContext<'_> {
125    /// Add a fresh variable to the running search.
126    ///
127    /// The variable is branched on as an aux variable at the end of the
128    /// search order, so subsequent solutions will enumerate its values.
129    /// It is tracked so its value appears in the solution map of every
130    /// later callback.
131    pub fn add_var(&mut self, name: &str, domain: VarDomain) -> Result<(), MinionError> {
132        let c_name = CString::new(name)
133            .map_err(|_| anyhow!("Variable name {:?} contains a null character.", name))?;
134        unsafe {
135            match domain {
136                VarDomain::Bool => {
137                    check_minion_result(ffi::minion_newVarMidsearch(
138                        self.ctx,
139                        self.instance,
140                        c_name.as_ptr() as *mut c_char,
141                        ffi::VariableType_VAR_BOOL,
142                        0,
143                        1,
144                    ))?;
145                }
146                VarDomain::Bound(a, b) => {
147                    check_minion_result(ffi::minion_newVarMidsearch(
148                        self.ctx,
149                        self.instance,
150                        c_name.as_ptr() as *mut c_char,
151                        ffi::VariableType_VAR_BOUND,
152                        a,
153                        b,
154                    ))?;
155                }
156                VarDomain::Discrete(a, b) => {
157                    check_minion_result(ffi::minion_newVarMidsearch(
158                        self.ctx,
159                        self.instance,
160                        c_name.as_ptr() as *mut c_char,
161                        ffi::VariableType_VAR_DISCRETE,
162                        a,
163                        b,
164                    ))?;
165                }
166                VarDomain::SparseBound(ref vals) => {
167                    let raw = Scoped::new(ffi::vec_int_new(), |x| ffi::vec_int_free(x as _));
168                    for v in vals {
169                        ffi::vec_int_push_back(raw.ptr, *v);
170                    }
171                    check_minion_result(ffi::minion_newSparseBoundVarMidsearch(
172                        self.ctx,
173                        self.instance,
174                        c_name.as_ptr() as *mut c_char,
175                        raw.ptr,
176                    ))?;
177                }
178                x => return Err(MinionError::NotImplemented(format!("{x:?}"))),
179            }
180        }
181        self.midsearch_vars.push(name.to_owned());
182        Ok(())
183    }
184
185    /// Add a new constraint to the running search.
186    ///
187    /// The constraint is propagated immediately; an immediate
188    /// propagation wipeout is reported as a [`MinionError`].
189    pub fn add_constraint(&mut self, constraint: Constraint) -> Result<(), MinionError> {
190        unsafe {
191            let ct = get_constraint_type(&constraint)?;
192            let raw = Scoped::new(ffi::constraint_new(ct), |x| ffi::constraint_free(x as _));
193            constraint_add_args(self.instance, raw.ptr, &constraint)?;
194            check_minion_result(ffi::minion_addConstraintMidsearch(
195                self.ctx,
196                self.instance,
197                raw.ptr,
198            ))?;
199        }
200        Ok(())
201    }
202}
203
204/// State passed through the C callback's `void* userdata` pointer.
205///
206/// This replaces the old thread-local approach — all callback state is now
207/// passed explicitly through the FFI userdata mechanism.
208struct CallbackState<'a> {
209    callback: MidSearchCallback<'a>,
210    instance: *mut ffi::ProbSpec_CSPInstance,
211    /// Variables that exist in the print matrix (set up before `runMinion`).
212    /// Read via `printMatrix_getValue` by index.
213    print_vars: Vec<VarName>,
214    /// Variables added mid-search via [`MidSearchContext::add_var`]. These
215    /// are not in the print matrix, so we read them via
216    /// `minion_getVarValue` by name.
217    midsearch_vars: Vec<VarName>,
218}
219
220/// Opaque handle to a Minion solver context.
221///
222/// Holds solver state (including run statistics) after a solve completes.
223/// Query results (e.g. via [`SolverContext::get_from_table`]) before dropping.
224pub struct SolverContext {
225    ctx: *mut ffi::MinionContext,
226}
227
228// Safety: MinionContext is an independent solver instance. It is safe to send
229// between threads as long as it is not used concurrently (which we ensure by
230// only accessing it through &mut self or after the solve completes).
231unsafe impl Send for SolverContext {}
232
233impl SolverContext {
234    /// Gets a value from Minion's TableOut (where it stores run statistics).
235    pub fn get_from_table(&self, key: String) -> Option<String> {
236        unsafe {
237            #[allow(clippy::expect_used)]
238            let c_string = CString::new(key).expect("");
239            let key_ptr = c_string.into_raw();
240            let val_ptr: *mut c_char = ffi::TableOut_get(self.ctx, key_ptr);
241
242            drop(CString::from_raw(key_ptr));
243
244            if val_ptr.is_null() {
245                None
246            } else {
247                #[allow(clippy::unwrap_used)]
248                let res = CStr::from_ptr(val_ptr).to_str().unwrap().to_owned();
249                libc::free(val_ptr as _);
250                Some(res)
251            }
252        }
253    }
254}
255
256impl Drop for SolverContext {
257    fn drop(&mut self) {
258        unsafe {
259            ffi::minion_freeContext(self.ctx);
260        }
261    }
262}
263
264/// The C callback passed to `runMinion`. Receives the active context and our
265/// `CallbackState` via the userdata pointer.
266unsafe extern "C" fn run_callback(ctx: *mut ffi::MinionContext, userdata: *mut c_void) -> bool {
267    // Safety: userdata is a pointer to our CallbackState, set by run_minion
268    // and valid for the duration of the runMinion call.
269    let state = unsafe { &mut *(userdata as *mut CallbackState<'_>) };
270
271    // Build solutions HashMap by reading variable values from Minion.
272    let mut solutions: HashMap<VarName, Constant> = HashMap::new();
273
274    // Print-matrix variables: read by index — established before runMinion.
275    for (i, var) in state.print_vars.iter().enumerate() {
276        let v: i32 = unsafe { ffi::printMatrix_getValue(ctx, i as _) };
277        solutions.insert(var.clone(), Constant::Integer(v));
278    }
279
280    // Mid-search variables: not in the print matrix, read by name.
281    for var in state.midsearch_vars.iter() {
282        #[allow(clippy::unwrap_used)]
283        let c_name = CString::new(var.clone()).unwrap();
284        let v: i32 = unsafe { ffi::minion_getVarValue(ctx, state.instance, c_name.as_ptr()) };
285        solutions.insert(var.clone(), Constant::Integer(v));
286    }
287
288    let mut midctx = MidSearchContext {
289        ctx,
290        instance: state.instance,
291        midsearch_vars: &mut state.midsearch_vars,
292        _not_send: std::marker::PhantomData,
293    };
294    (state.callback)(&mut midctx, solutions)
295}
296
297/// Propagation strength, matching minion's `PropagationType`.
298///
299/// Applied either once at preprocess time (via
300/// [`RunOptions::preprocess`]) or at every search node (via
301/// [`RunOptions::prop_node`]). Strengthens from `None` (no
302/// propagation — only valid for preprocess, not prop_node) through
303/// `GAC` (generalised arc consistency, the default per-node level)
304/// up to `SSAC` (singleton-SAC).
305#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
306pub enum PropLevel {
307    #[default]
308    None,
309    Gac,
310    SacBounds,
311    Sac,
312    SsacBounds,
313    Ssac,
314}
315
316impl PropLevel {
317    fn to_ffi(self) -> ffi::PropagationType {
318        match self {
319            PropLevel::None => ffi::PropagationType_PropLevel_None,
320            PropLevel::Gac => ffi::PropagationType_PropLevel_GAC,
321            PropLevel::SacBounds => ffi::PropagationType_PropLevel_SACBounds,
322            PropLevel::Sac => ffi::PropagationType_PropLevel_SAC,
323            PropLevel::SsacBounds => ffi::PropagationType_PropLevel_SSACBounds,
324            PropLevel::Ssac => ffi::PropagationType_PropLevel_SSAC,
325        }
326    }
327}
328
329/// A propagation level together with minion's `_limit` modifier.
330///
331/// `limit=true` maps to the `*_limit` spellings in exec mode
332/// (`SAC_limit`, `SSACBounds_limit`, etc.), which cap the amount of
333/// work the propagator will do before giving up and falling back to
334/// the next-weaker level.
335#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
336pub struct Propagation {
337    pub level: PropLevel,
338    pub limit: bool,
339}
340
341impl Propagation {
342    fn to_ffi(self) -> ffi::PropagationLevel {
343        ffi::PropagationLevel {
344            type_: self.level.to_ffi(),
345            limit: self.limit,
346        }
347    }
348
349    pub fn is_default_preprocess(self) -> bool {
350        self.level == PropLevel::None && !self.limit
351    }
352
353    pub fn is_default_prop_node(self) -> bool {
354        self.level == PropLevel::Gac && !self.limit
355    }
356}
357
358/// Variable-ordering heuristic (maps to Minion's `VarOrderEnum`).
359///
360/// `Static` follows the variable declaration order and is state-free;
361/// the rest (Sdf, Srf, Ldf, Wdeg, DomOverWdeg, Conflict) depend on the
362/// current domain sizes / weight counters at each decision, so their
363/// solution order is sensitive to mid-search mutations. `Original`
364/// is a (non-state-dependent) alias minion uses internally.
365#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
366pub enum VarOrder {
367    #[default]
368    Static,
369    Sdf,
370    Srf,
371    Ldf,
372    Original,
373    Wdeg,
374    DomOverWdeg,
375    Conflict,
376}
377
378/// Value-ordering heuristic (maps to Minion's `ValOrderEnum`).
379///
380/// `Random` consumes the solver RNG, so two runs with the same seed
381/// reach the same leaves only if they made the same number of prior
382/// random draws — i.e. mid-search injection *will* diverge the
383/// solution order, even under `VarOrder::Static`.
384#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
385pub enum ValOrder {
386    #[default]
387    Ascend,
388    Descend,
389    Random,
390}
391
392/// Knobs for a single solve. Extend as needed — using a struct keeps the
393/// public call surface stable when we add more options.
394#[derive(Clone, Copy, Debug)]
395pub struct RunOptions {
396    /// Seed for Minion's random heuristics. `None` keeps Minion's own
397    /// default (currently `std::random_device{}()`, i.e. non-deterministic).
398    ///
399    /// Pass `Some(s)` to make a solve reproducible — tests that want to
400    /// diff two runs (e.g. "same prefix before the injection point")
401    /// must set the same seed for both runs.
402    pub seed: Option<u32>,
403
404    /// Variable-ordering heuristic. Defaults to [`VarOrder::Static`].
405    pub var_order: VarOrder,
406
407    /// Value-ordering heuristic. Defaults to [`ValOrder::Ascend`].
408    pub val_order: ValOrder,
409
410    /// Shuffle the variable order and force a random value order, on top
411    /// of `var_order`/`val_order`. Mirrors `-randomiseorder`, and is what
412    /// exec mode's `-varorder random` / `-varorder sdf-random` (etc.) set.
413    ///
414    /// This is orthogonal to `var_order`: minion shuffles the search order
415    /// once, when the instance is built, and overwrites every value order
416    /// with `VALORDER_RANDOM`. Set `seed` too if you want it reproducible.
417    pub randomise_order: bool,
418
419    /// One-shot propagation level applied before search begins.
420    /// Default is `None + no limit` — no preprocessing beyond each
421    /// constraint's own `fullPropagate`.
422    pub preprocess: Propagation,
423
424    /// Propagation level applied at every search node.
425    /// Default is `GAC + no limit` — each constraint runs its own
426    /// propagator. Stronger levels (SAC, SSAC) add global
427    /// propagation on top at every node; weaker levels (None) are
428    /// not meaningful here (a solver has to propagate *something*).
429    pub prop_node: Propagation,
430
431    /// Abort search after this many nodes have been explored. `0` (default)
432    /// means unlimited. Mirrors `-nodelimit N`.
433    ///
434    /// A truncated run still returns normally; compare the reported `Nodes`
435    /// against the limit to tell "finished" from "gave up".
436    pub node_limit: u64,
437
438    /// Abort search after this many seconds of wall-clock or CPU time.
439    /// `None` (default) means unlimited. `is_cpu_time = true` mirrors
440    /// `-cpulimit`; `false` mirrors `-timelimit`. C++ exec mode rejects
441    /// setting both; in this struct we accept whichever is non-None.
442    ///
443    /// Unlike `node_limit`, a run that hits the time limit comes back as
444    /// `RuntimeError::Timeout` — no context, and so no statistics.
445    pub time_limit: Option<TimeLimit>,
446}
447
448/// Time limit specification: a number of seconds and whether to measure
449/// CPU time (`-cpulimit`) or wall-clock time (`-timelimit`).
450///
451/// Both are armed process-wide, not per-solve: minion uses `alarm()` or
452/// `setrlimit(RLIMIT_CPU)` together with a process-static trigger pointer
453/// (`system/trigger_timer.cpp`), so concurrent solves in one process
454/// overwrite each other's timers. `is_cpu_time` additionally sets a hard
455/// RLIMIT_CPU of `seconds + 5` on the calling process, which will kill it
456/// outright — one solve per process is the only safe use.
457#[derive(Clone, Copy, Debug, PartialEq, Eq)]
458pub struct TimeLimit {
459    pub seconds: u32,
460    pub is_cpu_time: bool,
461}
462
463impl Default for RunOptions {
464    fn default() -> Self {
465        Self {
466            seed: None,
467            var_order: VarOrder::default(),
468            val_order: ValOrder::default(),
469            randomise_order: false,
470            preprocess: Propagation {
471                level: PropLevel::None,
472                limit: false,
473            },
474            prop_node: Propagation {
475                level: PropLevel::Gac,
476                limit: false,
477            },
478            node_limit: 0,
479            time_limit: None,
480        }
481    }
482}
483
484impl VarOrder {
485    fn to_ffi(self) -> ffi::VarOrderEnum {
486        match self {
487            VarOrder::Static => ffi::VarOrderEnum_ORDER_STATIC,
488            VarOrder::Sdf => ffi::VarOrderEnum_ORDER_SDF,
489            VarOrder::Srf => ffi::VarOrderEnum_ORDER_SRF,
490            VarOrder::Ldf => ffi::VarOrderEnum_ORDER_LDF,
491            VarOrder::Original => ffi::VarOrderEnum_ORDER_ORIGINAL,
492            VarOrder::Wdeg => ffi::VarOrderEnum_ORDER_WDEG,
493            VarOrder::DomOverWdeg => ffi::VarOrderEnum_ORDER_DOMOVERWDEG,
494            VarOrder::Conflict => ffi::VarOrderEnum_ORDER_CONFLICT,
495        }
496    }
497}
498
499impl ValOrder {
500    fn to_ffi(self) -> ffi::ValOrderEnum {
501        match self {
502            ValOrder::Ascend => ffi::ValOrderEnum_VALORDER_ASCEND,
503            ValOrder::Descend => ffi::ValOrderEnum_VALORDER_DESCEND,
504            ValOrder::Random => ffi::ValOrderEnum_VALORDER_RANDOM,
505        }
506    }
507}
508
509/// Run Minion on the given [Model].
510///
511/// The given [callback](Callback) is ran whenever a new solution set is found.
512///
513/// Returns a [`SolverContext`] on success, which can be used to query run
514/// statistics via [`SolverContext::get_from_table`].
515///
516/// For callbacks that need to add variables or constraints during search,
517/// use [`run_minion_midsearch`].
518pub fn run_minion(model: Model, callback: Callback<'_>) -> Result<SolverContext, MinionError> {
519    run_minion_with_options(model, RunOptions::default(), callback)
520}
521
522/// Like [`run_minion`] but lets the caller pin solver-side knobs
523/// (e.g. the random seed) via [`RunOptions`].
524pub fn run_minion_with_options(
525    model: Model,
526    options: RunOptions,
527    mut callback: Callback<'_>,
528) -> Result<SolverContext, MinionError> {
529    run_minion_midsearch_with_options(model, options, Box::new(move |_ctx, sol| callback(sol)))
530}
531
532/// Run Minion on the given [Model] with a callback that can mutate the
533/// running search via a [`MidSearchContext`] handle.
534pub fn run_minion_midsearch(
535    model: Model,
536    callback: MidSearchCallback<'_>,
537) -> Result<SolverContext, MinionError> {
538    run_minion_midsearch_with_options(model, RunOptions::default(), callback)
539}
540
541/// Callback type for parallel portfolio search.
542///
543/// The closure must be `Send + Sync` because it may be invoked from any of
544/// the worker threads. The C-side controller serialises invocations with a
545/// mutex (so two workers never call this concurrently) but Rust still needs
546/// the bound for soundness when we cross the FFI boundary from multiple
547/// threads.
548pub type ParallelCallback<'a> = Box<dyn FnMut(HashMap<VarName, Constant>) -> bool + Send + 'a>;
549
550/// State stored behind the parallel callback's userdata pointer. The mutex
551/// gives Rust ownership semantics on the FnMut even though the C-side
552/// already serialises calls — we need the &mut for the closure invocation.
553struct ParallelCallbackState<'a> {
554    callback: std::sync::Mutex<ParallelCallback<'a>>,
555    // Variables that are in the print matrix of every worker context. The
556    // print-matrix order is identical across workers because each worker
557    // builds its CSPInstance from the same shared description.
558    print_vars: Vec<VarName>,
559}
560
561unsafe extern "C" fn parallel_callback_thunk(
562    ctx: *mut ffi::MinionContext,
563    userdata: *mut c_void,
564) -> bool {
565    let state = unsafe { &*(userdata as *mut ParallelCallbackState<'_>) };
566
567    let mut solutions: HashMap<VarName, Constant> = HashMap::new();
568    for (i, var) in state.print_vars.iter().enumerate() {
569        let v: i32 = unsafe { ffi::printMatrix_getValue(ctx, i as _) };
570        solutions.insert(var.clone(), Constant::Integer(v));
571    }
572
573    // The C-side controller has already locked its callback mutex; this
574    // Rust mutex is therefore uncontended in practice. We still take it
575    // because Rust's borrow checker doesn't see the C-side serialisation.
576    #[allow(clippy::unwrap_used)]
577    let mut cb = state.callback.lock().unwrap();
578    cb(solutions)
579}
580
581/// Run Minion as a portfolio search across `num_threads` worker threads.
582///
583/// Each thread builds its own solver from a shared `Model`, with a derived
584/// random seed; the first thread to find `sollimit` solutions (or to prove
585/// unsat) signals the others to stop. The callback is invoked at most once
586/// per solution found by any worker, serialised internally so it never
587/// re-enters itself.
588///
589/// `num_threads` must be >= 1. With `num_threads == 1` behaviour is
590/// equivalent to a sequential `run_minion`.
591///
592/// Mid-search mutation is NOT supported in this mode (each worker has its
593/// own context; mutating one wouldn't propagate). Use the sequential
594/// [`run_minion_midsearch`] path if you need that.
595///
596/// # Concurrency
597///
598/// `run_minion_parallel` itself spawns OS threads internally. It is safe
599/// to call from a single thread of the host process per call, but it is
600/// **not** safe to invoke from multiple host threads concurrently — the
601/// underlying Minion alarm/ctrl-C handlers are per-process and racing
602/// threaded runs can corrupt them. (The fork-based `-parallel` flag has
603/// the same constraint at the process level.) For sequential use across
604/// many models, just call this function repeatedly.
605pub fn run_minion_parallel(
606    num_threads: usize,
607    model: Model,
608    callback: ParallelCallback<'_>,
609) -> Result<(), MinionError> {
610    run_minion_parallel_with_options(num_threads, model, RunOptions::default(), callback)
611}
612
613/// Run Minion with thread-based work-stealing: N workers cooperatively
614/// split the search tree. Worker 0 starts at the root; idle workers wait
615/// on a shared queue. Busy workers, on each search node, donate one
616/// stealable left-branch (encoded as a path-from-root) to the queue when
617/// other workers are idle. Idle workers fast-forward via worldPush +
618/// propagate replay and continue search from there.
619///
620/// Unlike [`run_minion_parallel`] (pure portfolio), this *divides* the
621/// search tree across workers and so speeds up UNSAT proving roughly with
622/// 1/N. Wdeg counters drift between workers — intended as a diversity
623/// source.
624///
625/// Mid-search mutation is not supported. See [`run_minion_parallel`] for
626/// the concurrency caveat (don't call this from multiple host threads
627/// concurrently — the underlying alarm/ctrl-C handler state is
628/// process-global).
629pub fn run_minion_work_steal(
630    num_threads: usize,
631    model: Model,
632    callback: ParallelCallback<'_>,
633) -> Result<WorkStealStats, MinionError> {
634    run_minion_work_steal_with_options(num_threads, model, RunOptions::default(), callback)
635}
636
637/// Aggregate work-stealing diagnostics, populated by
638/// [`run_minion_work_steal`] and [`run_minion_work_steal_with_options`].
639///
640/// `donations` counts donate() calls that fired (a busy worker handed off
641/// a sub-tree). `items_taken` counts work items popped and replayed by
642/// idle workers. `replay_failures` counts replays that found
643/// infeasibility before beginning sub-tree search. `total_nodes` is the
644/// sum of per-worker node counts.
645///
646/// `donations == 0` after a run means the donation/replay path was never
647/// exercised — typically because the search finished before idle workers
648/// reached their wait. Useful for confirming a test instance is large
649/// enough to stress the work-stealing protocol.
650///
651/// Contention diagnostics (cumulative across all workers, nanoseconds):
652/// `queue_lock_wait_nanos` is total time spent acquiring the donation
653/// queue mutex. `idle_wait_nanos` is total time blocked inside
654/// `popOrFinish` waiting for a donation — divide by `num_threads *
655/// wall_time` for "fraction of available CPU spent idle".
656/// `callback_lock_wait_nanos` is total time spent acquiring the
657/// per-solution lock; rises with enumeration-heavy SAT runs.
658#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
659pub struct WorkStealStats {
660    pub donations: i64,
661    pub items_taken: i64,
662    pub replay_failures: i64,
663    pub total_nodes: i64,
664    pub queue_lock_wait_nanos: i64,
665    pub idle_wait_nanos: i64,
666    pub callback_lock_wait_nanos: i64,
667}
668
669/// Like [`run_minion_work_steal`] but with [`RunOptions`].
670#[allow(clippy::unwrap_used)]
671pub fn run_minion_work_steal_with_options(
672    num_threads: usize,
673    model: Model,
674    options: RunOptions,
675    callback: ParallelCallback<'_>,
676) -> Result<WorkStealStats, MinionError> {
677    if num_threads == 0 {
678        return Err(MinionError::Other(anyhow!(
679            "num_threads must be at least 1"
680        )));
681    }
682    if num_threads > i32::MAX as usize {
683        return Err(MinionError::Other(anyhow!("num_threads is too large")));
684    }
685
686    unsafe {
687        let search_opts = ffi::searchOptions_new();
688        let search_method = ffi::searchMethod_new();
689        let search_instance = ffi::instance_new();
690
691        (*search_opts).randomiseValvarorder = options.randomise_order;
692        (*search_opts).silent = true;
693        (*search_opts).print_solution = false;
694        (*search_opts).sollimit = -1;
695
696        if let Some(seed) = options.seed {
697            // Identity at the default width; a widening conversion under the
698            // domains64 feature, where UnsignedSysInt is 64 bits.
699            #[allow(clippy::useless_conversion)]
700            {
701                (*search_method).randomSeed = seed.into();
702            }
703        }
704        (*search_method).preprocess = options.preprocess.to_ffi();
705        (*search_method).propMethod = options.prop_node.to_ffi();
706        if options.node_limit > 0 {
707            (*search_opts).nodelimit = options.node_limit as _;
708        }
709        if let Some(tl) = options.time_limit {
710            (*search_opts).timeoutActive = true;
711            (*search_opts).time_limit = tl.seconds as _;
712            (*search_opts).time_limit_is_CPUTime = tl.is_cpu_time;
713        }
714
715        let mut print_vars: Vec<VarName> = vec![];
716        let convert_result =
717            convert_model_to_raw(search_instance, &model, &options, &mut print_vars);
718        if let Err(e) = convert_result {
719            ffi::searchMethod_free(search_method);
720            ffi::searchOptions_free(search_opts);
721            ffi::instance_free(search_instance);
722            return Err(e);
723        }
724
725        let state = ParallelCallbackState {
726            callback: std::sync::Mutex::new(callback),
727            print_vars,
728        };
729        let userdata = &state as *const ParallelCallbackState<'_> as *mut c_void;
730
731        let cfg = ffi::MinionThreadConfig {
732            numThreads: num_threads as i32,
733            baseSeed: options.seed.unwrap_or(0),
734        };
735
736        let mut raw_stats = ffi::MinionWorkStealStats {
737            donations: 0,
738            itemsTaken: 0,
739            replayFailures: 0,
740            totalNodes: 0,
741            queueLockWaitNanos: 0,
742            idleWaitNanos: 0,
743            callbackLockWaitNanos: 0,
744        };
745
746        let res = ffi::runMinionWorkSteal(
747            cfg,
748            search_opts,
749            search_method,
750            search_instance,
751            Some(parallel_callback_thunk),
752            userdata,
753            &mut raw_stats,
754        );
755
756        ffi::searchMethod_free(search_method);
757        ffi::searchOptions_free(search_opts);
758        ffi::instance_free(search_instance);
759
760        check_minion_result(res)?;
761        Ok(WorkStealStats {
762            donations: raw_stats.donations,
763            items_taken: raw_stats.itemsTaken,
764            replay_failures: raw_stats.replayFailures,
765            total_nodes: raw_stats.totalNodes,
766            queue_lock_wait_nanos: raw_stats.queueLockWaitNanos,
767            idle_wait_nanos: raw_stats.idleWaitNanos,
768            callback_lock_wait_nanos: raw_stats.callbackLockWaitNanos,
769        })
770    }
771}
772
773/// Like [`run_minion_parallel`] but with [`RunOptions`] (random seed, var/
774/// val orderings, propagation levels). The seed (if set) is the controller
775/// base seed; per-thread seeds are derived as `base XOR thread_index`.
776#[allow(clippy::unwrap_used)]
777pub fn run_minion_parallel_with_options(
778    num_threads: usize,
779    model: Model,
780    options: RunOptions,
781    callback: ParallelCallback<'_>,
782) -> Result<(), MinionError> {
783    if num_threads == 0 {
784        return Err(MinionError::Other(anyhow!(
785            "num_threads must be at least 1"
786        )));
787    }
788    if num_threads > i32::MAX as usize {
789        return Err(MinionError::Other(anyhow!("num_threads is too large")));
790    }
791
792    unsafe {
793        let search_opts = ffi::searchOptions_new();
794        let search_method = ffi::searchMethod_new();
795        let search_instance = ffi::instance_new();
796
797        // Quiet by default: workers don't print to cout under runMinionParallel
798        // (they observe getOptions().silent = true via the per-thread copy).
799        // The user receives results via the callback.
800        (*search_opts).randomiseValvarorder = options.randomise_order;
801        (*search_opts).silent = true;
802        (*search_opts).print_solution = false;
803        // Library convention: enumerate all solutions and let the caller's
804        // callback decide when to stop by returning false. The C-side
805        // controller enforces this `sollimit = -1` shared value.
806        (*search_opts).sollimit = -1;
807
808        if let Some(seed) = options.seed {
809            // Identity at the default width; a widening conversion under the
810            // domains64 feature, where UnsignedSysInt is 64 bits.
811            #[allow(clippy::useless_conversion)]
812            {
813                (*search_method).randomSeed = seed.into();
814            }
815        }
816        (*search_method).preprocess = options.preprocess.to_ffi();
817        (*search_method).propMethod = options.prop_node.to_ffi();
818        if options.node_limit > 0 {
819            (*search_opts).nodelimit = options.node_limit as _;
820        }
821        if let Some(tl) = options.time_limit {
822            (*search_opts).timeoutActive = true;
823            (*search_opts).time_limit = tl.seconds as _;
824            (*search_opts).time_limit_is_CPUTime = tl.is_cpu_time;
825        }
826
827        let mut print_vars: Vec<VarName> = vec![];
828        let convert_result =
829            convert_model_to_raw(search_instance, &model, &options, &mut print_vars);
830        if let Err(e) = convert_result {
831            ffi::searchMethod_free(search_method);
832            ffi::searchOptions_free(search_opts);
833            ffi::instance_free(search_instance);
834            return Err(e);
835        }
836
837        let state = ParallelCallbackState {
838            callback: std::sync::Mutex::new(callback),
839            print_vars,
840        };
841        let userdata = &state as *const ParallelCallbackState<'_> as *mut c_void;
842
843        let cfg = ffi::MinionThreadConfig {
844            numThreads: num_threads as i32,
845            baseSeed: options.seed.unwrap_or(0),
846        };
847
848        let res = ffi::runMinionParallel(
849            cfg,
850            search_opts,
851            search_method,
852            search_instance,
853            Some(parallel_callback_thunk),
854            userdata,
855        );
856
857        ffi::searchMethod_free(search_method);
858        ffi::searchOptions_free(search_opts);
859        ffi::instance_free(search_instance);
860
861        check_minion_result(res)?;
862        Ok(())
863    }
864}
865
866/// Like [`run_minion_midsearch`] but with [`RunOptions`].
867#[allow(clippy::unwrap_used)]
868pub fn run_minion_midsearch_with_options(
869    model: Model,
870    options: RunOptions,
871    callback: MidSearchCallback<'_>,
872) -> Result<SolverContext, MinionError> {
873    unsafe {
874        let ctx = ffi::minion_newContext();
875        let search_opts = ffi::searchOptions_new();
876        let search_method = ffi::searchMethod_new();
877        let search_instance = ffi::instance_new();
878
879        // Use Minion as a quiet library by default. Low-level FFI callers that
880        // want native solver output can opt out by configuring SearchOptions
881        // themselves instead of going through this wrapper.
882        (*search_opts).randomiseValvarorder = options.randomise_order;
883        (*search_opts).silent = true;
884        (*search_opts).print_solution = false;
885
886        if let Some(seed) = options.seed {
887            // Identity at the default width; a widening conversion under the
888            // domains64 feature, where UnsignedSysInt is 64 bits.
889            #[allow(clippy::useless_conversion)]
890            {
891                (*search_method).randomSeed = seed.into();
892            }
893        }
894        (*search_method).preprocess = options.preprocess.to_ffi();
895        (*search_method).propMethod = options.prop_node.to_ffi();
896        if options.node_limit > 0 {
897            (*search_opts).nodelimit = options.node_limit as _;
898        }
899        if let Some(tl) = options.time_limit {
900            (*search_opts).timeoutActive = true;
901            (*search_opts).time_limit = tl.seconds as _;
902            (*search_opts).time_limit_is_CPUTime = tl.is_cpu_time;
903        }
904
905        let mut state = CallbackState {
906            callback,
907            instance: search_instance,
908            print_vars: vec![],
909            midsearch_vars: vec![],
910        };
911
912        convert_model_to_raw(search_instance, &model, &options, &mut state.print_vars)?;
913
914        let userdata = &mut state as *mut CallbackState<'_> as *mut c_void;
915        let res = ffi::runMinion(
916            ctx,
917            search_opts,
918            search_method,
919            search_instance,
920            Some(run_callback),
921            userdata,
922        );
923
924        ffi::searchMethod_free(search_method);
925        ffi::searchOptions_free(search_opts);
926        ffi::instance_free(search_instance);
927
928        match check_minion_result(res) {
929            Ok(()) => Ok(SolverContext { ctx }),
930            Err(e) => {
931                ffi::minion_freeContext(ctx);
932                Err(MinionError::from(e))
933            }
934        }
935    }
936}
937
938unsafe fn convert_model_to_raw(
939    instance: *mut ffi::ProbSpec_CSPInstance,
940    model: &Model,
941    options: &RunOptions,
942    print_vars: &mut Vec<VarName>,
943) -> Result<(), MinionError> {
944    /*******************************/
945    /*        Add variables        */
946    /*******************************/
947
948    /*
949     * Add variables to:
950     * 1. symbol table
951     * 2. print matrix
952     * 3. search vars
953     *
954     * These are all done in the order saved in the SymbolTable.
955     */
956
957    let search_vars = Scoped::new(ffi::vec_var_new(), |x| ffi::vec_var_free(x as _));
958
959    // initialise all variables, and add all variables to the print order
960    for var_name in model.named_variables.get_variable_order() {
961        let c_str = CString::new(var_name.clone()).map_err(|_| {
962            anyhow!(
963                "Variable name {:?} contains a null character.",
964                var_name.clone()
965            )
966        })?;
967
968        let vartype = model
969            .named_variables
970            .get_vartype(var_name.clone())
971            .ok_or(anyhow!("Could not get var type for {:?}", var_name.clone()))?;
972
973        match vartype {
974            VarDomain::Bound(a, b) => {
975                check_minion_result(ffi::minion_newVar(
976                    instance,
977                    c_str.as_ptr() as *mut c_char,
978                    ffi::VariableType_VAR_BOUND,
979                    a,
980                    b,
981                ))?;
982            }
983            VarDomain::Discrete(a, b) => {
984                check_minion_result(ffi::minion_newVar(
985                    instance,
986                    c_str.as_ptr() as *mut c_char,
987                    ffi::VariableType_VAR_DISCRETE,
988                    a,
989                    b,
990                ))?;
991            }
992            VarDomain::Bool => {
993                check_minion_result(ffi::minion_newVar(
994                    instance,
995                    c_str.as_ptr() as *mut c_char,
996                    ffi::VariableType_VAR_BOOL,
997                    0,
998                    1,
999                ))?;
1000            }
1001            VarDomain::SparseBound(ref vals) => {
1002                let raw = Scoped::new(ffi::vec_int_new(), |x| ffi::vec_int_free(x as _));
1003                for v in vals {
1004                    ffi::vec_int_push_back(raw.ptr, *v);
1005                }
1006                check_minion_result(ffi::minion_newSparseBoundVar(
1007                    instance,
1008                    c_str.as_ptr() as *mut c_char,
1009                    raw.ptr,
1010                ))?;
1011            }
1012            x => return Err(MinionError::NotImplemented(format!("{x:?}"))),
1013        };
1014
1015        let var_result = ffi::minion_getVarByName(instance, c_str.as_ptr() as _);
1016        check_minion_result(var_result.result)?;
1017        let var = var_result.var;
1018
1019        ffi::printMatrix_addVar(instance, var);
1020
1021        // Remember the order for the callback function.
1022        print_vars.push(var_name.clone());
1023    }
1024
1025    // only add search variables to search order
1026    let primary_search_var_names: std::collections::HashSet<VarName> = model
1027        .named_variables
1028        .get_search_variable_order()
1029        .into_iter()
1030        .collect();
1031    for search_var_name in model.named_variables.get_search_variable_order() {
1032        let c_str = CString::new(search_var_name.clone()).map_err(|_| {
1033            anyhow!(
1034                "Variable name {:?} contains a null character.",
1035                search_var_name.clone()
1036            )
1037        })?;
1038        let var_result = ffi::minion_getVarByName(instance, c_str.as_ptr() as _);
1039        check_minion_result(var_result.result)?;
1040        ffi::vec_var_push_back(search_vars.ptr, var_result.var);
1041    }
1042
1043    let search_order = Scoped::new(
1044        ffi::searchOrder_new(search_vars.ptr, options.var_order.to_ffi(), false),
1045        |x| ffi::searchOrder_free(x as _),
1046    );
1047    // Minion's text parser defaults every variable's value-ordering to
1048    // ASCEND via `SearchOrder::setupValueOrder` during BuildCSP. To set
1049    // DESCEND or RANDOM from the library path we have to fill the
1050    // per-variable valOrder vector ourselves before BuildCSP runs.
1051    ffi::searchOrder_setValOrder(search_order.ptr, options.val_order.to_ffi());
1052
1053    ffi::instance_addSearchOrder(instance, search_order.ptr);
1054
1055    // Minion's text parser appends every variable omitted from VARORDER as a
1056    // find-one-assignment auxiliary search block. Models built through this
1057    // library bypass that finalisation step, so reproduce it here. Without
1058    // this block Minion reports a solution while auxiliary variables are
1059    // still unassigned -- in practice it reports no solutions at all -- and
1060    // the callback observes their lower bounds even where those violate
1061    // constraints.
1062    let auxiliary_search_vars = Scoped::new(ffi::vec_var_new(), |x| ffi::vec_var_free(x as _));
1063    let mut has_auxiliary_search_vars = false;
1064    for auxiliary_name in model
1065        .named_variables
1066        .get_variable_order()
1067        .into_iter()
1068        .filter(|name| !primary_search_var_names.contains(name))
1069    {
1070        let c_str = CString::new(auxiliary_name.clone())
1071            .map_err(|_| anyhow!("Variable name {auxiliary_name:?} contains a null character"))?;
1072        let var_result = ffi::minion_getVarByName(instance, c_str.as_ptr() as _);
1073        check_minion_result(var_result.result)?;
1074        ffi::vec_var_push_back(auxiliary_search_vars.ptr, var_result.var);
1075        has_auxiliary_search_vars = true;
1076    }
1077    if has_auxiliary_search_vars {
1078        let auxiliary_search_order = Scoped::new(
1079            ffi::searchOrder_new(
1080                auxiliary_search_vars.ptr,
1081                ffi::VarOrderEnum_ORDER_STATIC,
1082                true,
1083            ),
1084            |x| ffi::searchOrder_free(x as _),
1085        );
1086        ffi::instance_addSearchOrder(instance, auxiliary_search_order.ptr);
1087    }
1088
1089    /*********************************/
1090    /*        Add tuple tables       */
1091    /*********************************/
1092    //
1093    // Must happen BEFORE add-constraint: constraints like
1094    // `Str2Plus(vars, Var::NameRef(table_name))` look the table up
1095    // by name on the instance.
1096
1097    for (name, tuples) in &model.tuple_tables {
1098        let c_name = CString::new(name.clone()).map_err(|_| {
1099            anyhow!(
1100                "Tuple-table name {:?} contains a null character.",
1101                name.clone()
1102            )
1103        })?;
1104
1105        // Build the raw Vec<Vec<DomainInt>> to hand to tupleList_new.
1106        // tupleList_new takes ownership semantically (it constructs a
1107        // TupleList from the contents); the vec_vec_int_new/free
1108        // pair only cleans up the intermediate carrier.
1109        let raw_tuples = Scoped::new(ffi::vec_vec_int_new(), |x| ffi::vec_vec_int_free(x as _));
1110        for tuple in tuples {
1111            let raw_tuple = Scoped::new(ffi::vec_int_new(), |x| ffi::vec_int_free(x as _));
1112            for constant in tuple {
1113                let val = match constant {
1114                    Constant::Integer(n) => *n,
1115                    Constant::Bool(true) => 1,
1116                    Constant::Bool(false) => 0,
1117                    #[allow(unreachable_patterns)]
1118                    x => return Err(MinionError::NotImplemented(format!("{x:?}"))),
1119                };
1120                ffi::vec_int_push_back(raw_tuple.ptr, val);
1121            }
1122            ffi::vec_vec_int_push_back_ptr(raw_tuples.ptr, raw_tuple.ptr);
1123        }
1124
1125        // `instance_addTupleTableSymbol` copies the name and takes
1126        // ownership of the TupleList via `shared_ptr<TupleList>`,
1127        // so we do NOT wrap the result of `tupleList_new` in Scoped.
1128        let raw_tuple_list = ffi::tupleList_new(raw_tuples.ptr);
1129        ffi::instance_addTupleTableSymbol(instance, c_name.as_ptr() as *mut c_char, raw_tuple_list);
1130    }
1131
1132    /*********************************/
1133    /*        Add constraints        */
1134    /*********************************/
1135
1136    for constraint in &model.constraints {
1137        // 1. get constraint type and create C++ constraint object
1138        // 2. run through arguments and add them to the constraint
1139        // 3. add constraint to instance
1140
1141        let constraint_type = get_constraint_type(constraint)?;
1142        let raw_constraint = Scoped::new(ffi::constraint_new(constraint_type), |x| {
1143            ffi::constraint_free(x as _)
1144        });
1145
1146        constraint_add_args(instance, raw_constraint.ptr, constraint)?;
1147        ffi::instance_addConstraint(instance, raw_constraint.ptr);
1148    }
1149
1150    /**********************************/
1151    /*       Optimisation directive   */
1152    /**********************************/
1153    if let Some(opt) = &model.optimise {
1154        let mut raw_var = resolve_var(instance, &opt.var)?;
1155        ffi::instance_setOptimise(instance, opt.minimise, &mut raw_var);
1156    }
1157
1158    Ok(())
1159}
1160
1161unsafe fn get_constraint_type(constraint: &Constraint) -> Result<u32, MinionError> {
1162    match constraint {
1163        Constraint::SumGeq(_, _) => Ok(ffi::ConstraintType_CT_GEQSUM),
1164        Constraint::SumLeq(_, _) => Ok(ffi::ConstraintType_CT_LEQSUM),
1165        Constraint::Ineq(_, _, _) => Ok(ffi::ConstraintType_CT_INEQ),
1166        Constraint::Eq(_, _) => Ok(ffi::ConstraintType_CT_EQ),
1167        Constraint::Difference(_, _) => Ok(ffi::ConstraintType_CT_DIFFERENCE),
1168        Constraint::Div(_, _) => Ok(ffi::ConstraintType_CT_DIV),
1169        Constraint::DivUndefZero(_, _) => Ok(ffi::ConstraintType_CT_DIV_UNDEFZERO),
1170        Constraint::Modulo(_, _) => Ok(ffi::ConstraintType_CT_MODULO),
1171        Constraint::ModuloUndefZero(_, _) => Ok(ffi::ConstraintType_CT_MODULO_UNDEFZERO),
1172        Constraint::Pow(_, _) => Ok(ffi::ConstraintType_CT_POW),
1173        Constraint::Product(_, _) => Ok(ffi::ConstraintType_CT_PRODUCT2),
1174        Constraint::WeightedSumGeq(_, _, _) => Ok(ffi::ConstraintType_CT_WEIGHTGEQSUM),
1175        Constraint::WeightedSumLeq(_, _, _) => Ok(ffi::ConstraintType_CT_WEIGHTLEQSUM),
1176        Constraint::CheckAssign(_) => Ok(ffi::ConstraintType_CT_CHECK_ASSIGN),
1177        Constraint::CheckGsa(_) => Ok(ffi::ConstraintType_CT_CHECK_GSA),
1178        Constraint::ForwardChecking(_) => Ok(ffi::ConstraintType_CT_FORWARD_CHECKING),
1179        Constraint::Reify(_, _) => Ok(ffi::ConstraintType_CT_REIFY),
1180        Constraint::ReifyImply(_, _) => Ok(ffi::ConstraintType_CT_REIFYIMPLY),
1181        Constraint::ReifyImplyQuick(_, _) => Ok(ffi::ConstraintType_CT_REIFYIMPLY_QUICK),
1182        Constraint::WatchedAnd(_) => Ok(ffi::ConstraintType_CT_WATCHED_NEW_AND),
1183        Constraint::WatchedOr(_) => Ok(ffi::ConstraintType_CT_WATCHED_NEW_OR),
1184        Constraint::GacAllDiff(_) => Ok(ffi::ConstraintType_CT_GACALLDIFF),
1185        Constraint::AllDiff(_) => Ok(ffi::ConstraintType_CT_ALLDIFF),
1186        Constraint::AllDiffMatrix(_, _) => Ok(ffi::ConstraintType_CT_ALLDIFFMATRIX),
1187        Constraint::WatchSumGeq(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_GEQSUM),
1188        Constraint::WatchSumLeq(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_LEQSUM),
1189        Constraint::OccurrenceGeq(_, _, _) => Ok(ffi::ConstraintType_CT_GEQ_OCCURRENCE),
1190        Constraint::OccurrenceLeq(_, _, _) => Ok(ffi::ConstraintType_CT_LEQ_OCCURRENCE),
1191        Constraint::Occurrence(_, _, _) => Ok(ffi::ConstraintType_CT_OCCURRENCE),
1192        Constraint::LitSumGeq(_, _, _) => Ok(ffi::ConstraintType_CT_WATCHED_LITSUM),
1193        Constraint::Gcc(_, _, _) => Ok(ffi::ConstraintType_CT_GCC),
1194        Constraint::GccWeak(_, _, _) => Ok(ffi::ConstraintType_CT_GCCWEAK),
1195        Constraint::LexLeqRv(_, _) => Ok(ffi::ConstraintType_CT_GACLEXLEQ),
1196        Constraint::LexLeq(_, _) => Ok(ffi::ConstraintType_CT_LEXLEQ),
1197        Constraint::LexLess(_, _) => Ok(ffi::ConstraintType_CT_LEXLESS),
1198        Constraint::LexLeqQuick(_, _) => Ok(ffi::ConstraintType_CT_QUICK_LEXLEQ),
1199        Constraint::LexLessQuick(_, _) => Ok(ffi::ConstraintType_CT_QUICK_LEXLESS),
1200        Constraint::WatchVecNeq(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_VECNEQ),
1201        Constraint::WatchVecExistsLess(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_VEC_OR_LESS),
1202        Constraint::Hamming(_, _, _) => Ok(ffi::ConstraintType_CT_WATCHED_HAMMING),
1203        Constraint::NotHamming(_, _, _) => Ok(ffi::ConstraintType_CT_WATCHED_NOT_HAMMING),
1204        Constraint::FrameUpdate(_, _, _, _, _) => Ok(ffi::ConstraintType_CT_FRAMEUPDATE),
1205        Constraint::NegativeTable(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_NEGATIVE_TABLE),
1206        Constraint::Table(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_TABLE),
1207        Constraint::GacSchema(_, _) => Ok(ffi::ConstraintType_CT_GACSCHEMA),
1208        Constraint::LightTable(_, _) => Ok(ffi::ConstraintType_CT_LIGHTTABLE),
1209        Constraint::Mddc(_, _) => Ok(ffi::ConstraintType_CT_MDDC),
1210        Constraint::NegativeMddc(_, _) => Ok(ffi::ConstraintType_CT_NEGATIVEMDDC),
1211        Constraint::Str2Plus(_, _) => Ok(ffi::ConstraintType_CT_STR),
1212        Constraint::ShortStr2(_, _) => Ok(ffi::ConstraintType_CT_SHORTSTR),
1213        Constraint::HaggisGac(_, _) => Ok(ffi::ConstraintType_CT_HAGGISGAC),
1214        Constraint::HaggisGacStable(_, _) => Ok(ffi::ConstraintType_CT_HAGGISGAC_STABLE),
1215        Constraint::ShortCTupleStr2(_, _) => Ok(ffi::ConstraintType_CT_SHORTSTR_CTUPLE),
1216        Constraint::Max(_, _) => Ok(ffi::ConstraintType_CT_MAX),
1217        Constraint::Min(_, _) => Ok(ffi::ConstraintType_CT_MIN),
1218        Constraint::NvalueGeq(_, _) => Ok(ffi::ConstraintType_CT_GEQNVALUE),
1219        Constraint::NvalueLeq(_, _) => Ok(ffi::ConstraintType_CT_LEQNVALUE),
1220        Constraint::Element(_, _, _) => Ok(ffi::ConstraintType_CT_ELEMENT),
1221        Constraint::ElementOne(_, _, _) => Ok(ffi::ConstraintType_CT_ELEMENT_ONE),
1222        Constraint::ElementUndefZero(_, _, _) => Ok(ffi::ConstraintType_CT_ELEMENT_UNDEFZERO),
1223        Constraint::WatchElement(_, _, _) => Ok(ffi::ConstraintType_CT_WATCHED_ELEMENT),
1224        Constraint::WatchElementOne(_, _, _) => Ok(ffi::ConstraintType_CT_WATCHED_ELEMENT_ONE),
1225        Constraint::WatchElementOneUndefZero(_, _, _) => {
1226            Ok(ffi::ConstraintType_CT_WATCHED_ELEMENT_ONE_UNDEFZERO)
1227        }
1228        Constraint::WatchElementUndefZero(_, _, _) => {
1229            Ok(ffi::ConstraintType_CT_WATCHED_ELEMENT_UNDEFZERO)
1230        }
1231        Constraint::WLiteral(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_LIT),
1232        Constraint::WNotLiteral(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_NOTLIT),
1233        Constraint::WInIntervalSet(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_ININTERVALSET),
1234        Constraint::WInRange(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_INRANGE),
1235        Constraint::WInset(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_INSET),
1236        Constraint::WNotInRange(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_NOT_INRANGE),
1237        Constraint::WNotInset(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_NOT_INSET),
1238        Constraint::Abs(_, _) => Ok(ffi::ConstraintType_CT_ABS),
1239        Constraint::DisEq(_, _) => Ok(ffi::ConstraintType_CT_DISEQ),
1240        Constraint::MinusEq(_, _) => Ok(ffi::ConstraintType_CT_MINUSEQ),
1241        Constraint::GacEq(_, _) => Ok(ffi::ConstraintType_CT_GACEQ),
1242        Constraint::WatchLess(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_LESS),
1243        Constraint::WatchNeq(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_NEQ),
1244        Constraint::True => Ok(ffi::ConstraintType_CT_TRUE),
1245        Constraint::False => Ok(ffi::ConstraintType_CT_FALSE),
1246
1247        #[allow(unreachable_patterns)]
1248        x => Err(MinionError::NotImplemented(format!(
1249            "Constraint not implemented {x:?}",
1250        ))),
1251    }
1252}
1253
1254unsafe fn constraint_add_args(
1255    i: *mut ffi::ProbSpec_CSPInstance,
1256    r_constr: *mut ffi::ProbSpec_ConstraintBlob,
1257    constr: &Constraint,
1258) -> Result<(), MinionError> {
1259    match constr {
1260        Constraint::SumGeq(lhs_vars, rhs_var) => {
1261            read_list(i, r_constr, lhs_vars)?;
1262            read_var(i, r_constr, rhs_var)?;
1263            Ok(())
1264        }
1265        Constraint::SumLeq(lhs_vars, rhs_var) => {
1266            read_list(i, r_constr, lhs_vars)?;
1267            read_var(i, r_constr, rhs_var)?;
1268            Ok(())
1269        }
1270        Constraint::Ineq(var1, var2, c) => {
1271            read_var(i, r_constr, var1)?;
1272            read_var(i, r_constr, var2)?;
1273            read_constant(r_constr, c)?;
1274            Ok(())
1275        }
1276        Constraint::Eq(var1, var2) => {
1277            read_var(i, r_constr, var1)?;
1278            read_var(i, r_constr, var2)?;
1279            Ok(())
1280        }
1281        Constraint::Difference((a, b), c) => {
1282            read_2_vars(i, r_constr, a, b)?;
1283            read_var(i, r_constr, c)?;
1284            Ok(())
1285        }
1286        Constraint::Div((a, b), c) => {
1287            read_2_vars(i, r_constr, a, b)?;
1288            read_var(i, r_constr, c)?;
1289            Ok(())
1290        }
1291        Constraint::DivUndefZero((a, b), c) => {
1292            read_2_vars(i, r_constr, a, b)?;
1293            read_var(i, r_constr, c)?;
1294            Ok(())
1295        }
1296        Constraint::Modulo((a, b), c) => {
1297            read_2_vars(i, r_constr, a, b)?;
1298            read_var(i, r_constr, c)?;
1299            Ok(())
1300        }
1301        Constraint::ModuloUndefZero((a, b), c) => {
1302            read_2_vars(i, r_constr, a, b)?;
1303            read_var(i, r_constr, c)?;
1304            Ok(())
1305        }
1306        Constraint::Pow((a, b), c) => {
1307            read_2_vars(i, r_constr, a, b)?;
1308            read_var(i, r_constr, c)?;
1309            Ok(())
1310        }
1311        Constraint::Product((a, b), c) => {
1312            read_2_vars(i, r_constr, a, b)?;
1313            read_var(i, r_constr, c)?;
1314            Ok(())
1315        }
1316        Constraint::WeightedSumGeq(a, b, c) => {
1317            read_constant_list(r_constr, a)?;
1318            read_list(i, r_constr, b)?;
1319            read_var(i, r_constr, c)?;
1320            Ok(())
1321        }
1322        Constraint::WeightedSumLeq(a, b, c) => {
1323            read_constant_list(r_constr, a)?;
1324            read_list(i, r_constr, b)?;
1325            read_var(i, r_constr, c)?;
1326            Ok(())
1327        }
1328        Constraint::CheckAssign(a) => {
1329            read_constraint(i, r_constr, (**a).clone())?;
1330            Ok(())
1331        }
1332        Constraint::CheckGsa(a) => {
1333            read_constraint(i, r_constr, (**a).clone())?;
1334            Ok(())
1335        }
1336        Constraint::ForwardChecking(a) => {
1337            read_constraint(i, r_constr, (**a).clone())?;
1338            Ok(())
1339        }
1340        Constraint::Reify(a, b) => {
1341            read_constraint(i, r_constr, (**a).clone())?;
1342            read_var(i, r_constr, b)?;
1343            Ok(())
1344        }
1345        Constraint::ReifyImply(a, b) => {
1346            read_constraint(i, r_constr, (**a).clone())?;
1347            read_var(i, r_constr, b)?;
1348            Ok(())
1349        }
1350        Constraint::ReifyImplyQuick(a, b) => {
1351            read_constraint(i, r_constr, (**a).clone())?;
1352            read_var(i, r_constr, b)?;
1353            Ok(())
1354        }
1355        Constraint::WatchedAnd(a) => {
1356            read_constraint_list(i, r_constr, a)?;
1357            Ok(())
1358        }
1359        Constraint::WatchedOr(a) => {
1360            read_constraint_list(i, r_constr, a)?;
1361            Ok(())
1362        }
1363        Constraint::GacAllDiff(a) => {
1364            read_list(i, r_constr, a)?;
1365            Ok(())
1366        }
1367        Constraint::AllDiff(a) => {
1368            read_list(i, r_constr, a)?;
1369            Ok(())
1370        }
1371        Constraint::AllDiffMatrix(a, b) => {
1372            read_list(i, r_constr, a)?;
1373            read_constant(r_constr, b)?;
1374            Ok(())
1375        }
1376        Constraint::WatchSumGeq(a, b) => {
1377            read_list(i, r_constr, a)?;
1378            read_constant(r_constr, b)?;
1379            Ok(())
1380        }
1381        Constraint::WatchSumLeq(a, b) => {
1382            read_list(i, r_constr, a)?;
1383            read_constant(r_constr, b)?;
1384            Ok(())
1385        }
1386        Constraint::OccurrenceGeq(a, b, c) => {
1387            read_list(i, r_constr, a)?;
1388            read_constant(r_constr, b)?;
1389            read_constant(r_constr, c)?;
1390            Ok(())
1391        }
1392        Constraint::OccurrenceLeq(a, b, c) => {
1393            read_list(i, r_constr, a)?;
1394            read_constant(r_constr, b)?;
1395            read_constant(r_constr, c)?;
1396            Ok(())
1397        }
1398        Constraint::Occurrence(a, b, c) => {
1399            read_list(i, r_constr, a)?;
1400            read_constant(r_constr, b)?;
1401            read_var(i, r_constr, c)?;
1402            Ok(())
1403        }
1404        Constraint::LexLess(a, b)
1405        | Constraint::LexLeq(a, b)
1406        | Constraint::LexLeqRv(a, b)
1407        | Constraint::LexLeqQuick(a, b)
1408        | Constraint::LexLessQuick(a, b)
1409        | Constraint::WatchVecNeq(a, b)
1410        | Constraint::WatchVecExistsLess(a, b) => {
1411            read_list(i, r_constr, a)?;
1412            read_list(i, r_constr, b)?;
1413            Ok(())
1414        }
1415        Constraint::LitSumGeq(a, b, c) => {
1416            read_list(i, r_constr, a)?;
1417            read_constant_list(r_constr, b)?;
1418            read_constant(r_constr, c)?;
1419            Ok(())
1420        }
1421        Constraint::Gcc(a, b, c) | Constraint::GccWeak(a, b, c) => {
1422            read_list(i, r_constr, a)?;
1423            read_constant_list(r_constr, b)?;
1424            read_list(i, r_constr, c)?;
1425            Ok(())
1426        }
1427        Constraint::Hamming(a, b, c) | Constraint::NotHamming(a, b, c) => {
1428            read_list(i, r_constr, a)?;
1429            read_list(i, r_constr, b)?;
1430            read_constant(r_constr, c)?;
1431            Ok(())
1432        }
1433        Constraint::FrameUpdate(a, b, c, d, e) => {
1434            read_list(i, r_constr, a)?;
1435            read_list(i, r_constr, b)?;
1436            read_list(i, r_constr, c)?;
1437            read_list(i, r_constr, d)?;
1438            read_constant(r_constr, e)?;
1439            Ok(())
1440        }
1441        Constraint::NegativeTable(vars, tuple_list)
1442        | Constraint::Table(vars, tuple_list)
1443        | Constraint::GacSchema(vars, tuple_list)
1444        | Constraint::LightTable(vars, tuple_list)
1445        | Constraint::Mddc(vars, tuple_list)
1446        | Constraint::NegativeMddc(vars, tuple_list) => {
1447            read_list(i, r_constr, vars)?;
1448            read_tuple_list(r_constr, tuple_list)?;
1449            Ok(())
1450        }
1451        Constraint::Str2Plus(vars, table_var) => {
1452            // CT_STR references a tuple table by NAME, not a variable.
1453            // Instead of looking it up in the symbol table as a var
1454            // (which fails), resolve it against the instance's
1455            // tuple-table symbol table via `constraint_setTuplesByName`
1456            // — that reuses the existing shared_ptr so refcounts stay
1457            // correct.
1458            read_list(i, r_constr, vars)?;
1459            let name = match table_var {
1460                Var::NameRef(n) => n.clone(),
1461                other => {
1462                    return Err(MinionError::NotImplemented(format!(
1463                        "Str2Plus second argument must be Var::NameRef(tuple_table_name), got {other:?}"
1464                    )));
1465                }
1466            };
1467            let c_name = CString::new(name.clone())
1468                .map_err(|_| anyhow!("Tuple-table name {name:?} contains a null character."))?;
1469            ffi::constraint_setTuplesByName(r_constr, i, c_name.as_ptr());
1470            Ok(())
1471        }
1472        Constraint::ShortStr2(vars, short_tuples)
1473        | Constraint::HaggisGac(vars, short_tuples)
1474        | Constraint::HaggisGacStable(vars, short_tuples)
1475        | Constraint::ShortCTupleStr2(vars, short_tuples) => {
1476            read_list(i, r_constr, vars)?;
1477            read_short_tuple_list(r_constr, short_tuples)?;
1478            Ok(())
1479        }
1480        Constraint::Max(a, b)
1481        | Constraint::Min(a, b)
1482        | Constraint::NvalueGeq(a, b)
1483        | Constraint::NvalueLeq(a, b) => {
1484            read_list(i, r_constr, a)?;
1485            read_var(i, r_constr, b)?;
1486            Ok(())
1487        }
1488        Constraint::Element(vec, j, e)
1489        | Constraint::ElementOne(vec, j, e)
1490        | Constraint::ElementUndefZero(vec, j, e)
1491        | Constraint::WatchElement(vec, j, e)
1492        | Constraint::WatchElementOne(vec, j, e)
1493        | Constraint::WatchElementOneUndefZero(vec, j, e)
1494        | Constraint::WatchElementUndefZero(vec, j, e) => {
1495            read_list(i, r_constr, vec)?;
1496            read_var(i, r_constr, j)?;
1497            read_var(i, r_constr, e)?;
1498            Ok(())
1499        }
1500        Constraint::WLiteral(a, b) | Constraint::WNotLiteral(a, b) => {
1501            read_var(i, r_constr, a)?;
1502            read_constant(r_constr, b)?;
1503            Ok(())
1504        }
1505        Constraint::WInIntervalSet(var, consts)
1506        | Constraint::WInRange(var, consts)
1507        | Constraint::WNotInRange(var, consts)
1508        | Constraint::WInset(var, consts)
1509        | Constraint::WNotInset(var, consts) => {
1510            read_var(i, r_constr, var)?;
1511            read_constant_list(r_constr, consts)?;
1512            Ok(())
1513        }
1514        Constraint::Abs(a, b)
1515        | Constraint::DisEq(a, b)
1516        | Constraint::MinusEq(a, b)
1517        | Constraint::GacEq(a, b)
1518        | Constraint::WatchLess(a, b)
1519        | Constraint::WatchNeq(a, b) => {
1520            read_var(i, r_constr, a)?;
1521            read_var(i, r_constr, b)?;
1522            Ok(())
1523        }
1524        Constraint::AllDiffMatrix(vars, c) => {
1525            read_list(i, r_constr, vars)?;
1526            read_constant(r_constr, c)?;
1527            Ok(())
1528        }
1529
1530        Constraint::True => Ok(()),
1531        Constraint::False => Ok(()),
1532        #[allow(unreachable_patterns)]
1533        x => Err(MinionError::NotImplemented(format!("{x:?}"))),
1534    }
1535}
1536
1537// DO NOT call manually - this assumes that all needed vars are already in the symbol table.
1538// TODO not happy with this just assuming the name is in the symbol table
1539/// Resolve an AST Var to a raw FFI Var.
1540unsafe fn resolve_var(
1541    instance: *mut ffi::ProbSpec_CSPInstance,
1542    var: &Var,
1543) -> Result<ffi::ProbSpec_Var, MinionError> {
1544    match var {
1545        Var::NameRef(name) => {
1546            let c_str = CString::new(name.clone()).map_err(|_| {
1547                anyhow!(
1548                    "Variable name {:?} contains a null character.",
1549                    name.clone()
1550                )
1551            })?;
1552            let var_result = ffi::minion_getVarByName(instance, c_str.as_ptr() as _);
1553            check_minion_result(var_result.result)?;
1554            Ok(var_result.var)
1555        }
1556        Var::ConstantAsVar(n) => Ok(ffi::constantAsVar(*n)),
1557    }
1558}
1559
1560unsafe fn read_list(
1561    instance: *mut ffi::ProbSpec_CSPInstance,
1562    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1563    vars: &Vec<Var>,
1564) -> Result<(), MinionError> {
1565    let raw_vars = Scoped::new(ffi::vec_var_new(), |x| ffi::vec_var_free(x as _));
1566    for var in vars {
1567        let raw_var = resolve_var(instance, var)?;
1568        ffi::vec_var_push_back(raw_vars.ptr, raw_var);
1569    }
1570
1571    ffi::constraint_addList(raw_constraint, raw_vars.ptr);
1572
1573    Ok(())
1574}
1575
1576unsafe fn read_var(
1577    instance: *mut ffi::ProbSpec_CSPInstance,
1578    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1579    var: &Var,
1580) -> Result<(), MinionError> {
1581    let raw_vars = Scoped::new(ffi::vec_var_new(), |x| ffi::vec_var_free(x as _));
1582    let raw_var = resolve_var(instance, var)?;
1583    ffi::vec_var_push_back(raw_vars.ptr, raw_var);
1584    ffi::constraint_addList(raw_constraint, raw_vars.ptr);
1585
1586    Ok(())
1587}
1588
1589unsafe fn read_2_vars(
1590    instance: *mut ffi::ProbSpec_CSPInstance,
1591    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1592    var1: &Var,
1593    var2: &Var,
1594) -> Result<(), MinionError> {
1595    let mut raw_var = resolve_var(instance, var1)?;
1596    let mut raw_var2 = resolve_var(instance, var2)?;
1597    // todo: does this move or copy? I am confus!
1598    // TODO need to mkae the semantics of move vs copy / ownership clear in libminion!!
1599    // This shouldve leaked everywhere by now but i think libminion copies stuff??
1600    ffi::constraint_addTwoVars(raw_constraint, &mut raw_var, &mut raw_var2);
1601    Ok(())
1602}
1603
1604unsafe fn read_constant(
1605    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1606    constant: &Constant,
1607) -> Result<(), MinionError> {
1608    let val: i32 = match constant {
1609        Constant::Integer(n) => Ok(*n),
1610        Constant::Bool(true) => Ok(1),
1611        Constant::Bool(false) => Ok(0),
1612        x => Err(MinionError::NotImplemented(format!("{x:?}"))),
1613    }?;
1614
1615    ffi::constraint_addConstant(raw_constraint, val);
1616
1617    Ok(())
1618}
1619
1620unsafe fn read_constant_list(
1621    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1622    constants: &[Constant],
1623) -> Result<(), MinionError> {
1624    let raw_consts = Scoped::new(ffi::vec_int_new(), |x| ffi::vec_int_free(x as _));
1625
1626    for constant in constants.iter() {
1627        let val = match constant {
1628            Constant::Integer(n) => Ok(*n),
1629            Constant::Bool(true) => Ok(1),
1630            Constant::Bool(false) => Ok(0),
1631            #[allow(unreachable_patterns)] // TODO: can there be other types?
1632            x => Err(MinionError::NotImplemented(format!("{x:?}"))),
1633        }?;
1634
1635        ffi::vec_int_push_back(raw_consts.ptr, val);
1636    }
1637
1638    ffi::constraint_addConstantList(raw_constraint, raw_consts.ptr);
1639    Ok(())
1640}
1641
1642//TODO: check if the inner constraint is listed in the model or not?
1643//Does this matter?
1644// TODO: type-check inner constraints vars and tuples and so on?
1645unsafe fn read_constraint(
1646    instance: *mut ffi::ProbSpec_CSPInstance,
1647    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1648    inner_constraint: Constraint,
1649) -> Result<(), MinionError> {
1650    let constraint_type = get_constraint_type(&inner_constraint)?;
1651    let raw_inner_constraint = Scoped::new(ffi::constraint_new(constraint_type), |x| {
1652        ffi::constraint_free(x as _)
1653    });
1654
1655    constraint_add_args(instance, raw_inner_constraint.ptr, &inner_constraint)?;
1656
1657    ffi::constraint_addConstraint(raw_constraint, raw_inner_constraint.ptr);
1658    Ok(())
1659}
1660
1661unsafe fn read_constraint_list(
1662    instance: *mut ffi::ProbSpec_CSPInstance,
1663    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1664    inner_constraints: &[Constraint],
1665) -> Result<(), MinionError> {
1666    let raw_inners = Scoped::new(ffi::vec_constraints_new(), |x| {
1667        ffi::vec_constraints_free(x as _)
1668    });
1669    for inner_constraint in inner_constraints.iter() {
1670        let constraint_type = get_constraint_type(inner_constraint)?;
1671        let raw_inner_constraint = Scoped::new(ffi::constraint_new(constraint_type), |x| {
1672            ffi::constraint_free(x as _)
1673        });
1674
1675        constraint_add_args(instance, raw_inner_constraint.ptr, inner_constraint)?;
1676        ffi::vec_constraints_push_back(raw_inners.ptr, raw_inner_constraint.ptr);
1677    }
1678
1679    ffi::constraint_addConstraintList(raw_constraint, raw_inners.ptr);
1680    Ok(())
1681}
1682
1683unsafe fn read_tuple_list(
1684    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1685    tuples: &Vec<Tuple>,
1686) -> Result<(), MinionError> {
1687    // a tuple list is just a vec<vec<int>>, where each inner vec is a tuple
1688    let raw_tuples = Scoped::new(ffi::vec_vec_int_new(), |x| ffi::vec_vec_int_free(x as _));
1689    for tuple in tuples {
1690        let raw_tuple = Scoped::new(ffi::vec_int_new(), |x| ffi::vec_int_free(x as _));
1691        for constant in tuple.iter() {
1692            let val = match constant {
1693                Constant::Integer(n) => Ok(*n),
1694                Constant::Bool(true) => Ok(1),
1695                Constant::Bool(false) => Ok(0),
1696                #[allow(unreachable_patterns)] // TODO: can there be other types?
1697                x => Err(MinionError::NotImplemented(format!("{:?}", x))),
1698            }?;
1699
1700            ffi::vec_int_push_back(raw_tuple.ptr, val);
1701        }
1702
1703        ffi::vec_vec_int_push_back_ptr(raw_tuples.ptr, raw_tuple.ptr);
1704    }
1705
1706    // `constraint_setTuples` transfers ownership of `TupleList` into Minion via shared_ptr.
1707    // Do not wrap this pointer in `Scoped` or it will be freed too early.
1708    let raw_tuple_list = ffi::tupleList_new(raw_tuples.ptr);
1709    ffi::constraint_setTuples(raw_constraint, raw_tuple_list);
1710
1711    Ok(())
1712}
1713
1714/// Build a `ShortTupleList` from a slice of [`ShortTuple`] and
1715/// attach it to `raw_constraint`. The C FFI takes a flat encoding
1716/// — each short tuple is one `vec<int>` carrying alternating
1717/// `[idx_0, val_0, idx_1, val_1, ...]` ints — so we materialise
1718/// that here.
1719unsafe fn read_short_tuple_list(
1720    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1721    short_tuples: &Vec<ShortTuple>,
1722) -> Result<(), MinionError> {
1723    let raw_outer = Scoped::new(ffi::vec_vec_int_new(), |x| ffi::vec_vec_int_free(x as _));
1724    for short in short_tuples {
1725        let raw_inner = Scoped::new(ffi::vec_int_new(), |x| ffi::vec_int_free(x as _));
1726        for &(idx, ref constant) in short {
1727            let val = match constant {
1728                Constant::Integer(n) => Ok(*n),
1729                Constant::Bool(true) => Ok(1),
1730                Constant::Bool(false) => Ok(0),
1731                #[allow(unreachable_patterns)]
1732                x => Err(MinionError::NotImplemented(format!("{:?}", x))),
1733            }?;
1734            ffi::vec_int_push_back(raw_inner.ptr, idx as i32);
1735            ffi::vec_int_push_back(raw_inner.ptr, val);
1736        }
1737        ffi::vec_vec_int_push_back_ptr(raw_outer.ptr, raw_inner.ptr);
1738    }
1739
1740    // `constraint_setShortTuples` takes ownership via shared_ptr;
1741    // do not wrap the result in `Scoped`.
1742    let raw_short_list = ffi::shortTupleList_new(raw_outer.ptr);
1743    ffi::constraint_setShortTuples(raw_constraint, raw_short_list);
1744
1745    Ok(())
1746}