Skip to main content

oxiz_solver/
z3_compat_ext2.rs

1//! Z3 API Compatibility Layer — Extension 2
2//!
3//! This module implements seven additional Z3-compatible surfaces on top of
4//! the core types defined in [`crate::z3_compat`] and the first extension
5//! layer in [`crate::z3_compat::ext`]:
6//!
7//! - [`Z3Statistics`]         — key/value statistics after solving
8//! - [`Z3Params`] / [`ParamVal`] — solver parameter bags
9//! - [`Z3Probe`]              — goal-analysis probes (size, depth, is-qfbv, …)
10//! - [`Z3Goal`]               — goal wrapper for tactic input
11//! - [`Z3Tactic`]             — named tactic factory + combinators
12//! - [`Z3ApplyResult`]        — result of tactic application
13//! - [`Z3DatatypeSort`] / [`Z3Constructor`] — algebraic datatype declarations
14//! - `check_assumptions` / `unsat_core` methods on [`Z3Solver`]
15//! - [`Z3AstVector`]          — a simple ordered collection of boolean terms
16
17use std::collections::HashMap;
18use std::rc::Rc;
19
20use oxiz_core::ast::{TermId, TermManager};
21use oxiz_core::sort::SortId;
22use oxiz_core::tactic::DepthProbe;
23use oxiz_core::tactic::{
24    Goal, HasArrayProbe, HasBitVectorProbe, HasQuantifierProbe, IsLinearProbe, NodeCountProbe,
25    Probe, SizeProbe, TacticResult,
26};
27use oxiz_theories::datatype::{Constructor, DatatypeDecl};
28
29use crate::solver::SolverConfig;
30use crate::z3_compat::{Bool, SatResult, Z3Context, Z3Solver};
31
32// ─── Z3Statistics ────────────────────────────────────────────────────────────
33
34/// Z3-style statistics object produced after a `check()` call.
35///
36/// Iterates the solver's internal [`Statistics`] struct as a flat list of
37/// named `f64` entries so callers don't need to know the internal field names.
38///
39/// [`Statistics`]: oxiz_core::statistics::Statistics
40pub struct Z3Statistics {
41    /// Pairs of (key, value) extracted from the solver statistics.
42    pairs: Vec<(&'static str, f64)>,
43}
44
45impl Z3Statistics {
46    /// Build a [`Z3Statistics`] from the solver's internal statistics.
47    fn from_solver_stats(stats: &crate::solver::Statistics) -> Self {
48        let pairs: Vec<(&'static str, f64)> = vec![
49            ("decisions", stats.decisions as f64),
50            ("propagations", stats.propagations as f64),
51            ("conflicts", stats.conflicts as f64),
52            ("restarts", stats.restarts as f64),
53            ("learned-clauses", stats.learned_clauses as f64),
54            ("theory-propagations", stats.theory_propagations as f64),
55            ("theory-conflicts", stats.theory_conflicts as f64),
56        ];
57        Self { pairs }
58    }
59
60    /// Number of statistical keys.
61    #[must_use]
62    pub fn num_keys(&self) -> usize {
63        self.pairs.len()
64    }
65
66    /// Return the key at index `i`.
67    ///
68    /// # Panics
69    ///
70    /// Panics if `i >= self.num_keys()`.
71    #[must_use]
72    pub fn key(&self, i: usize) -> &str {
73        self.pairs[i].0
74    }
75
76    /// Return the value at index `i` as an `f64`.
77    ///
78    /// # Panics
79    ///
80    /// Panics if `i >= self.num_keys()`.
81    #[must_use]
82    pub fn value(&self, i: usize) -> f64 {
83        self.pairs[i].1
84    }
85
86    /// Look up a statistic by name.  Returns `None` if the key is absent.
87    #[must_use]
88    pub fn get(&self, key: &str) -> Option<f64> {
89        self.pairs.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
90    }
91
92    /// Format all statistics as a multi-line string (for debugging).
93    #[must_use]
94    pub fn to_stat_string(&self) -> String {
95        let mut s = String::new();
96        for (k, v) in &self.pairs {
97            s.push_str(&format!("  {} = {}\n", k, v));
98        }
99        s
100    }
101}
102
103impl std::fmt::Display for Z3Statistics {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(f, "{}", self.to_stat_string())
106    }
107}
108
109// ─── Z3Solver::statistics ─────────────────────────────────────────────────────
110
111impl Z3Solver {
112    /// Return a snapshot of solver statistics after the last `check()` call.
113    #[must_use]
114    pub fn statistics(&self) -> Z3Statistics {
115        Z3Statistics::from_solver_stats(self.ctx.raw_statistics())
116    }
117}
118
119// ─── Z3Params / ParamVal ─────────────────────────────────────────────────────
120
121/// A parameter value that can be stored in a [`Z3Params`] bag.
122#[derive(Debug, Clone)]
123pub enum ParamVal {
124    /// A boolean parameter.
125    Bool(bool),
126    /// An unsigned integer parameter.
127    UInt(u64),
128    /// A double-precision floating-point parameter.
129    Double(f64),
130    /// A string parameter.
131    Str(String),
132}
133
134/// Analogue of `z3::Params`.
135///
136/// A key/value bag that can be applied to a [`Z3Solver`] to override solver
137/// configuration options before calling `check()`.
138#[derive(Debug, Clone, Default)]
139pub struct Z3Params {
140    map: HashMap<String, ParamVal>,
141}
142
143impl Z3Params {
144    /// Create an empty parameter bag.
145    #[must_use]
146    pub fn new(_ctx: &Z3Context) -> Self {
147        Self {
148            map: HashMap::new(),
149        }
150    }
151
152    /// Set a boolean parameter (e.g. `"mbqi"`, `"proof"`).
153    pub fn set_bool(&mut self, key: &str, val: bool) {
154        self.map.insert(key.to_string(), ParamVal::Bool(val));
155    }
156
157    /// Set an unsigned-integer parameter (e.g. `"seed"`, `"threads"`).
158    pub fn set_u32(&mut self, key: &str, val: u64) {
159        self.map.insert(key.to_string(), ParamVal::UInt(val));
160    }
161
162    /// Set a double-precision parameter (e.g. `"var-decay"`).
163    pub fn set_double(&mut self, key: &str, val: f64) {
164        self.map.insert(key.to_string(), ParamVal::Double(val));
165    }
166
167    /// Set a string parameter (e.g. `"logic"`).
168    pub fn set_str(&mut self, key: &str, val: &str) {
169        self.map
170            .insert(key.to_string(), ParamVal::Str(val.to_string()));
171    }
172
173    /// Return the underlying map.
174    #[must_use]
175    pub fn as_map(&self) -> &HashMap<String, ParamVal> {
176        &self.map
177    }
178}
179
180/// Apply a [`Z3Params`] bag to the solver, mapping common keys to
181/// [`SolverConfig`] fields.
182impl Z3Solver {
183    /// Apply a parameter bag to the solver.
184    ///
185    /// Recognised keys:
186    /// - `"timeout"` / `"timeout_ms"` (UInt or Double) → `config.timeout_ms`
187    /// - `"seed"` (UInt) → (accepted but currently no-op for reproducibility)
188    /// - `"mbqi"` (Bool) → no direct field; forwarded to the underlying solver
189    ///   option `"mbqi"`.
190    /// - `"max-conflicts"` (UInt) → `config.max_conflicts`
191    /// - `"max-decisions"` (UInt) → `config.max_decisions`
192    /// - `"proof"` (Bool) → `config.proof`
193    pub fn set_params(&mut self, params: &Z3Params) {
194        let mut config: SolverConfig = self.ctx.solver_config().clone();
195
196        for (key, val) in &params.map {
197            match (key.as_str(), val) {
198                ("timeout" | "timeout_ms", ParamVal::UInt(ms)) => {
199                    config.timeout_ms = *ms;
200                }
201                ("timeout", ParamVal::Double(ms)) => {
202                    config.timeout_ms = *ms as u64;
203                }
204                ("max-conflicts", ParamVal::UInt(n)) => {
205                    config.max_conflicts = *n;
206                }
207                ("max-decisions", ParamVal::UInt(n)) => {
208                    config.max_decisions = *n;
209                }
210                ("proof", ParamVal::Bool(b)) => {
211                    config.proof = *b;
212                }
213                // "seed" / "mbqi" / unknown keys: accepted silently.
214                _ => {}
215            }
216        }
217
218        self.ctx.set_solver_config(config);
219    }
220}
221
222// ─── Z3Goal ──────────────────────────────────────────────────────────────────
223
224/// Analogue of `z3::Goal`.
225///
226/// A container of [`Bool`] assertions that can be fed to a [`Z3Tactic`].
227pub struct Z3Goal {
228    /// Inner goal from the tactic framework.
229    inner: Goal,
230    /// Back-reference to the context for term building.
231    ctx_tm: Rc<std::cell::RefCell<TermManager>>,
232}
233
234impl Z3Goal {
235    /// Create a new empty goal.
236    #[must_use]
237    pub fn new(ctx: &Z3Context) -> Self {
238        Self {
239            inner: Goal::empty(),
240            ctx_tm: ctx.tm.clone(),
241        }
242    }
243
244    /// Add an assertion to the goal.
245    pub fn assert(&mut self, b: &Bool) {
246        self.inner.add(b.id);
247    }
248
249    /// Number of assertions in this goal.
250    #[must_use]
251    pub fn size(&self) -> usize {
252        self.inner.len()
253    }
254
255    /// Retrieve the `i`-th assertion as a [`Bool`].
256    ///
257    /// # Panics
258    ///
259    /// Panics if `i >= self.size()`.
260    #[must_use]
261    pub fn get_formula(&self, i: usize) -> Bool {
262        Bool {
263            id: self.inner.assertions[i],
264        }
265    }
266
267    /// Returns `true` if the goal has been determined to be satisfiable
268    /// (all assertions simplified to `true`).
269    #[must_use]
270    pub fn is_decided_sat(&self) -> bool {
271        if self.inner.is_empty() {
272            return true;
273        }
274        let tm = self.ctx_tm.borrow();
275        let true_id = tm.mk_true_ro();
276        self.inner.assertions.iter().all(|&a| a == true_id)
277    }
278
279    /// Returns `true` if the goal has been determined to be unsatisfiable
280    /// (at least one assertion is `false`).
281    #[must_use]
282    pub fn is_decided_unsat(&self) -> bool {
283        let tm = self.ctx_tm.borrow();
284        let false_id = tm.mk_false_ro();
285        self.inner.assertions.contains(&false_id)
286    }
287
288    /// Access the underlying [`Goal`] for use with the tactic framework.
289    #[must_use]
290    pub fn as_inner(&self) -> &Goal {
291        &self.inner
292    }
293}
294
295// ─── Z3ApplyResult ────────────────────────────────────────────────────────────
296
297/// Analogue of `z3::ApplyResult`.
298///
299/// Holds the sub-goals produced by applying a [`Z3Tactic`] to a [`Z3Goal`].
300pub struct Z3ApplyResult {
301    subgoals: Vec<Z3Goal>,
302}
303
304impl Z3ApplyResult {
305    /// Number of sub-goals in this result.
306    #[must_use]
307    pub fn num_subgoals(&self) -> usize {
308        self.subgoals.len()
309    }
310
311    /// Return a reference to the `i`-th sub-goal.
312    ///
313    /// # Panics
314    ///
315    /// Panics if `i >= self.num_subgoals()`.
316    #[must_use]
317    pub fn get_subgoal(&self, i: usize) -> &Z3Goal {
318        &self.subgoals[i]
319    }
320}
321
322// ─── TacticKind — internal enum ───────────────────────────────────────────────
323
324/// Internal representation of a tactic, used to allow cheap cloning and
325/// combinator construction without requiring all concrete tactic types to
326/// implement `Clone`.
327#[derive(Clone)]
328enum TacticKind {
329    /// A named leaf tactic.
330    Named(String),
331    /// Sequential composition.
332    Then(Box<TacticKind>, Box<TacticKind>),
333    /// Fallback composition.
334    OrElse(Box<TacticKind>, Box<TacticKind>),
335    /// Fixed-point repetition.
336    Repeat(Box<TacticKind>),
337    /// Time-limited wrapper (milliseconds).
338    TryFor(Box<TacticKind>, u64),
339}
340
341impl TacticKind {
342    /// Apply this tactic kind to a [`Goal`].
343    fn apply_to_goal(&self, goal: &Goal) -> TacticResult {
344        match self {
345            TacticKind::Named(name) => apply_named_tactic(name.as_str(), goal),
346            TacticKind::Then(a, b) => {
347                let first_result = a.apply_to_goal(goal);
348                match first_result {
349                    TacticResult::SubGoals(sub) => {
350                        // Apply `b` to each sub-goal, collecting results.
351                        let mut combined: Vec<Goal> = Vec::new();
352                        for sg in sub {
353                            match b.apply_to_goal(&sg) {
354                                TacticResult::SubGoals(more) => combined.extend(more),
355                                TacticResult::Solved(r) => {
356                                    return TacticResult::Solved(r);
357                                }
358                                TacticResult::NotApplicable => combined.push(sg),
359                                TacticResult::Failed(msg) => {
360                                    return TacticResult::Failed(msg);
361                                }
362                            }
363                        }
364                        TacticResult::SubGoals(combined)
365                    }
366                    other => other,
367                }
368            }
369            TacticKind::OrElse(a, b) => {
370                let r = a.apply_to_goal(goal);
371                if matches!(r, TacticResult::NotApplicable) {
372                    b.apply_to_goal(goal)
373                } else {
374                    r
375                }
376            }
377            TacticKind::Repeat(inner) => {
378                let mut current = goal.clone();
379                for _ in 0..1000_usize {
380                    match inner.apply_to_goal(&current) {
381                        TacticResult::Solved(r) => return TacticResult::Solved(r),
382                        TacticResult::SubGoals(sub) if sub.len() == 1 => {
383                            if sub[0].assertions == current.assertions {
384                                break; // fixed-point
385                            }
386                            current = sub
387                                .into_iter()
388                                .next()
389                                .expect("sub.len() == 1 guarantees exactly one element");
390                        }
391                        TacticResult::SubGoals(sub) => {
392                            return TacticResult::SubGoals(sub);
393                        }
394                        TacticResult::NotApplicable => break,
395                        TacticResult::Failed(msg) => return TacticResult::Failed(msg),
396                    }
397                }
398                TacticResult::SubGoals(vec![current])
399            }
400            TacticKind::TryFor(inner, _ms) => {
401                // Best-effort: run synchronously; timeout semantics are
402                // honoured by the underlying solver's conflict limit, not by
403                // wall-clock here.
404                inner.apply_to_goal(goal)
405            }
406        }
407    }
408}
409
410/// Lazily-built, process-wide [`TacticRegistry`].
411///
412/// [`default_registry`] allocates and registers all 19 canonical tactics on
413/// every call.  `apply_named_tactic` is on a hot path — the `Repeat` and `Then`
414/// combinators in [`TacticKind::apply_to_goal`] can invoke it up to 1000 times
415/// for a single `Z3Tactic::apply` — so we build the registry exactly once and
416/// share it behind a [`OnceLock`].
417///
418/// This is only sound because [`TacticRegistry`] is `Send + Sync`: its factory
419/// closures are stored as `Box<dyn Fn() -> Box<dyn Tactic> + Send + Sync>`.
420///
421/// [`default_registry`]: oxiz_core::tactic::default_registry
422/// [`TacticRegistry`]: oxiz_core::tactic::TacticRegistry
423fn tactic_registry() -> &'static oxiz_core::tactic::TacticRegistry {
424    use oxiz_core::tactic::{TacticRegistry, default_registry};
425    use std::sync::OnceLock;
426    static REG: OnceLock<TacticRegistry> = OnceLock::new();
427    REG.get_or_init(default_registry)
428}
429
430/// Dispatch a named tactic via the canonical [`TacticRegistry`].
431///
432/// Backend-only tactics that need a full solver (`"smt"`, `"sat"`) are not in
433/// the registry; they fall through to the `None` branch and return the goal
434/// unchanged so a tactic pipeline can continue on to the solver backend.
435///
436/// [`TacticRegistry`]: oxiz_core::tactic::TacticRegistry
437fn apply_named_tactic(name: &str, goal: &Goal) -> TacticResult {
438    // Backward-compatibility aliases: map historical Z3 short-form names onto
439    // the registry's canonical keys.
440    let canonical = match name {
441        "ctx-simplify" => "ctx-solver-simplify",
442        other => other,
443    };
444
445    match tactic_registry().create(canonical) {
446        Some(tactic) => tactic.apply(goal).unwrap_or(TacticResult::NotApplicable),
447        None => {
448            // Unknown / backend-only tactic (e.g. "smt", "sat"): return goal
449            // unchanged so a tactic pipeline can continue to the solver backend.
450            TacticResult::SubGoals(vec![goal.clone()])
451        }
452    }
453}
454
455// ─── Z3Tactic ────────────────────────────────────────────────────────────────
456
457/// Analogue of `z3::Tactic`.
458///
459/// A Z3Tactic wraps a `TacticKind` tree and can be combined with other
460/// tactics using `.then()`, `.or_else()`, `.repeat()`, and `.try_for()`.
461#[derive(Clone)]
462pub struct Z3Tactic {
463    kind: TacticKind,
464}
465
466impl Z3Tactic {
467    /// Create a tactic by name.
468    ///
469    /// Names are resolved through the canonical
470    /// [`TacticRegistry`](oxiz_core::tactic::TacticRegistry), so every tactic
471    /// registered by
472    /// [`default_registry`](oxiz_core::tactic::default_registry) is reachable —
473    /// e.g. `"simplify"`, `"propagate-values"`, `"ctx-solver-simplify"`,
474    /// `"bit-blast"`, `"ackermannize"`, `"solve-eqs"`, `"nnf"`, `"tseitin-cnf"`,
475    /// `"fm"`, `"pb2bv"`, `"split"`, `"skip"`, and more.
476    ///
477    /// The historical short form `"ctx-simplify"` is accepted as an alias for
478    /// `"ctx-solver-simplify"`.
479    ///
480    /// Backend-only names that require a full solver (`"smt"`, `"sat"`) and any
481    /// unrecognised name are accepted and return the goal unchanged so a
482    /// pipeline can continue on to the solver backend.
483    #[must_use]
484    pub fn new(_ctx: &Z3Context, name: &str) -> Self {
485        Self {
486            kind: TacticKind::Named(name.to_string()),
487        }
488    }
489
490    /// Apply this tactic to `goal`.
491    ///
492    /// Returns a [`Z3ApplyResult`] containing zero or more sub-goals.
493    #[must_use]
494    pub fn apply(&self, ctx: &Z3Context, goal: &Z3Goal) -> Z3ApplyResult {
495        let raw_result = self.kind.apply_to_goal(goal.as_inner());
496        let ctx_tm = ctx.tm.clone();
497        let subgoals = match raw_result {
498            TacticResult::SubGoals(goals) => goals
499                .into_iter()
500                .map(|g| Z3Goal {
501                    inner: g,
502                    ctx_tm: ctx_tm.clone(),
503                })
504                .collect(),
505            TacticResult::Solved(_) | TacticResult::NotApplicable | TacticResult::Failed(_) => {
506                // A solved / inapplicable / failed result produces no sub-goals.
507                Vec::new()
508            }
509        };
510        Z3ApplyResult { subgoals }
511    }
512
513    /// Sequential composition: apply `self`, then apply `other` to each
514    /// resulting sub-goal.
515    #[must_use]
516    pub fn then(&self, other: &Z3Tactic) -> Self {
517        Self {
518            kind: TacticKind::Then(Box::new(self.kind.clone()), Box::new(other.kind.clone())),
519        }
520    }
521
522    /// Fallback composition: if `self` returns `NotApplicable`, apply `other`.
523    #[must_use]
524    pub fn or_else(&self, other: &Z3Tactic) -> Self {
525        Self {
526            kind: TacticKind::OrElse(Box::new(self.kind.clone()), Box::new(other.kind.clone())),
527        }
528    }
529
530    /// Repeat `self` until a fixed-point is reached (at most 1000 iterations).
531    #[must_use]
532    pub fn repeat(&self) -> Self {
533        Self {
534            kind: TacticKind::Repeat(Box::new(self.kind.clone())),
535        }
536    }
537
538    /// Wrap `self` with a millisecond timeout.
539    ///
540    /// The timeout is stored but currently enforced at the solver level via the
541    /// `timeout_ms` config field, not by a wall-clock check in the tactic runner.
542    #[must_use]
543    pub fn try_for(&self, ms: u64) -> Self {
544        Self {
545            kind: TacticKind::TryFor(Box::new(self.kind.clone()), ms),
546        }
547    }
548}
549
550// ─── Z3Probe ─────────────────────────────────────────────────────────────────
551
552/// Internal enum of concrete probe implementations.
553///
554/// Using an enum rather than `Box<dyn Probe>` lets [`Z3Probe`] be cheaply
555/// composed without heap allocation on the hot path.
556#[derive(Clone)]
557enum ProbeKind {
558    Size,
559    NodeCount,
560    Depth,
561    HasQuantifier,
562    IsLinear,
563    HasBitVector,
564    HasArray,
565    Const(f64),
566    /// Combinator: result = 1.0 if left < right, else 0.0.
567    Lt(Box<ProbeKind>, Box<ProbeKind>),
568    /// Combinator: result = 1.0 if left > right, else 0.0.
569    Gt(Box<ProbeKind>, Box<ProbeKind>),
570}
571
572impl ProbeKind {
573    fn evaluate(&self, goal: &Goal, tm: &TermManager) -> f64 {
574        match self {
575            ProbeKind::Size => SizeProbe.evaluate(goal, tm),
576            ProbeKind::NodeCount => NodeCountProbe.evaluate(goal, tm),
577            ProbeKind::Depth => DepthProbe.evaluate(goal, tm),
578            ProbeKind::HasQuantifier => HasQuantifierProbe.evaluate(goal, tm),
579            ProbeKind::IsLinear => IsLinearProbe.evaluate(goal, tm),
580            ProbeKind::HasBitVector => HasBitVectorProbe.evaluate(goal, tm),
581            ProbeKind::HasArray => HasArrayProbe.evaluate(goal, tm),
582            ProbeKind::Const(v) => *v,
583            ProbeKind::Lt(a, b) => {
584                if a.evaluate(goal, tm) < b.evaluate(goal, tm) {
585                    1.0
586                } else {
587                    0.0
588                }
589            }
590            ProbeKind::Gt(a, b) => {
591                if a.evaluate(goal, tm) > b.evaluate(goal, tm) {
592                    1.0
593                } else {
594                    0.0
595                }
596            }
597        }
598    }
599}
600
601/// Analogue of `z3::Probe`.
602///
603/// A function that analyses a [`Z3Goal`] and returns a numeric value.
604/// Probes can be combined using `.lt()` and `.gt()` combinators.
605#[derive(Clone)]
606pub struct Z3Probe {
607    kind: ProbeKind,
608}
609
610impl Z3Probe {
611    /// Create a probe by name.
612    ///
613    /// Supported names:
614    /// - `"size"` — number of assertions
615    /// - `"num-exprs"` / `"num-consts"` — total unique term nodes
616    /// - `"depth"` — maximum term depth
617    /// - `"has-quantifiers"` / `"is-quantified"` — quantifier check
618    /// - `"is-linear"` / `"is-qflia"` — linearity check
619    /// - `"is-qfbv"` / `"has-bitvector"` — bitvector check
620    /// - `"has-array"` — array check
621    /// - any unrecognised name — constant 0.0
622    #[must_use]
623    pub fn new(_ctx: &Z3Context, name: &str) -> Self {
624        let kind = match name {
625            "size" => ProbeKind::Size,
626            "num-exprs" | "num-consts" => ProbeKind::NodeCount,
627            "depth" => ProbeKind::Depth,
628            "has-quantifiers" | "is-quantified" => ProbeKind::HasQuantifier,
629            "is-linear" | "is-qflia" => ProbeKind::IsLinear,
630            "is-qfbv" | "has-bitvector" => ProbeKind::HasBitVector,
631            "has-array" => ProbeKind::HasArray,
632            _ => ProbeKind::Const(0.0),
633        };
634        Self { kind }
635    }
636
637    /// Evaluate the probe on `goal`.  Returns a numeric value.
638    #[must_use]
639    pub fn apply(&self, ctx: &Z3Context, goal: &Z3Goal) -> f64 {
640        let tm = ctx.tm.borrow();
641        self.kind.evaluate(goal.as_inner(), &tm)
642    }
643
644    /// Combinator: returns a probe that is 1.0 iff `self < other`.
645    #[must_use]
646    pub fn lt(self, other: Z3Probe) -> Z3Probe {
647        Z3Probe {
648            kind: ProbeKind::Lt(Box::new(self.kind), Box::new(other.kind)),
649        }
650    }
651
652    /// Combinator: returns a probe that is 1.0 iff `self > other`.
653    #[must_use]
654    pub fn gt(self, other: Z3Probe) -> Z3Probe {
655        Z3Probe {
656            kind: ProbeKind::Gt(Box::new(self.kind), Box::new(other.kind)),
657        }
658    }
659}
660
661// ─── Z3DatatypeSort / Z3Constructor ──────────────────────────────────────────
662
663/// A field specification in a [`Z3Constructor`].
664#[derive(Debug, Clone)]
665pub struct Z3Field {
666    /// Field name.
667    pub name: String,
668    /// Field sort (as a string, e.g. `"Int"`, `"Bool"`, `"Real"`).
669    pub sort_name: String,
670}
671
672/// A constructor specification for a [`Z3DatatypeSort`].
673#[derive(Debug, Clone)]
674pub struct Z3Constructor {
675    /// Constructor name (e.g. `"Cons"`, `"Nil"`).
676    pub name: String,
677    /// Fields of this constructor.
678    pub fields: Vec<Z3Field>,
679}
680
681/// Analogue of `z3::DatatypeSort`.
682///
683/// Wraps a [`DatatypeDecl`] and provides indexed access to constructor,
684/// recogniser, and accessor [`FuncDecl`]s.
685///
686/// [`FuncDecl`]: crate::z3_compat::ext::FuncDecl
687pub struct Z3DatatypeSort {
688    decl: DatatypeDecl,
689    sort_id: SortId,
690}
691
692impl Z3DatatypeSort {
693    /// Declare a new algebraic datatype from a list of [`Z3Constructor`]s.
694    ///
695    /// # Panics
696    ///
697    /// Panics if `constructors` is empty.
698    #[must_use]
699    pub fn new(ctx: &Z3Context, name: &str, constructors: &[Z3Constructor]) -> Self {
700        assert!(
701            !constructors.is_empty(),
702            "Z3DatatypeSort requires at least one constructor"
703        );
704
705        let mut decl = DatatypeDecl::new(name);
706        for (tag, z3_con) in constructors.iter().enumerate() {
707            let mut con = Constructor::new(z3_con.name.clone(), tag as u32);
708            for field in &z3_con.fields {
709                con = con.with_field(field.name.clone(), field.sort_name.clone());
710            }
711            decl = decl.with_constructor(con);
712        }
713
714        // Register the sort in the term manager so that TermId-based usage
715        // can reference it by SortId.
716        let sort_id = ctx.tm.borrow_mut().sorts.mk_datatype_sort(name);
717
718        Self { decl, sort_id }
719    }
720
721    /// Number of constructors in this datatype.
722    #[must_use]
723    pub fn num_constructors(&self) -> usize {
724        self.decl.constructors.len()
725    }
726
727    /// Return the [`crate::z3_compat::ext::FuncDecl`] for the `i`-th constructor.
728    ///
729    /// The returned `FuncDecl` has arity equal to the number of fields of the
730    /// constructor, and its range is this datatype's sort.
731    ///
732    /// # Panics
733    ///
734    /// Panics if `i >= self.num_constructors()`.
735    #[must_use]
736    pub fn constructor(&self, ctx: &Z3Context, i: usize) -> crate::z3_compat::ext::FuncDecl {
737        let con = &self.decl.constructors[i];
738        // Each field maps to the range sort declared for it; for simplicity we
739        // use the context's int sort as a placeholder for unknown sorts and
740        // look up Bool/Real specifically.
741        let domain: Vec<SortId> = con
742            .fields
743            .iter()
744            .map(|f| sort_name_to_id(ctx, &f.sort))
745            .collect();
746        crate::z3_compat::ext::FuncDecl::new(ctx, &con.name, &domain, self.sort_id)
747    }
748
749    /// Return the recogniser [`crate::z3_compat::ext::FuncDecl`] for the `i`-th constructor.
750    ///
751    /// The recogniser takes one argument of this datatype's sort and returns
752    /// `Bool`.  Its name is `"is-<constructor-name>"`.
753    ///
754    /// # Panics
755    ///
756    /// Panics if `i >= self.num_constructors()`.
757    #[must_use]
758    pub fn recognizer(&self, ctx: &Z3Context, i: usize) -> crate::z3_compat::ext::FuncDecl {
759        let con = &self.decl.constructors[i];
760        let name = format!("is-{}", con.name);
761        let bool_sort = ctx.bool_sort();
762        crate::z3_compat::ext::FuncDecl::new(ctx, &name, &[self.sort_id], bool_sort)
763    }
764
765    /// Return the accessor [`crate::z3_compat::ext::FuncDecl`] for field `field_i` of constructor `con_i`.
766    ///
767    /// The accessor takes one argument of this datatype's sort and returns the
768    /// sort of the field.
769    ///
770    /// # Panics
771    ///
772    /// Panics if `con_i >= self.num_constructors()` or
773    /// `field_i >= constructors[con_i].fields.len()`.
774    #[must_use]
775    pub fn accessor(
776        &self,
777        ctx: &Z3Context,
778        con_i: usize,
779        field_i: usize,
780    ) -> crate::z3_compat::ext::FuncDecl {
781        let con = &self.decl.constructors[con_i];
782        let field = &con.fields[field_i];
783        let field_sort = sort_name_to_id(ctx, &field.sort);
784        crate::z3_compat::ext::FuncDecl::new(ctx, &field.name, &[self.sort_id], field_sort)
785    }
786
787    /// Return the sort ID of this datatype's sort.
788    #[must_use]
789    pub fn sort_id(&self) -> SortId {
790        self.sort_id
791    }
792
793    /// Return a reference to the underlying [`DatatypeDecl`].
794    #[must_use]
795    pub fn decl(&self) -> &DatatypeDecl {
796        &self.decl
797    }
798}
799
800/// Convenience constructor for building a [`Z3Constructor`] specification.
801#[must_use]
802pub fn mk_constructor(name: &str, fields: &[(&str, &str)]) -> Z3Constructor {
803    Z3Constructor {
804        name: name.to_string(),
805        fields: fields
806            .iter()
807            .map(|&(fname, fsort)| Z3Field {
808                name: fname.to_string(),
809                sort_name: fsort.to_string(),
810            })
811            .collect(),
812    }
813}
814
815/// Map a sort-name string to a [`SortId`] in the given context.
816///
817/// Handles `"Bool"`, `"Int"`, `"Real"` and falls back to the integer sort for
818/// anything else (the sort must be declared separately for full correctness).
819fn sort_name_to_id(ctx: &Z3Context, name: &str) -> SortId {
820    match name {
821        "Bool" => ctx.bool_sort(),
822        "Int" => ctx.int_sort(),
823        "Real" => ctx.real_sort(),
824        other => ctx.tm.borrow_mut().sorts.mk_datatype_sort(other),
825    }
826}
827
828// ─── Z3Solver: check_assumptions + unsat_core ────────────────────────────────
829
830impl Z3Solver {
831    /// Check satisfiability under a list of additional assumptions.
832    ///
833    /// The assumptions are asserted into a temporary scope (push/pop) so they
834    /// do not modify the permanent assertion stack.  The assumptions must have
835    /// been built using the same [`Z3Context`] that was passed to
836    /// [`Z3Solver::new`].
837    pub fn check_assumptions(&mut self, assumptions: &[Bool]) -> SatResult {
838        let term_ids: Vec<TermId> = assumptions.iter().map(|b| b.id).collect();
839        self.ctx.check_with_assumptions_raw(&term_ids).into()
840    }
841
842    /// Return the unsat core from the most recent `check()` or
843    /// `check_assumptions()` that returned `Unsat`.
844    ///
845    /// Returns an empty `Vec` if no core is available (e.g. the last result
846    /// was `Sat`, or unsat-core production is not enabled).
847    #[must_use]
848    pub fn unsat_core(&self) -> Vec<Bool> {
849        match self.ctx.get_unsat_core_raw() {
850            None => Vec::new(),
851            Some(core) => {
852                // The UnsatCore stores assertion *indices* into the assertion
853                // list.  We use Context::get_assertions() to map back to TermIds.
854                let assertions = self.ctx.get_assertions();
855                core.indices
856                    .iter()
857                    .filter_map(|&idx| assertions.get(idx as usize).copied())
858                    .map(|id| Bool { id })
859                    .collect()
860            }
861        }
862    }
863}
864
865// ─── Z3AstVector ─────────────────────────────────────────────────────────────
866
867/// Analogue of `z3::AstVector`.
868///
869/// An ordered collection of [`Bool`] terms associated with a context.
870/// Terms are stored by [`TermId`] and can be iterated or indexed.
871pub struct Z3AstVector {
872    terms: Vec<TermId>,
873    ctx_tm: Rc<std::cell::RefCell<TermManager>>,
874}
875
876impl Z3AstVector {
877    /// Create a new, empty vector.
878    #[must_use]
879    pub fn new(ctx: &Z3Context) -> Self {
880        Self {
881            terms: Vec::new(),
882            ctx_tm: ctx.tm.clone(),
883        }
884    }
885
886    /// Append a boolean term to the vector.
887    pub fn push(&mut self, term: &Bool) {
888        self.terms.push(term.id);
889    }
890
891    /// Number of terms in this vector.
892    #[must_use]
893    pub fn len(&self) -> usize {
894        self.terms.len()
895    }
896
897    /// Returns `true` if the vector contains no terms.
898    #[must_use]
899    pub fn is_empty(&self) -> bool {
900        self.terms.is_empty()
901    }
902
903    /// Retrieve the `i`-th term as a [`Bool`].
904    ///
905    /// # Panics
906    ///
907    /// Panics if `i >= self.len()`.
908    #[must_use]
909    pub fn get(&self, i: usize) -> Bool {
910        Bool { id: self.terms[i] }
911    }
912
913    /// Iterate over all terms as [`Bool`] values.
914    pub fn iter(&self) -> impl Iterator<Item = Bool> + '_ {
915        self.terms.iter().map(|&id| Bool { id })
916    }
917
918    /// Return `true` if any term in this vector is syntactically `true`.
919    ///
920    /// Uses the context's term manager to check the `TermKind`.
921    #[must_use]
922    pub fn any_true(&self) -> bool {
923        use oxiz_core::ast::TermKind;
924        let tm = self.ctx_tm.borrow();
925        self.terms
926            .iter()
927            .any(|&id| tm.get(id).is_some_and(|t| matches!(t.kind, TermKind::True)))
928    }
929}
930
931// ─── TermManager read-only helpers ───────────────────────────────────────────
932// The core TermManager does not yet expose a `mk_true_ro` / `mk_false_ro` that
933// takes `&self`.  We add a small compatibility shim via a local trait to avoid
934// an immutable borrow while calling methods that internally need `&mut self`
935// only for caching.
936
937trait TermManagerExt {
938    fn mk_true_ro(&self) -> TermId;
939    fn mk_false_ro(&self) -> TermId;
940}
941
942impl TermManagerExt for TermManager {
943    fn mk_true_ro(&self) -> TermId {
944        // True and False are always the first two terms in every fresh
945        // TermManager; their IDs are stable across the lifetime of any
946        // single manager instance.
947        use oxiz_core::ast::TermKind;
948        // Walk the first few entries to find True.
949        for idx in 0..8u32 {
950            let tid = TermId(idx);
951            if let Some(t) = self.get(tid) {
952                if matches!(t.kind, TermKind::True) {
953                    return tid;
954                }
955            }
956        }
957        TermId(0) // fallback — should never be reached
958    }
959
960    fn mk_false_ro(&self) -> TermId {
961        use oxiz_core::ast::TermKind;
962        for idx in 0..8u32 {
963            let tid = TermId(idx);
964            if let Some(t) = self.get(tid) {
965                if matches!(t.kind, TermKind::False) {
966                    return tid;
967                }
968            }
969        }
970        TermId(1) // fallback
971    }
972}
973
974// ─── Z3FuncInterp / Z3FuncEntry / Z3Value ────────────────────────────────────
975
976/// A model value exposed through the Z3 compat layer.
977///
978/// Wraps a string representation of the value so callers do not need to depend
979/// directly on `oxiz_core::model::Value`.
980#[derive(Debug, Clone, PartialEq, Eq)]
981pub struct Z3Value {
982    /// String representation (e.g. `"42"`, `"true"`, `"#b0011"`).
983    pub inner: String,
984}
985
986impl Z3Value {
987    /// Create a `Z3Value` from an arbitrary string representation.
988    #[must_use]
989    pub fn from_string(s: String) -> Self {
990        Self { inner: s }
991    }
992
993    /// Return the string representation of this value.
994    #[must_use]
995    pub fn as_str(&self) -> &str {
996        &self.inner
997    }
998}
999
1000impl std::fmt::Display for Z3Value {
1001    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1002        f.write_str(&self.inner)
1003    }
1004}
1005
1006/// One `(args → value)` entry in a [`Z3FuncInterp`].
1007#[derive(Debug, Clone)]
1008pub struct Z3FuncEntry {
1009    /// Argument values for this entry (one per function parameter).
1010    pub args: Vec<Z3Value>,
1011    /// The output value for this argument combination.
1012    pub value: Z3Value,
1013}
1014
1015/// Analogue of `z3::FuncInterp`.
1016///
1017/// Represents the interpretation of an uninterpreted function in a model as a
1018/// finite table of `(args → value)` entries plus an `else_value` for all other
1019/// input combinations.
1020///
1021/// Obtained via [`crate::z3_compat::Z3Model::get_func_interp`].
1022pub struct Z3FuncInterp {
1023    /// Finite explicit entries.
1024    entries: Vec<Z3FuncEntry>,
1025    /// Value returned for inputs not covered by any entry.
1026    else_value: Z3Value,
1027    /// Number of arguments of the function.
1028    arity: usize,
1029}
1030
1031impl Z3FuncInterp {
1032    /// Create a `Z3FuncInterp` from the raw data returned by the solver context.
1033    pub(crate) fn from_raw(raw: &crate::z3_compat::FuncInterpRaw) -> Self {
1034        let (raw_entries, else_str, arity) = raw;
1035        let entries = raw_entries
1036            .iter()
1037            .map(|(arg_strs, val_str)| Z3FuncEntry {
1038                args: arg_strs
1039                    .iter()
1040                    .map(|s| Z3Value::from_string(s.clone()))
1041                    .collect(),
1042                value: Z3Value::from_string(val_str.clone()),
1043            })
1044            .collect();
1045        Self {
1046            entries,
1047            else_value: Z3Value::from_string(else_str.clone()),
1048            arity: *arity,
1049        }
1050    }
1051
1052    /// Return the number of explicit `(args → value)` entries.
1053    #[must_use]
1054    pub fn num_entries(&self) -> usize {
1055        self.entries.len()
1056    }
1057
1058    /// Return the arity (number of arguments) of the interpreted function.
1059    #[must_use]
1060    pub fn arity(&self) -> usize {
1061        self.arity
1062    }
1063
1064    /// Return the `else` value applied to any input not matched by an entry.
1065    #[must_use]
1066    pub fn else_value(&self) -> &Z3Value {
1067        &self.else_value
1068    }
1069
1070    /// Return the `i`-th entry.
1071    ///
1072    /// # Panics
1073    ///
1074    /// Panics if `i >= self.num_entries()`.
1075    #[must_use]
1076    pub fn get_entry(&self, i: usize) -> &Z3FuncEntry {
1077        &self.entries[i]
1078    }
1079
1080    /// Iterate over all explicit entries.
1081    pub fn entries(&self) -> impl Iterator<Item = &Z3FuncEntry> {
1082        self.entries.iter()
1083    }
1084}
1085
1086// ─── Z3Model::get_func_interp ─────────────────────────────────────────────────
1087
1088impl crate::z3_compat::Z3Model {
1089    /// Return the full interpretation of an uninterpreted function `f` in this
1090    /// model, or `None` if `f` was not declared or is not present in the model.
1091    ///
1092    /// The returned [`Z3FuncInterp`] contains the finite set of `(args → value)`
1093    /// entries that the solver determined, plus an `else_value` for all other
1094    /// inputs.
1095    ///
1096    /// # Stub note
1097    ///
1098    /// When the EUF e-graph does not contain any application of `f` (e.g. the
1099    /// function was declared but never constrained), `num_entries()` will be 0
1100    /// and `else_value()` will be the default value for the return sort.  This
1101    /// is a valid (conservative) interpretation: the solver is free to choose
1102    /// any value for unconstrained applications.
1103    #[must_use]
1104    pub fn get_func_interp(&self, f: &crate::z3_compat::ext::FuncDecl) -> Option<Z3FuncInterp> {
1105        self.func_interp_raw(&f.name).map(Z3FuncInterp::from_raw)
1106    }
1107}
1108
1109// ─── Re-export convenience items ─────────────────────────────────────────────
1110
1111/// Re-export `DatatypeDecl`, `Constructor`, `Selector`, and `Field` from the
1112/// theories crate so downstream code can use them without a direct dep on
1113/// `oxiz-theories`.
1114pub use oxiz_theories::datatype::{
1115    Constructor as DtConstructor, DatatypeDecl as DtDecl, DatatypeSort as DtSort, Field as DtField,
1116    Selector as DtSelector,
1117};