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 ¶ms.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(¤t) {
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};