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 limits return `RuntimeError::InvalidArgument` on Emscripten; the host
449/// must terminate the Web Worker to enforce a deadline.
450///
451/// Time limit specification: a number of seconds and whether to measure
452/// CPU time (`-cpulimit`) or wall-clock time (`-timelimit`).
453///
454/// Both are armed process-wide, not per-solve: minion uses `alarm()` or
455/// `setrlimit(RLIMIT_CPU)` together with a process-static trigger pointer
456/// (`system/trigger_timer.cpp`), so concurrent solves in one process
457/// overwrite each other's timers. `is_cpu_time` additionally sets a hard
458/// RLIMIT_CPU of `seconds + 5` on the calling process, which will kill it
459/// outright — one solve per process is the only safe use.
460#[derive(Clone, Copy, Debug, PartialEq, Eq)]
461pub struct TimeLimit {
462    pub seconds: u32,
463    pub is_cpu_time: bool,
464}
465
466impl Default for RunOptions {
467    fn default() -> Self {
468        Self {
469            seed: None,
470            var_order: VarOrder::default(),
471            val_order: ValOrder::default(),
472            randomise_order: false,
473            preprocess: Propagation {
474                level: PropLevel::None,
475                limit: false,
476            },
477            prop_node: Propagation {
478                level: PropLevel::Gac,
479                limit: false,
480            },
481            node_limit: 0,
482            time_limit: None,
483        }
484    }
485}
486
487impl VarOrder {
488    fn to_ffi(self) -> ffi::VarOrderEnum {
489        match self {
490            VarOrder::Static => ffi::VarOrderEnum_ORDER_STATIC,
491            VarOrder::Sdf => ffi::VarOrderEnum_ORDER_SDF,
492            VarOrder::Srf => ffi::VarOrderEnum_ORDER_SRF,
493            VarOrder::Ldf => ffi::VarOrderEnum_ORDER_LDF,
494            VarOrder::Original => ffi::VarOrderEnum_ORDER_ORIGINAL,
495            VarOrder::Wdeg => ffi::VarOrderEnum_ORDER_WDEG,
496            VarOrder::DomOverWdeg => ffi::VarOrderEnum_ORDER_DOMOVERWDEG,
497            VarOrder::Conflict => ffi::VarOrderEnum_ORDER_CONFLICT,
498        }
499    }
500}
501
502impl ValOrder {
503    fn to_ffi(self) -> ffi::ValOrderEnum {
504        match self {
505            ValOrder::Ascend => ffi::ValOrderEnum_VALORDER_ASCEND,
506            ValOrder::Descend => ffi::ValOrderEnum_VALORDER_DESCEND,
507            ValOrder::Random => ffi::ValOrderEnum_VALORDER_RANDOM,
508        }
509    }
510}
511
512/// Run Minion on the given [Model].
513///
514/// The given [callback](Callback) is ran whenever a new solution set is found.
515///
516/// Returns a [`SolverContext`] on success, which can be used to query run
517/// statistics via [`SolverContext::get_from_table`].
518///
519/// For callbacks that need to add variables or constraints during search,
520/// use [`run_minion_midsearch`].
521pub fn run_minion(model: Model, callback: Callback<'_>) -> Result<SolverContext, MinionError> {
522    run_minion_with_options(model, RunOptions::default(), callback)
523}
524
525/// Like [`run_minion`] but lets the caller pin solver-side knobs
526/// (e.g. the random seed) via [`RunOptions`].
527pub fn run_minion_with_options(
528    model: Model,
529    options: RunOptions,
530    mut callback: Callback<'_>,
531) -> Result<SolverContext, MinionError> {
532    run_minion_midsearch_with_options(model, options, Box::new(move |_ctx, sol| callback(sol)))
533}
534
535/// Run Minion on the given [Model] with a callback that can mutate the
536/// running search via a [`MidSearchContext`] handle.
537pub fn run_minion_midsearch(
538    model: Model,
539    callback: MidSearchCallback<'_>,
540) -> Result<SolverContext, MinionError> {
541    run_minion_midsearch_with_options(model, RunOptions::default(), callback)
542}
543
544/// Callback type for parallel portfolio search.
545///
546/// The closure must be `Send + Sync` because it may be invoked from any of
547/// the worker threads. The C-side controller serialises invocations with a
548/// mutex (so two workers never call this concurrently) but Rust still needs
549/// the bound for soundness when we cross the FFI boundary from multiple
550/// threads.
551pub type ParallelCallback<'a> = Box<dyn FnMut(HashMap<VarName, Constant>) -> bool + Send + 'a>;
552
553/// State stored behind the parallel callback's userdata pointer. The mutex
554/// gives Rust ownership semantics on the FnMut even though the C-side
555/// already serialises calls — we need the &mut for the closure invocation.
556struct ParallelCallbackState<'a> {
557    callback: std::sync::Mutex<ParallelCallback<'a>>,
558    // Variables that are in the print matrix of every worker context. The
559    // print-matrix order is identical across workers because each worker
560    // builds its CSPInstance from the same shared description.
561    print_vars: Vec<VarName>,
562}
563
564unsafe extern "C" fn parallel_callback_thunk(
565    ctx: *mut ffi::MinionContext,
566    userdata: *mut c_void,
567) -> bool {
568    let state = unsafe { &*(userdata as *mut ParallelCallbackState<'_>) };
569
570    let mut solutions: HashMap<VarName, Constant> = HashMap::new();
571    for (i, var) in state.print_vars.iter().enumerate() {
572        let v: i32 = unsafe { ffi::printMatrix_getValue(ctx, i as _) };
573        solutions.insert(var.clone(), Constant::Integer(v));
574    }
575
576    // The C-side controller has already locked its callback mutex; this
577    // Rust mutex is therefore uncontended in practice. We still take it
578    // because Rust's borrow checker doesn't see the C-side serialisation.
579    #[allow(clippy::unwrap_used)]
580    let mut cb = state.callback.lock().unwrap();
581    cb(solutions)
582}
583
584/// Run Minion as a portfolio search across `num_threads` worker threads.
585///
586/// Unavailable on Emscripten (including one worker): returns
587/// `RuntimeError::InvalidArgument`.
588///
589/// Each thread builds its own solver from a shared `Model`, with a derived
590/// random seed; the first thread to find `sollimit` solutions (or to prove
591/// unsat) signals the others to stop. The callback is invoked at most once
592/// per solution found by any worker, serialised internally so it never
593/// re-enters itself.
594///
595/// `num_threads` must be >= 1. With `num_threads == 1` behaviour is
596/// equivalent to a sequential `run_minion`.
597///
598/// Mid-search mutation is NOT supported in this mode (each worker has its
599/// own context; mutating one wouldn't propagate). Use the sequential
600/// [`run_minion_midsearch`] path if you need that.
601///
602/// # Concurrency
603///
604/// `run_minion_parallel` itself spawns OS threads internally. It is safe
605/// to call from a single thread of the host process per call, but it is
606/// **not** safe to invoke from multiple host threads concurrently — the
607/// underlying Minion alarm/ctrl-C handlers are per-process and racing
608/// threaded runs can corrupt them. (The fork-based `-parallel` flag has
609/// the same constraint at the process level.) For sequential use across
610/// many models, just call this function repeatedly.
611pub fn run_minion_parallel(
612    num_threads: usize,
613    model: Model,
614    callback: ParallelCallback<'_>,
615) -> Result<(), MinionError> {
616    run_minion_parallel_with_options(num_threads, model, RunOptions::default(), callback)
617}
618
619/// Unavailable on Emscripten (including one worker): returns
620/// `RuntimeError::InvalidArgument`.
621///
622/// Run Minion with thread-based work-stealing: N workers cooperatively
623/// split the search tree. Worker 0 starts at the root; idle workers wait
624/// on a shared queue. Busy workers, on each search node, donate one
625/// stealable left-branch (encoded as a path-from-root) to the queue when
626/// other workers are idle. Idle workers fast-forward via worldPush +
627/// propagate replay and continue search from there.
628///
629/// Unlike [`run_minion_parallel`] (pure portfolio), this *divides* the
630/// search tree across workers and so speeds up UNSAT proving roughly with
631/// 1/N. Wdeg counters drift between workers — intended as a diversity
632/// source.
633///
634/// Mid-search mutation is not supported. See [`run_minion_parallel`] for
635/// the concurrency caveat (don't call this from multiple host threads
636/// concurrently — the underlying alarm/ctrl-C handler state is
637/// process-global).
638pub fn run_minion_work_steal(
639    num_threads: usize,
640    model: Model,
641    callback: ParallelCallback<'_>,
642) -> Result<WorkStealStats, MinionError> {
643    run_minion_work_steal_with_options(num_threads, model, RunOptions::default(), callback)
644}
645
646/// Aggregate work-stealing diagnostics, populated by
647/// [`run_minion_work_steal`] and [`run_minion_work_steal_with_options`].
648///
649/// `donations` counts donate() calls that fired (a busy worker handed off
650/// a sub-tree). `items_taken` counts work items popped and replayed by
651/// idle workers. `replay_failures` counts replays that found
652/// infeasibility before beginning sub-tree search. `total_nodes` is the
653/// sum of per-worker node counts.
654///
655/// `donations == 0` after a run means the donation/replay path was never
656/// exercised — typically because the search finished before idle workers
657/// reached their wait. Useful for confirming a test instance is large
658/// enough to stress the work-stealing protocol.
659///
660/// Contention diagnostics (cumulative across all workers, nanoseconds):
661/// `queue_lock_wait_nanos` is total time spent acquiring the donation
662/// queue mutex. `idle_wait_nanos` is total time blocked inside
663/// `popOrFinish` waiting for a donation — divide by `num_threads *
664/// wall_time` for "fraction of available CPU spent idle".
665/// `callback_lock_wait_nanos` is total time spent acquiring the
666/// per-solution lock; rises with enumeration-heavy SAT runs.
667#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
668pub struct WorkStealStats {
669    pub donations: i64,
670    pub items_taken: i64,
671    pub replay_failures: i64,
672    pub total_nodes: i64,
673    pub queue_lock_wait_nanos: i64,
674    pub idle_wait_nanos: i64,
675    pub callback_lock_wait_nanos: i64,
676}
677
678/// Like [`run_minion_work_steal`] but with [`RunOptions`].
679#[allow(clippy::unwrap_used)]
680pub fn run_minion_work_steal_with_options(
681    num_threads: usize,
682    model: Model,
683    options: RunOptions,
684    callback: ParallelCallback<'_>,
685) -> Result<WorkStealStats, MinionError> {
686    if num_threads == 0 {
687        return Err(MinionError::Other(anyhow!(
688            "num_threads must be at least 1"
689        )));
690    }
691    if num_threads > i32::MAX as usize {
692        return Err(MinionError::Other(anyhow!("num_threads is too large")));
693    }
694
695    unsafe {
696        let search_opts = ffi::searchOptions_new();
697        let search_method = ffi::searchMethod_new();
698        let search_instance = ffi::instance_new();
699
700        (*search_opts).randomiseValvarorder = options.randomise_order;
701        (*search_opts).silent = true;
702        (*search_opts).print_solution = false;
703        (*search_opts).sollimit = -1;
704
705        if let Some(seed) = options.seed {
706            // Identity at the default width; a widening conversion under the
707            // domains64 feature, where UnsignedSysInt is 64 bits.
708            #[allow(clippy::useless_conversion)]
709            {
710                (*search_method).randomSeed = seed.into();
711            }
712        }
713        (*search_method).preprocess = options.preprocess.to_ffi();
714        (*search_method).propMethod = options.prop_node.to_ffi();
715        if options.node_limit > 0 {
716            (*search_opts).nodelimit = options.node_limit as _;
717        }
718        if let Some(tl) = options.time_limit {
719            (*search_opts).timeoutActive = true;
720            (*search_opts).time_limit = tl.seconds as _;
721            (*search_opts).time_limit_is_CPUTime = tl.is_cpu_time;
722        }
723
724        let mut print_vars: Vec<VarName> = vec![];
725        let convert_result =
726            convert_model_to_raw(search_instance, &model, &options, &mut print_vars);
727        if let Err(e) = convert_result {
728            ffi::searchMethod_free(search_method);
729            ffi::searchOptions_free(search_opts);
730            ffi::instance_free(search_instance);
731            return Err(e);
732        }
733
734        let state = ParallelCallbackState {
735            callback: std::sync::Mutex::new(callback),
736            print_vars,
737        };
738        let userdata = &state as *const ParallelCallbackState<'_> as *mut c_void;
739
740        let cfg = ffi::MinionThreadConfig {
741            numThreads: num_threads as i32,
742            baseSeed: options.seed.unwrap_or(0),
743        };
744
745        let mut raw_stats = ffi::MinionWorkStealStats {
746            donations: 0,
747            itemsTaken: 0,
748            replayFailures: 0,
749            totalNodes: 0,
750            queueLockWaitNanos: 0,
751            idleWaitNanos: 0,
752            callbackLockWaitNanos: 0,
753        };
754
755        let res = ffi::runMinionWorkSteal(
756            cfg,
757            search_opts,
758            search_method,
759            search_instance,
760            Some(parallel_callback_thunk),
761            userdata,
762            &mut raw_stats,
763        );
764
765        ffi::searchMethod_free(search_method);
766        ffi::searchOptions_free(search_opts);
767        ffi::instance_free(search_instance);
768
769        check_minion_result(res)?;
770        Ok(WorkStealStats {
771            donations: raw_stats.donations,
772            items_taken: raw_stats.itemsTaken,
773            replay_failures: raw_stats.replayFailures,
774            total_nodes: raw_stats.totalNodes,
775            queue_lock_wait_nanos: raw_stats.queueLockWaitNanos,
776            idle_wait_nanos: raw_stats.idleWaitNanos,
777            callback_lock_wait_nanos: raw_stats.callbackLockWaitNanos,
778        })
779    }
780}
781
782/// Like [`run_minion_parallel`] but with [`RunOptions`] (random seed, var/
783/// val orderings, propagation levels). The seed (if set) is the controller
784/// base seed; per-thread seeds are derived as `base XOR thread_index`.
785#[allow(clippy::unwrap_used)]
786pub fn run_minion_parallel_with_options(
787    num_threads: usize,
788    model: Model,
789    options: RunOptions,
790    callback: ParallelCallback<'_>,
791) -> Result<(), MinionError> {
792    if num_threads == 0 {
793        return Err(MinionError::Other(anyhow!(
794            "num_threads must be at least 1"
795        )));
796    }
797    if num_threads > i32::MAX as usize {
798        return Err(MinionError::Other(anyhow!("num_threads is too large")));
799    }
800
801    unsafe {
802        let search_opts = ffi::searchOptions_new();
803        let search_method = ffi::searchMethod_new();
804        let search_instance = ffi::instance_new();
805
806        // Quiet by default: workers don't print to cout under runMinionParallel
807        // (they observe getOptions().silent = true via the per-thread copy).
808        // The user receives results via the callback.
809        (*search_opts).randomiseValvarorder = options.randomise_order;
810        (*search_opts).silent = true;
811        (*search_opts).print_solution = false;
812        // Library convention: enumerate all solutions and let the caller's
813        // callback decide when to stop by returning false. The C-side
814        // controller enforces this `sollimit = -1` shared value.
815        (*search_opts).sollimit = -1;
816
817        if let Some(seed) = options.seed {
818            // Identity at the default width; a widening conversion under the
819            // domains64 feature, where UnsignedSysInt is 64 bits.
820            #[allow(clippy::useless_conversion)]
821            {
822                (*search_method).randomSeed = seed.into();
823            }
824        }
825        (*search_method).preprocess = options.preprocess.to_ffi();
826        (*search_method).propMethod = options.prop_node.to_ffi();
827        if options.node_limit > 0 {
828            (*search_opts).nodelimit = options.node_limit as _;
829        }
830        if let Some(tl) = options.time_limit {
831            (*search_opts).timeoutActive = true;
832            (*search_opts).time_limit = tl.seconds as _;
833            (*search_opts).time_limit_is_CPUTime = tl.is_cpu_time;
834        }
835
836        let mut print_vars: Vec<VarName> = vec![];
837        let convert_result =
838            convert_model_to_raw(search_instance, &model, &options, &mut print_vars);
839        if let Err(e) = convert_result {
840            ffi::searchMethod_free(search_method);
841            ffi::searchOptions_free(search_opts);
842            ffi::instance_free(search_instance);
843            return Err(e);
844        }
845
846        let state = ParallelCallbackState {
847            callback: std::sync::Mutex::new(callback),
848            print_vars,
849        };
850        let userdata = &state as *const ParallelCallbackState<'_> as *mut c_void;
851
852        let cfg = ffi::MinionThreadConfig {
853            numThreads: num_threads as i32,
854            baseSeed: options.seed.unwrap_or(0),
855        };
856
857        let res = ffi::runMinionParallel(
858            cfg,
859            search_opts,
860            search_method,
861            search_instance,
862            Some(parallel_callback_thunk),
863            userdata,
864        );
865
866        ffi::searchMethod_free(search_method);
867        ffi::searchOptions_free(search_opts);
868        ffi::instance_free(search_instance);
869
870        check_minion_result(res)?;
871        Ok(())
872    }
873}
874
875/// Like [`run_minion_midsearch`] but with [`RunOptions`].
876#[allow(clippy::unwrap_used)]
877pub fn run_minion_midsearch_with_options(
878    model: Model,
879    options: RunOptions,
880    callback: MidSearchCallback<'_>,
881) -> Result<SolverContext, MinionError> {
882    unsafe {
883        let ctx = ffi::minion_newContext();
884        let search_opts = ffi::searchOptions_new();
885        let search_method = ffi::searchMethod_new();
886        let search_instance = ffi::instance_new();
887
888        // Use Minion as a quiet library by default. Low-level FFI callers that
889        // want native solver output can opt out by configuring SearchOptions
890        // themselves instead of going through this wrapper.
891        (*search_opts).randomiseValvarorder = options.randomise_order;
892        (*search_opts).silent = true;
893        (*search_opts).print_solution = false;
894
895        if let Some(seed) = options.seed {
896            // Identity at the default width; a widening conversion under the
897            // domains64 feature, where UnsignedSysInt is 64 bits.
898            #[allow(clippy::useless_conversion)]
899            {
900                (*search_method).randomSeed = seed.into();
901            }
902        }
903        (*search_method).preprocess = options.preprocess.to_ffi();
904        (*search_method).propMethod = options.prop_node.to_ffi();
905        if options.node_limit > 0 {
906            (*search_opts).nodelimit = options.node_limit as _;
907        }
908        if let Some(tl) = options.time_limit {
909            (*search_opts).timeoutActive = true;
910            (*search_opts).time_limit = tl.seconds as _;
911            (*search_opts).time_limit_is_CPUTime = tl.is_cpu_time;
912        }
913
914        let mut state = CallbackState {
915            callback,
916            instance: search_instance,
917            print_vars: vec![],
918            midsearch_vars: vec![],
919        };
920
921        convert_model_to_raw(search_instance, &model, &options, &mut state.print_vars)?;
922
923        let userdata = &mut state as *mut CallbackState<'_> as *mut c_void;
924        let res = ffi::runMinion(
925            ctx,
926            search_opts,
927            search_method,
928            search_instance,
929            Some(run_callback),
930            userdata,
931        );
932
933        ffi::searchMethod_free(search_method);
934        ffi::searchOptions_free(search_opts);
935        ffi::instance_free(search_instance);
936
937        match check_minion_result(res) {
938            Ok(()) => Ok(SolverContext { ctx }),
939            Err(e) => {
940                ffi::minion_freeContext(ctx);
941                Err(MinionError::from(e))
942            }
943        }
944    }
945}
946
947unsafe fn convert_model_to_raw(
948    instance: *mut ffi::ProbSpec_CSPInstance,
949    model: &Model,
950    options: &RunOptions,
951    print_vars: &mut Vec<VarName>,
952) -> Result<(), MinionError> {
953    /*******************************/
954    /*        Add variables        */
955    /*******************************/
956
957    /*
958     * Add variables to:
959     * 1. symbol table
960     * 2. print matrix
961     * 3. search vars
962     *
963     * These are all done in the order saved in the SymbolTable.
964     */
965
966    let search_vars = Scoped::new(ffi::vec_var_new(), |x| ffi::vec_var_free(x as _));
967
968    // initialise all variables, and add all variables to the print order
969    for var_name in model.named_variables.get_variable_order() {
970        let c_str = CString::new(var_name.clone()).map_err(|_| {
971            anyhow!(
972                "Variable name {:?} contains a null character.",
973                var_name.clone()
974            )
975        })?;
976
977        let vartype = model
978            .named_variables
979            .get_vartype(var_name.clone())
980            .ok_or(anyhow!("Could not get var type for {:?}", var_name.clone()))?;
981
982        match vartype {
983            VarDomain::Bound(a, b) => {
984                check_minion_result(ffi::minion_newVar(
985                    instance,
986                    c_str.as_ptr() as *mut c_char,
987                    ffi::VariableType_VAR_BOUND,
988                    a,
989                    b,
990                ))?;
991            }
992            VarDomain::Discrete(a, b) => {
993                check_minion_result(ffi::minion_newVar(
994                    instance,
995                    c_str.as_ptr() as *mut c_char,
996                    ffi::VariableType_VAR_DISCRETE,
997                    a,
998                    b,
999                ))?;
1000            }
1001            VarDomain::Bool => {
1002                check_minion_result(ffi::minion_newVar(
1003                    instance,
1004                    c_str.as_ptr() as *mut c_char,
1005                    ffi::VariableType_VAR_BOOL,
1006                    0,
1007                    1,
1008                ))?;
1009            }
1010            VarDomain::SparseBound(ref vals) => {
1011                let raw = Scoped::new(ffi::vec_int_new(), |x| ffi::vec_int_free(x as _));
1012                for v in vals {
1013                    ffi::vec_int_push_back(raw.ptr, *v);
1014                }
1015                check_minion_result(ffi::minion_newSparseBoundVar(
1016                    instance,
1017                    c_str.as_ptr() as *mut c_char,
1018                    raw.ptr,
1019                ))?;
1020            }
1021            x => return Err(MinionError::NotImplemented(format!("{x:?}"))),
1022        };
1023
1024        let var_result = ffi::minion_getVarByName(instance, c_str.as_ptr() as _);
1025        check_minion_result(var_result.result)?;
1026        let var = var_result.var;
1027
1028        ffi::printMatrix_addVar(instance, var);
1029
1030        // Remember the order for the callback function.
1031        print_vars.push(var_name.clone());
1032    }
1033
1034    // only add search variables to search order
1035    let primary_search_var_names: std::collections::HashSet<VarName> = model
1036        .named_variables
1037        .get_search_variable_order()
1038        .into_iter()
1039        .collect();
1040    for search_var_name in model.named_variables.get_search_variable_order() {
1041        let c_str = CString::new(search_var_name.clone()).map_err(|_| {
1042            anyhow!(
1043                "Variable name {:?} contains a null character.",
1044                search_var_name.clone()
1045            )
1046        })?;
1047        let var_result = ffi::minion_getVarByName(instance, c_str.as_ptr() as _);
1048        check_minion_result(var_result.result)?;
1049        ffi::vec_var_push_back(search_vars.ptr, var_result.var);
1050    }
1051
1052    let search_order = Scoped::new(
1053        ffi::searchOrder_new(search_vars.ptr, options.var_order.to_ffi(), false),
1054        |x| ffi::searchOrder_free(x as _),
1055    );
1056    // Minion's text parser defaults every variable's value-ordering to
1057    // ASCEND via `SearchOrder::setupValueOrder` during BuildCSP. To set
1058    // DESCEND or RANDOM from the library path we have to fill the
1059    // per-variable valOrder vector ourselves before BuildCSP runs.
1060    ffi::searchOrder_setValOrder(search_order.ptr, options.val_order.to_ffi());
1061
1062    ffi::instance_addSearchOrder(instance, search_order.ptr);
1063
1064    // Minion's text parser appends every variable omitted from VARORDER as a
1065    // find-one-assignment auxiliary search block. Models built through this
1066    // library bypass that finalisation step, so reproduce it here. Without
1067    // this block Minion reports a solution while auxiliary variables are
1068    // still unassigned -- in practice it reports no solutions at all -- and
1069    // the callback observes their lower bounds even where those violate
1070    // constraints.
1071    let auxiliary_search_vars = Scoped::new(ffi::vec_var_new(), |x| ffi::vec_var_free(x as _));
1072    let mut has_auxiliary_search_vars = false;
1073    for auxiliary_name in model
1074        .named_variables
1075        .get_variable_order()
1076        .into_iter()
1077        .filter(|name| !primary_search_var_names.contains(name))
1078    {
1079        let c_str = CString::new(auxiliary_name.clone())
1080            .map_err(|_| anyhow!("Variable name {auxiliary_name:?} contains a null character"))?;
1081        let var_result = ffi::minion_getVarByName(instance, c_str.as_ptr() as _);
1082        check_minion_result(var_result.result)?;
1083        ffi::vec_var_push_back(auxiliary_search_vars.ptr, var_result.var);
1084        has_auxiliary_search_vars = true;
1085    }
1086    if has_auxiliary_search_vars {
1087        let auxiliary_search_order = Scoped::new(
1088            ffi::searchOrder_new(
1089                auxiliary_search_vars.ptr,
1090                ffi::VarOrderEnum_ORDER_STATIC,
1091                true,
1092            ),
1093            |x| ffi::searchOrder_free(x as _),
1094        );
1095        ffi::instance_addSearchOrder(instance, auxiliary_search_order.ptr);
1096    }
1097
1098    /*********************************/
1099    /*        Add tuple tables       */
1100    /*********************************/
1101    //
1102    // Must happen BEFORE add-constraint: constraints like
1103    // `Str2Plus(vars, Var::NameRef(table_name))` look the table up
1104    // by name on the instance.
1105
1106    for (name, tuples) in &model.tuple_tables {
1107        let c_name = CString::new(name.clone()).map_err(|_| {
1108            anyhow!(
1109                "Tuple-table name {:?} contains a null character.",
1110                name.clone()
1111            )
1112        })?;
1113
1114        // Build the raw Vec<Vec<DomainInt>> to hand to tupleList_new.
1115        // tupleList_new takes ownership semantically (it constructs a
1116        // TupleList from the contents); the vec_vec_int_new/free
1117        // pair only cleans up the intermediate carrier.
1118        let raw_tuples = Scoped::new(ffi::vec_vec_int_new(), |x| ffi::vec_vec_int_free(x as _));
1119        for tuple in tuples {
1120            let raw_tuple = Scoped::new(ffi::vec_int_new(), |x| ffi::vec_int_free(x as _));
1121            for constant in tuple {
1122                let val = match constant {
1123                    Constant::Integer(n) => *n,
1124                    Constant::Bool(true) => 1,
1125                    Constant::Bool(false) => 0,
1126                    #[allow(unreachable_patterns)]
1127                    x => return Err(MinionError::NotImplemented(format!("{x:?}"))),
1128                };
1129                ffi::vec_int_push_back(raw_tuple.ptr, val);
1130            }
1131            ffi::vec_vec_int_push_back_ptr(raw_tuples.ptr, raw_tuple.ptr);
1132        }
1133
1134        // `instance_addTupleTableSymbol` copies the name and takes
1135        // ownership of the TupleList via `shared_ptr<TupleList>`,
1136        // so we do NOT wrap the result of `tupleList_new` in Scoped.
1137        let raw_tuple_list = ffi::tupleList_new(raw_tuples.ptr);
1138        ffi::instance_addTupleTableSymbol(instance, c_name.as_ptr() as *mut c_char, raw_tuple_list);
1139    }
1140
1141    /*********************************/
1142    /*        Add constraints        */
1143    /*********************************/
1144
1145    for constraint in &model.constraints {
1146        // 1. get constraint type and create C++ constraint object
1147        // 2. run through arguments and add them to the constraint
1148        // 3. add constraint to instance
1149
1150        let constraint_type = get_constraint_type(constraint)?;
1151        let raw_constraint = Scoped::new(ffi::constraint_new(constraint_type), |x| {
1152            ffi::constraint_free(x as _)
1153        });
1154
1155        constraint_add_args(instance, raw_constraint.ptr, constraint)?;
1156        ffi::instance_addConstraint(instance, raw_constraint.ptr);
1157    }
1158
1159    /**********************************/
1160    /*       Optimisation directive   */
1161    /**********************************/
1162    if let Some(opt) = &model.optimise {
1163        let mut raw_var = resolve_var(instance, &opt.var)?;
1164        ffi::instance_setOptimise(instance, opt.minimise, &mut raw_var);
1165    }
1166
1167    Ok(())
1168}
1169
1170unsafe fn get_constraint_type(constraint: &Constraint) -> Result<u32, MinionError> {
1171    match constraint {
1172        Constraint::SumGeq(_, _) => Ok(ffi::ConstraintType_CT_GEQSUM),
1173        Constraint::SumLeq(_, _) => Ok(ffi::ConstraintType_CT_LEQSUM),
1174        Constraint::Ineq(_, _, _) => Ok(ffi::ConstraintType_CT_INEQ),
1175        Constraint::Eq(_, _) => Ok(ffi::ConstraintType_CT_EQ),
1176        Constraint::Difference(_, _) => Ok(ffi::ConstraintType_CT_DIFFERENCE),
1177        Constraint::Div(_, _) => Ok(ffi::ConstraintType_CT_DIV),
1178        Constraint::DivUndefZero(_, _) => Ok(ffi::ConstraintType_CT_DIV_UNDEFZERO),
1179        Constraint::Modulo(_, _) => Ok(ffi::ConstraintType_CT_MODULO),
1180        Constraint::ModuloUndefZero(_, _) => Ok(ffi::ConstraintType_CT_MODULO_UNDEFZERO),
1181        Constraint::Pow(_, _) => Ok(ffi::ConstraintType_CT_POW),
1182        Constraint::Product(_, _) => Ok(ffi::ConstraintType_CT_PRODUCT2),
1183        Constraint::WeightedSumGeq(_, _, _) => Ok(ffi::ConstraintType_CT_WEIGHTGEQSUM),
1184        Constraint::WeightedSumLeq(_, _, _) => Ok(ffi::ConstraintType_CT_WEIGHTLEQSUM),
1185        Constraint::CheckAssign(_) => Ok(ffi::ConstraintType_CT_CHECK_ASSIGN),
1186        Constraint::CheckGsa(_) => Ok(ffi::ConstraintType_CT_CHECK_GSA),
1187        Constraint::ForwardChecking(_) => Ok(ffi::ConstraintType_CT_FORWARD_CHECKING),
1188        Constraint::Reify(_, _) => Ok(ffi::ConstraintType_CT_REIFY),
1189        Constraint::ReifyImply(_, _) => Ok(ffi::ConstraintType_CT_REIFYIMPLY),
1190        Constraint::ReifyImplyQuick(_, _) => Ok(ffi::ConstraintType_CT_REIFYIMPLY_QUICK),
1191        Constraint::WatchedAnd(_) => Ok(ffi::ConstraintType_CT_WATCHED_NEW_AND),
1192        Constraint::WatchedOr(_) => Ok(ffi::ConstraintType_CT_WATCHED_NEW_OR),
1193        Constraint::GacAllDiff(_) => Ok(ffi::ConstraintType_CT_GACALLDIFF),
1194        Constraint::AllDiff(_) => Ok(ffi::ConstraintType_CT_ALLDIFF),
1195        Constraint::AllDiffMatrix(_, _) => Ok(ffi::ConstraintType_CT_ALLDIFFMATRIX),
1196        Constraint::WatchSumGeq(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_GEQSUM),
1197        Constraint::WatchSumLeq(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_LEQSUM),
1198        Constraint::OccurrenceGeq(_, _, _) => Ok(ffi::ConstraintType_CT_GEQ_OCCURRENCE),
1199        Constraint::OccurrenceLeq(_, _, _) => Ok(ffi::ConstraintType_CT_LEQ_OCCURRENCE),
1200        Constraint::Occurrence(_, _, _) => Ok(ffi::ConstraintType_CT_OCCURRENCE),
1201        Constraint::LitSumGeq(_, _, _) => Ok(ffi::ConstraintType_CT_WATCHED_LITSUM),
1202        Constraint::Gcc(_, _, _) => Ok(ffi::ConstraintType_CT_GCC),
1203        Constraint::GccWeak(_, _, _) => Ok(ffi::ConstraintType_CT_GCCWEAK),
1204        Constraint::LexLeqRv(_, _) => Ok(ffi::ConstraintType_CT_GACLEXLEQ),
1205        Constraint::LexLeq(_, _) => Ok(ffi::ConstraintType_CT_LEXLEQ),
1206        Constraint::LexLess(_, _) => Ok(ffi::ConstraintType_CT_LEXLESS),
1207        Constraint::LexLeqQuick(_, _) => Ok(ffi::ConstraintType_CT_QUICK_LEXLEQ),
1208        Constraint::LexLessQuick(_, _) => Ok(ffi::ConstraintType_CT_QUICK_LEXLESS),
1209        Constraint::WatchVecNeq(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_VECNEQ),
1210        Constraint::WatchVecExistsLess(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_VEC_OR_LESS),
1211        Constraint::Hamming(_, _, _) => Ok(ffi::ConstraintType_CT_WATCHED_HAMMING),
1212        Constraint::NotHamming(_, _, _) => Ok(ffi::ConstraintType_CT_WATCHED_NOT_HAMMING),
1213        Constraint::FrameUpdate(_, _, _, _, _) => Ok(ffi::ConstraintType_CT_FRAMEUPDATE),
1214        Constraint::NegativeTable(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_NEGATIVE_TABLE),
1215        Constraint::Table(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_TABLE),
1216        Constraint::GacSchema(_, _) => Ok(ffi::ConstraintType_CT_GACSCHEMA),
1217        Constraint::LightTable(_, _) => Ok(ffi::ConstraintType_CT_LIGHTTABLE),
1218        Constraint::Mddc(_, _) => Ok(ffi::ConstraintType_CT_MDDC),
1219        Constraint::NegativeMddc(_, _) => Ok(ffi::ConstraintType_CT_NEGATIVEMDDC),
1220        Constraint::Str2Plus(_, _) => Ok(ffi::ConstraintType_CT_STR),
1221        Constraint::ShortStr2(_, _) => Ok(ffi::ConstraintType_CT_SHORTSTR),
1222        Constraint::HaggisGac(_, _) => Ok(ffi::ConstraintType_CT_HAGGISGAC),
1223        Constraint::HaggisGacStable(_, _) => Ok(ffi::ConstraintType_CT_HAGGISGAC_STABLE),
1224        Constraint::ShortCTupleStr2(_, _) => Ok(ffi::ConstraintType_CT_SHORTSTR_CTUPLE),
1225        Constraint::Max(_, _) => Ok(ffi::ConstraintType_CT_MAX),
1226        Constraint::Min(_, _) => Ok(ffi::ConstraintType_CT_MIN),
1227        Constraint::NvalueGeq(_, _) => Ok(ffi::ConstraintType_CT_GEQNVALUE),
1228        Constraint::NvalueLeq(_, _) => Ok(ffi::ConstraintType_CT_LEQNVALUE),
1229        Constraint::Element(_, _, _) => Ok(ffi::ConstraintType_CT_ELEMENT),
1230        Constraint::ElementOne(_, _, _) => Ok(ffi::ConstraintType_CT_ELEMENT_ONE),
1231        Constraint::ElementUndefZero(_, _, _) => Ok(ffi::ConstraintType_CT_ELEMENT_UNDEFZERO),
1232        Constraint::WatchElement(_, _, _) => Ok(ffi::ConstraintType_CT_WATCHED_ELEMENT),
1233        Constraint::WatchElementOne(_, _, _) => Ok(ffi::ConstraintType_CT_WATCHED_ELEMENT_ONE),
1234        Constraint::WatchElementOneUndefZero(_, _, _) => {
1235            Ok(ffi::ConstraintType_CT_WATCHED_ELEMENT_ONE_UNDEFZERO)
1236        }
1237        Constraint::WatchElementUndefZero(_, _, _) => {
1238            Ok(ffi::ConstraintType_CT_WATCHED_ELEMENT_UNDEFZERO)
1239        }
1240        Constraint::WLiteral(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_LIT),
1241        Constraint::WNotLiteral(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_NOTLIT),
1242        Constraint::WInIntervalSet(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_ININTERVALSET),
1243        Constraint::WInRange(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_INRANGE),
1244        Constraint::WInset(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_INSET),
1245        Constraint::WNotInRange(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_NOT_INRANGE),
1246        Constraint::WNotInset(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_NOT_INSET),
1247        Constraint::Abs(_, _) => Ok(ffi::ConstraintType_CT_ABS),
1248        Constraint::DisEq(_, _) => Ok(ffi::ConstraintType_CT_DISEQ),
1249        Constraint::MinusEq(_, _) => Ok(ffi::ConstraintType_CT_MINUSEQ),
1250        Constraint::GacEq(_, _) => Ok(ffi::ConstraintType_CT_GACEQ),
1251        Constraint::WatchLess(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_LESS),
1252        Constraint::WatchNeq(_, _) => Ok(ffi::ConstraintType_CT_WATCHED_NEQ),
1253        Constraint::True => Ok(ffi::ConstraintType_CT_TRUE),
1254        Constraint::False => Ok(ffi::ConstraintType_CT_FALSE),
1255
1256        #[allow(unreachable_patterns)]
1257        x => Err(MinionError::NotImplemented(format!(
1258            "Constraint not implemented {x:?}",
1259        ))),
1260    }
1261}
1262
1263unsafe fn constraint_add_args(
1264    i: *mut ffi::ProbSpec_CSPInstance,
1265    r_constr: *mut ffi::ProbSpec_ConstraintBlob,
1266    constr: &Constraint,
1267) -> Result<(), MinionError> {
1268    match constr {
1269        Constraint::SumGeq(lhs_vars, rhs_var) => {
1270            read_list(i, r_constr, lhs_vars)?;
1271            read_var(i, r_constr, rhs_var)?;
1272            Ok(())
1273        }
1274        Constraint::SumLeq(lhs_vars, rhs_var) => {
1275            read_list(i, r_constr, lhs_vars)?;
1276            read_var(i, r_constr, rhs_var)?;
1277            Ok(())
1278        }
1279        Constraint::Ineq(var1, var2, c) => {
1280            read_var(i, r_constr, var1)?;
1281            read_var(i, r_constr, var2)?;
1282            read_constant(r_constr, c)?;
1283            Ok(())
1284        }
1285        Constraint::Eq(var1, var2) => {
1286            read_var(i, r_constr, var1)?;
1287            read_var(i, r_constr, var2)?;
1288            Ok(())
1289        }
1290        Constraint::Difference((a, b), c) => {
1291            read_2_vars(i, r_constr, a, b)?;
1292            read_var(i, r_constr, c)?;
1293            Ok(())
1294        }
1295        Constraint::Div((a, b), c) => {
1296            read_2_vars(i, r_constr, a, b)?;
1297            read_var(i, r_constr, c)?;
1298            Ok(())
1299        }
1300        Constraint::DivUndefZero((a, b), c) => {
1301            read_2_vars(i, r_constr, a, b)?;
1302            read_var(i, r_constr, c)?;
1303            Ok(())
1304        }
1305        Constraint::Modulo((a, b), c) => {
1306            read_2_vars(i, r_constr, a, b)?;
1307            read_var(i, r_constr, c)?;
1308            Ok(())
1309        }
1310        Constraint::ModuloUndefZero((a, b), c) => {
1311            read_2_vars(i, r_constr, a, b)?;
1312            read_var(i, r_constr, c)?;
1313            Ok(())
1314        }
1315        Constraint::Pow((a, b), c) => {
1316            read_2_vars(i, r_constr, a, b)?;
1317            read_var(i, r_constr, c)?;
1318            Ok(())
1319        }
1320        Constraint::Product((a, b), c) => {
1321            read_2_vars(i, r_constr, a, b)?;
1322            read_var(i, r_constr, c)?;
1323            Ok(())
1324        }
1325        Constraint::WeightedSumGeq(a, b, c) => {
1326            read_constant_list(r_constr, a)?;
1327            read_list(i, r_constr, b)?;
1328            read_var(i, r_constr, c)?;
1329            Ok(())
1330        }
1331        Constraint::WeightedSumLeq(a, b, c) => {
1332            read_constant_list(r_constr, a)?;
1333            read_list(i, r_constr, b)?;
1334            read_var(i, r_constr, c)?;
1335            Ok(())
1336        }
1337        Constraint::CheckAssign(a) => {
1338            read_constraint(i, r_constr, (**a).clone())?;
1339            Ok(())
1340        }
1341        Constraint::CheckGsa(a) => {
1342            read_constraint(i, r_constr, (**a).clone())?;
1343            Ok(())
1344        }
1345        Constraint::ForwardChecking(a) => {
1346            read_constraint(i, r_constr, (**a).clone())?;
1347            Ok(())
1348        }
1349        Constraint::Reify(a, b) => {
1350            read_constraint(i, r_constr, (**a).clone())?;
1351            read_var(i, r_constr, b)?;
1352            Ok(())
1353        }
1354        Constraint::ReifyImply(a, b) => {
1355            read_constraint(i, r_constr, (**a).clone())?;
1356            read_var(i, r_constr, b)?;
1357            Ok(())
1358        }
1359        Constraint::ReifyImplyQuick(a, b) => {
1360            read_constraint(i, r_constr, (**a).clone())?;
1361            read_var(i, r_constr, b)?;
1362            Ok(())
1363        }
1364        Constraint::WatchedAnd(a) => {
1365            read_constraint_list(i, r_constr, a)?;
1366            Ok(())
1367        }
1368        Constraint::WatchedOr(a) => {
1369            read_constraint_list(i, r_constr, a)?;
1370            Ok(())
1371        }
1372        Constraint::GacAllDiff(a) => {
1373            read_list(i, r_constr, a)?;
1374            Ok(())
1375        }
1376        Constraint::AllDiff(a) => {
1377            read_list(i, r_constr, a)?;
1378            Ok(())
1379        }
1380        Constraint::AllDiffMatrix(a, b) => {
1381            read_list(i, r_constr, a)?;
1382            read_constant(r_constr, b)?;
1383            Ok(())
1384        }
1385        Constraint::WatchSumGeq(a, b) => {
1386            read_list(i, r_constr, a)?;
1387            read_constant(r_constr, b)?;
1388            Ok(())
1389        }
1390        Constraint::WatchSumLeq(a, b) => {
1391            read_list(i, r_constr, a)?;
1392            read_constant(r_constr, b)?;
1393            Ok(())
1394        }
1395        Constraint::OccurrenceGeq(a, b, c) => {
1396            read_list(i, r_constr, a)?;
1397            read_constant(r_constr, b)?;
1398            read_constant(r_constr, c)?;
1399            Ok(())
1400        }
1401        Constraint::OccurrenceLeq(a, b, c) => {
1402            read_list(i, r_constr, a)?;
1403            read_constant(r_constr, b)?;
1404            read_constant(r_constr, c)?;
1405            Ok(())
1406        }
1407        Constraint::Occurrence(a, b, c) => {
1408            read_list(i, r_constr, a)?;
1409            read_constant(r_constr, b)?;
1410            read_var(i, r_constr, c)?;
1411            Ok(())
1412        }
1413        Constraint::LexLess(a, b)
1414        | Constraint::LexLeq(a, b)
1415        | Constraint::LexLeqRv(a, b)
1416        | Constraint::LexLeqQuick(a, b)
1417        | Constraint::LexLessQuick(a, b)
1418        | Constraint::WatchVecNeq(a, b)
1419        | Constraint::WatchVecExistsLess(a, b) => {
1420            read_list(i, r_constr, a)?;
1421            read_list(i, r_constr, b)?;
1422            Ok(())
1423        }
1424        Constraint::LitSumGeq(a, b, c) => {
1425            read_list(i, r_constr, a)?;
1426            read_constant_list(r_constr, b)?;
1427            read_constant(r_constr, c)?;
1428            Ok(())
1429        }
1430        Constraint::Gcc(a, b, c) | Constraint::GccWeak(a, b, c) => {
1431            read_list(i, r_constr, a)?;
1432            read_constant_list(r_constr, b)?;
1433            read_list(i, r_constr, c)?;
1434            Ok(())
1435        }
1436        Constraint::Hamming(a, b, c) | Constraint::NotHamming(a, b, c) => {
1437            read_list(i, r_constr, a)?;
1438            read_list(i, r_constr, b)?;
1439            read_constant(r_constr, c)?;
1440            Ok(())
1441        }
1442        Constraint::FrameUpdate(a, b, c, d, e) => {
1443            read_list(i, r_constr, a)?;
1444            read_list(i, r_constr, b)?;
1445            read_list(i, r_constr, c)?;
1446            read_list(i, r_constr, d)?;
1447            read_constant(r_constr, e)?;
1448            Ok(())
1449        }
1450        Constraint::NegativeTable(vars, tuple_list)
1451        | Constraint::Table(vars, tuple_list)
1452        | Constraint::GacSchema(vars, tuple_list)
1453        | Constraint::LightTable(vars, tuple_list)
1454        | Constraint::Mddc(vars, tuple_list)
1455        | Constraint::NegativeMddc(vars, tuple_list) => {
1456            read_list(i, r_constr, vars)?;
1457            read_tuple_list(r_constr, tuple_list)?;
1458            Ok(())
1459        }
1460        Constraint::Str2Plus(vars, table_var) => {
1461            // CT_STR references a tuple table by NAME, not a variable.
1462            // Instead of looking it up in the symbol table as a var
1463            // (which fails), resolve it against the instance's
1464            // tuple-table symbol table via `constraint_setTuplesByName`
1465            // — that reuses the existing shared_ptr so refcounts stay
1466            // correct.
1467            read_list(i, r_constr, vars)?;
1468            let name = match table_var {
1469                Var::NameRef(n) => n.clone(),
1470                other => {
1471                    return Err(MinionError::NotImplemented(format!(
1472                        "Str2Plus second argument must be Var::NameRef(tuple_table_name), got {other:?}"
1473                    )));
1474                }
1475            };
1476            let c_name = CString::new(name.clone())
1477                .map_err(|_| anyhow!("Tuple-table name {name:?} contains a null character."))?;
1478            ffi::constraint_setTuplesByName(r_constr, i, c_name.as_ptr());
1479            Ok(())
1480        }
1481        Constraint::ShortStr2(vars, short_tuples)
1482        | Constraint::HaggisGac(vars, short_tuples)
1483        | Constraint::HaggisGacStable(vars, short_tuples)
1484        | Constraint::ShortCTupleStr2(vars, short_tuples) => {
1485            read_list(i, r_constr, vars)?;
1486            read_short_tuple_list(r_constr, short_tuples)?;
1487            Ok(())
1488        }
1489        Constraint::Max(a, b)
1490        | Constraint::Min(a, b)
1491        | Constraint::NvalueGeq(a, b)
1492        | Constraint::NvalueLeq(a, b) => {
1493            read_list(i, r_constr, a)?;
1494            read_var(i, r_constr, b)?;
1495            Ok(())
1496        }
1497        Constraint::Element(vec, j, e)
1498        | Constraint::ElementOne(vec, j, e)
1499        | Constraint::ElementUndefZero(vec, j, e)
1500        | Constraint::WatchElement(vec, j, e)
1501        | Constraint::WatchElementOne(vec, j, e)
1502        | Constraint::WatchElementOneUndefZero(vec, j, e)
1503        | Constraint::WatchElementUndefZero(vec, j, e) => {
1504            read_list(i, r_constr, vec)?;
1505            read_var(i, r_constr, j)?;
1506            read_var(i, r_constr, e)?;
1507            Ok(())
1508        }
1509        Constraint::WLiteral(a, b) | Constraint::WNotLiteral(a, b) => {
1510            read_var(i, r_constr, a)?;
1511            read_constant(r_constr, b)?;
1512            Ok(())
1513        }
1514        Constraint::WInIntervalSet(var, consts)
1515        | Constraint::WInRange(var, consts)
1516        | Constraint::WNotInRange(var, consts)
1517        | Constraint::WInset(var, consts)
1518        | Constraint::WNotInset(var, consts) => {
1519            read_var(i, r_constr, var)?;
1520            read_constant_list(r_constr, consts)?;
1521            Ok(())
1522        }
1523        Constraint::Abs(a, b)
1524        | Constraint::DisEq(a, b)
1525        | Constraint::MinusEq(a, b)
1526        | Constraint::GacEq(a, b)
1527        | Constraint::WatchLess(a, b)
1528        | Constraint::WatchNeq(a, b) => {
1529            read_var(i, r_constr, a)?;
1530            read_var(i, r_constr, b)?;
1531            Ok(())
1532        }
1533        Constraint::AllDiffMatrix(vars, c) => {
1534            read_list(i, r_constr, vars)?;
1535            read_constant(r_constr, c)?;
1536            Ok(())
1537        }
1538
1539        Constraint::True => Ok(()),
1540        Constraint::False => Ok(()),
1541        #[allow(unreachable_patterns)]
1542        x => Err(MinionError::NotImplemented(format!("{x:?}"))),
1543    }
1544}
1545
1546// DO NOT call manually - this assumes that all needed vars are already in the symbol table.
1547// TODO not happy with this just assuming the name is in the symbol table
1548/// Resolve an AST Var to a raw FFI Var.
1549unsafe fn resolve_var(
1550    instance: *mut ffi::ProbSpec_CSPInstance,
1551    var: &Var,
1552) -> Result<ffi::ProbSpec_Var, MinionError> {
1553    match var {
1554        Var::NameRef(name) => {
1555            let c_str = CString::new(name.clone()).map_err(|_| {
1556                anyhow!(
1557                    "Variable name {:?} contains a null character.",
1558                    name.clone()
1559                )
1560            })?;
1561            let var_result = ffi::minion_getVarByName(instance, c_str.as_ptr() as _);
1562            check_minion_result(var_result.result)?;
1563            Ok(var_result.var)
1564        }
1565        Var::ConstantAsVar(n) => Ok(ffi::constantAsVar(*n)),
1566    }
1567}
1568
1569unsafe fn read_list(
1570    instance: *mut ffi::ProbSpec_CSPInstance,
1571    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1572    vars: &Vec<Var>,
1573) -> Result<(), MinionError> {
1574    let raw_vars = Scoped::new(ffi::vec_var_new(), |x| ffi::vec_var_free(x as _));
1575    for var in vars {
1576        let raw_var = resolve_var(instance, var)?;
1577        ffi::vec_var_push_back(raw_vars.ptr, raw_var);
1578    }
1579
1580    ffi::constraint_addList(raw_constraint, raw_vars.ptr);
1581
1582    Ok(())
1583}
1584
1585unsafe fn read_var(
1586    instance: *mut ffi::ProbSpec_CSPInstance,
1587    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1588    var: &Var,
1589) -> Result<(), MinionError> {
1590    let raw_vars = Scoped::new(ffi::vec_var_new(), |x| ffi::vec_var_free(x as _));
1591    let raw_var = resolve_var(instance, var)?;
1592    ffi::vec_var_push_back(raw_vars.ptr, raw_var);
1593    ffi::constraint_addList(raw_constraint, raw_vars.ptr);
1594
1595    Ok(())
1596}
1597
1598unsafe fn read_2_vars(
1599    instance: *mut ffi::ProbSpec_CSPInstance,
1600    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1601    var1: &Var,
1602    var2: &Var,
1603) -> Result<(), MinionError> {
1604    let mut raw_var = resolve_var(instance, var1)?;
1605    let mut raw_var2 = resolve_var(instance, var2)?;
1606    // todo: does this move or copy? I am confus!
1607    // TODO need to mkae the semantics of move vs copy / ownership clear in libminion!!
1608    // This shouldve leaked everywhere by now but i think libminion copies stuff??
1609    ffi::constraint_addTwoVars(raw_constraint, &mut raw_var, &mut raw_var2);
1610    Ok(())
1611}
1612
1613unsafe fn read_constant(
1614    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1615    constant: &Constant,
1616) -> Result<(), MinionError> {
1617    let val: i32 = match constant {
1618        Constant::Integer(n) => Ok(*n),
1619        Constant::Bool(true) => Ok(1),
1620        Constant::Bool(false) => Ok(0),
1621        x => Err(MinionError::NotImplemented(format!("{x:?}"))),
1622    }?;
1623
1624    ffi::constraint_addConstant(raw_constraint, val);
1625
1626    Ok(())
1627}
1628
1629unsafe fn read_constant_list(
1630    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1631    constants: &[Constant],
1632) -> Result<(), MinionError> {
1633    let raw_consts = Scoped::new(ffi::vec_int_new(), |x| ffi::vec_int_free(x as _));
1634
1635    for constant in constants.iter() {
1636        let val = match constant {
1637            Constant::Integer(n) => Ok(*n),
1638            Constant::Bool(true) => Ok(1),
1639            Constant::Bool(false) => Ok(0),
1640            #[allow(unreachable_patterns)] // TODO: can there be other types?
1641            x => Err(MinionError::NotImplemented(format!("{x:?}"))),
1642        }?;
1643
1644        ffi::vec_int_push_back(raw_consts.ptr, val);
1645    }
1646
1647    ffi::constraint_addConstantList(raw_constraint, raw_consts.ptr);
1648    Ok(())
1649}
1650
1651//TODO: check if the inner constraint is listed in the model or not?
1652//Does this matter?
1653// TODO: type-check inner constraints vars and tuples and so on?
1654unsafe fn read_constraint(
1655    instance: *mut ffi::ProbSpec_CSPInstance,
1656    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1657    inner_constraint: Constraint,
1658) -> Result<(), MinionError> {
1659    let constraint_type = get_constraint_type(&inner_constraint)?;
1660    let raw_inner_constraint = Scoped::new(ffi::constraint_new(constraint_type), |x| {
1661        ffi::constraint_free(x as _)
1662    });
1663
1664    constraint_add_args(instance, raw_inner_constraint.ptr, &inner_constraint)?;
1665
1666    ffi::constraint_addConstraint(raw_constraint, raw_inner_constraint.ptr);
1667    Ok(())
1668}
1669
1670unsafe fn read_constraint_list(
1671    instance: *mut ffi::ProbSpec_CSPInstance,
1672    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1673    inner_constraints: &[Constraint],
1674) -> Result<(), MinionError> {
1675    let raw_inners = Scoped::new(ffi::vec_constraints_new(), |x| {
1676        ffi::vec_constraints_free(x as _)
1677    });
1678    for inner_constraint in inner_constraints.iter() {
1679        let constraint_type = get_constraint_type(inner_constraint)?;
1680        let raw_inner_constraint = Scoped::new(ffi::constraint_new(constraint_type), |x| {
1681            ffi::constraint_free(x as _)
1682        });
1683
1684        constraint_add_args(instance, raw_inner_constraint.ptr, inner_constraint)?;
1685        ffi::vec_constraints_push_back(raw_inners.ptr, raw_inner_constraint.ptr);
1686    }
1687
1688    ffi::constraint_addConstraintList(raw_constraint, raw_inners.ptr);
1689    Ok(())
1690}
1691
1692unsafe fn read_tuple_list(
1693    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1694    tuples: &Vec<Tuple>,
1695) -> Result<(), MinionError> {
1696    // a tuple list is just a vec<vec<int>>, where each inner vec is a tuple
1697    let raw_tuples = Scoped::new(ffi::vec_vec_int_new(), |x| ffi::vec_vec_int_free(x as _));
1698    for tuple in tuples {
1699        let raw_tuple = Scoped::new(ffi::vec_int_new(), |x| ffi::vec_int_free(x as _));
1700        for constant in tuple.iter() {
1701            let val = match constant {
1702                Constant::Integer(n) => Ok(*n),
1703                Constant::Bool(true) => Ok(1),
1704                Constant::Bool(false) => Ok(0),
1705                #[allow(unreachable_patterns)] // TODO: can there be other types?
1706                x => Err(MinionError::NotImplemented(format!("{:?}", x))),
1707            }?;
1708
1709            ffi::vec_int_push_back(raw_tuple.ptr, val);
1710        }
1711
1712        ffi::vec_vec_int_push_back_ptr(raw_tuples.ptr, raw_tuple.ptr);
1713    }
1714
1715    // `constraint_setTuples` transfers ownership of `TupleList` into Minion via shared_ptr.
1716    // Do not wrap this pointer in `Scoped` or it will be freed too early.
1717    let raw_tuple_list = ffi::tupleList_new(raw_tuples.ptr);
1718    ffi::constraint_setTuples(raw_constraint, raw_tuple_list);
1719
1720    Ok(())
1721}
1722
1723/// Build a `ShortTupleList` from a slice of [`ShortTuple`] and
1724/// attach it to `raw_constraint`. The C FFI takes a flat encoding
1725/// — each short tuple is one `vec<int>` carrying alternating
1726/// `[idx_0, val_0, idx_1, val_1, ...]` ints — so we materialise
1727/// that here.
1728unsafe fn read_short_tuple_list(
1729    raw_constraint: *mut ffi::ProbSpec_ConstraintBlob,
1730    short_tuples: &Vec<ShortTuple>,
1731) -> Result<(), MinionError> {
1732    let raw_outer = Scoped::new(ffi::vec_vec_int_new(), |x| ffi::vec_vec_int_free(x as _));
1733    for short in short_tuples {
1734        let raw_inner = Scoped::new(ffi::vec_int_new(), |x| ffi::vec_int_free(x as _));
1735        for &(idx, ref constant) in short {
1736            let val = match constant {
1737                Constant::Integer(n) => Ok(*n),
1738                Constant::Bool(true) => Ok(1),
1739                Constant::Bool(false) => Ok(0),
1740                #[allow(unreachable_patterns)]
1741                x => Err(MinionError::NotImplemented(format!("{:?}", x))),
1742            }?;
1743            ffi::vec_int_push_back(raw_inner.ptr, idx as i32);
1744            ffi::vec_int_push_back(raw_inner.ptr, val);
1745        }
1746        ffi::vec_vec_int_push_back_ptr(raw_outer.ptr, raw_inner.ptr);
1747    }
1748
1749    // `constraint_setShortTuples` takes ownership via shared_ptr;
1750    // do not wrap the result in `Scoped`.
1751    let raw_short_list = ffi::shortTupleList_new(raw_outer.ptr);
1752    ffi::constraint_setShortTuples(raw_constraint, raw_short_list);
1753
1754    Ok(())
1755}