Skip to main content

oxiz_solver/
skolemization.rs

1//! Skolemization: transforms existential quantifiers by replacing
2//! existentially quantified variables with Skolem constants or functions.
3//!
4//! For `exists x. phi(x)` with no outer universals: replace x with a fresh constant c,
5//! yielding `phi(c)`.
6//!
7//! For `forall y. exists x. phi(x, y)` with outer universals: replace x with `f(y)`
8//! where f is a fresh function symbol (Skolem function).
9//!
10//! This module performs NNF conversion first, then Skolemization, ensuring that
11//! quantifier polarities are correctly determined before replacement.
12
13use oxiz_core::ast::TermManager;
14use oxiz_core::ast::{TermId, TermKind};
15use oxiz_core::sort::SortId;
16use std::collections::HashMap;
17use std::fmt;
18
19/// Errors that can occur during Skolemization
20#[derive(Debug, Clone)]
21pub enum SkolemizationError {
22    /// A term ID could not be resolved in the TermManager
23    UnknownTerm(TermId),
24    /// Sort information could not be retrieved
25    UnknownSort(SortId),
26    /// The Skolem counter overflowed (extremely unlikely)
27    CounterOverflow,
28    /// Internal error during term construction
29    TermConstructionFailed(String),
30}
31
32impl fmt::Display for SkolemizationError {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            SkolemizationError::UnknownTerm(id) => {
36                write!(f, "unknown term with id {}", id.raw())
37            }
38            SkolemizationError::UnknownSort(id) => {
39                write!(f, "unknown sort with id {}", id.0)
40            }
41            SkolemizationError::CounterOverflow => {
42                write!(f, "Skolem counter overflow")
43            }
44            SkolemizationError::TermConstructionFailed(msg) => {
45                write!(f, "term construction failed: {}", msg)
46            }
47        }
48    }
49}
50
51impl std::error::Error for SkolemizationError {}
52
53/// Represents a generated Skolem symbol (constant or function)
54#[derive(Debug, Clone)]
55pub struct SkolemSymbol {
56    /// The generated name (e.g., "sk!0", "skf!1")
57    pub name: String,
58    /// The result sort of the Skolem constant or function
59    pub sort: SortId,
60    /// The term created for this Skolem symbol
61    pub term: TermId,
62    /// For Skolem functions, the sorts of the arguments (universal variables).
63    /// Empty for Skolem constants.
64    pub arg_sorts: Vec<SortId>,
65}
66
67/// Context that tracks state during Skolemization.
68///
69/// Maintains the stack of outer universal variables, the mapping from
70/// existential variables to their Skolem replacements, and a counter
71/// for generating unique Skolem names.
72#[derive(Debug)]
73pub struct SkolemizationContext {
74    /// Stack of outer universal variables: (sort, term_id) pairs.
75    /// When we enter a Forall scope, the bound variables are pushed here;
76    /// when we leave, they are popped.
77    outer_universals: Vec<(SortId, TermId)>,
78    /// Map from original existential variable TermId to its Skolem replacement TermId.
79    skolem_map: HashMap<TermId, TermId>,
80    /// Counter for generating unique Skolem names
81    skolem_counter: u64,
82    /// All generated Skolem symbols, for inspection/tracking
83    skolem_symbols: Vec<SkolemSymbol>,
84    /// Cache for NNF conversion: (term_id, negated) -> result_id
85    nnf_cache: HashMap<(TermId, bool), TermId>,
86    /// Cache for skolemize_inner to avoid reprocessing subterms
87    skolem_cache: HashMap<TermId, TermId>,
88}
89
90impl Default for SkolemizationContext {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl SkolemizationContext {
97    /// Create a new Skolemization context
98    pub fn new() -> Self {
99        SkolemizationContext {
100            outer_universals: Vec::new(),
101            skolem_map: HashMap::new(),
102            skolem_counter: 0,
103            skolem_symbols: Vec::new(),
104            nnf_cache: HashMap::new(),
105            skolem_cache: HashMap::new(),
106        }
107    }
108
109    /// Get the list of generated Skolem symbols
110    pub fn skolem_symbols(&self) -> &[SkolemSymbol] {
111        &self.skolem_symbols
112    }
113
114    /// Get the number of Skolem symbols generated so far
115    pub fn skolem_count(&self) -> u64 {
116        self.skolem_counter
117    }
118
119    /// Main entry point: Skolemize a term, returning the transformed term.
120    ///
121    /// This performs NNF conversion first (to ensure quantifier polarities are
122    /// correctly determined), then Skolemization.
123    ///
124    /// # Errors
125    ///
126    /// Returns `SkolemizationError` if terms cannot be looked up, sorts are
127    /// missing, or the Skolem counter overflows.
128    pub fn skolemize(
129        &mut self,
130        tm: &mut TermManager,
131        term: TermId,
132    ) -> Result<TermId, SkolemizationError> {
133        // 1. Convert to NNF (push negations inward so quantifier polarities are clear)
134        let nnf = self.convert_nnf(tm, term, false)?;
135        // 2. Skolemize the NNF term
136        self.skolemize_inner(tm, nnf)
137    }
138
139    /// Convert to Negation Normal Form.
140    ///
141    /// When `negated` is true, we are converting under a negation context, which
142    /// flips AND/OR and swaps Forall/Exists.
143    fn convert_nnf(
144        &mut self,
145        tm: &mut TermManager,
146        term: TermId,
147        negated: bool,
148    ) -> Result<TermId, SkolemizationError> {
149        // Check cache
150        if let Some(&cached) = self.nnf_cache.get(&(term, negated)) {
151            return Ok(cached);
152        }
153
154        let t = tm.get(term).ok_or(SkolemizationError::UnknownTerm(term))?;
155        let kind = t.kind.clone();
156
157        let result = match kind {
158            TermKind::True => {
159                if negated {
160                    Ok(tm.mk_false())
161                } else {
162                    Ok(tm.mk_true())
163                }
164            }
165            TermKind::False => {
166                if negated {
167                    Ok(tm.mk_true())
168                } else {
169                    Ok(tm.mk_false())
170                }
171            }
172            TermKind::Var(_)
173            | TermKind::IntConst(_)
174            | TermKind::RealConst(_)
175            | TermKind::BitVecConst { .. }
176            | TermKind::StringLit(_) => {
177                if negated {
178                    Ok(tm.mk_not(term))
179                } else {
180                    Ok(term)
181                }
182            }
183            TermKind::Not(arg) => {
184                // Double negation elimination: push negation through
185                self.convert_nnf(tm, arg, !negated)
186            }
187            TermKind::And(args) => {
188                let mut nnf_args = Vec::with_capacity(args.len());
189                for &a in &args {
190                    nnf_args.push(self.convert_nnf(tm, a, negated)?);
191                }
192                if negated {
193                    // De Morgan: NOT(a AND b) = (NOT a) OR (NOT b)
194                    Ok(tm.mk_or(nnf_args))
195                } else {
196                    Ok(tm.mk_and(nnf_args))
197                }
198            }
199            TermKind::Or(args) => {
200                let mut nnf_args = Vec::with_capacity(args.len());
201                for &a in &args {
202                    nnf_args.push(self.convert_nnf(tm, a, negated)?);
203                }
204                if negated {
205                    // De Morgan: NOT(a OR b) = (NOT a) AND (NOT b)
206                    Ok(tm.mk_and(nnf_args))
207                } else {
208                    Ok(tm.mk_or(nnf_args))
209                }
210            }
211            TermKind::Implies(lhs, rhs) => {
212                // (a -> b) = (NOT a) OR b
213                // NOT(a -> b) = a AND (NOT b)
214                let lhs_nnf = self.convert_nnf(tm, lhs, !negated)?;
215                let rhs_nnf = self.convert_nnf(tm, rhs, negated)?;
216                if negated {
217                    Ok(tm.mk_and([lhs_nnf, rhs_nnf]))
218                } else {
219                    Ok(tm.mk_or([lhs_nnf, rhs_nnf]))
220                }
221            }
222            TermKind::Xor(lhs, rhs) => {
223                // a XOR b = (a OR b) AND (NOT a OR NOT b)
224                let a = self.convert_nnf(tm, lhs, false)?;
225                let b = self.convert_nnf(tm, rhs, false)?;
226                let not_a = self.convert_nnf(tm, lhs, true)?;
227                let not_b = self.convert_nnf(tm, rhs, true)?;
228                let clause1 = tm.mk_or([a, b]);
229                let clause2 = tm.mk_or([not_a, not_b]);
230                let xor_nnf = tm.mk_and([clause1, clause2]);
231                if negated {
232                    // NOT(a XOR b) = (a AND b) OR (NOT a AND NOT b)
233                    // Re-derive under negation to keep NNF:
234                    // Simply negate the XOR NNF
235                    self.convert_nnf(tm, xor_nnf, true)
236                } else {
237                    Ok(xor_nnf)
238                }
239            }
240            TermKind::Forall {
241                vars,
242                body,
243                patterns,
244            } => {
245                let body_nnf = self.convert_nnf(tm, body, negated)?;
246
247                // Resolve variable names first to avoid borrowing issues
248                let var_names: Vec<(String, SortId)> = vars
249                    .iter()
250                    .map(|(s, sort)| (tm.resolve_str(*s).to_string(), *sort))
251                    .collect();
252
253                if negated {
254                    // NOT(forall x. P(x)) = exists x. NOT P(x)
255                    Ok(tm.mk_exists_with_patterns(
256                        var_names
257                            .iter()
258                            .map(|(s, sort): &(String, SortId)| (s.as_str(), *sort)),
259                        body_nnf,
260                        patterns,
261                    ))
262                } else {
263                    Ok(tm.mk_forall_with_patterns(
264                        var_names
265                            .iter()
266                            .map(|(s, sort): &(String, SortId)| (s.as_str(), *sort)),
267                        body_nnf,
268                        patterns,
269                    ))
270                }
271            }
272            TermKind::Exists {
273                vars,
274                body,
275                patterns,
276            } => {
277                let body_nnf = self.convert_nnf(tm, body, negated)?;
278
279                // Resolve variable names first to avoid borrowing issues
280                let var_names: Vec<(String, SortId)> = vars
281                    .iter()
282                    .map(|(s, sort)| (tm.resolve_str(*s).to_string(), *sort))
283                    .collect();
284
285                if negated {
286                    // NOT(exists x. P(x)) = forall x. NOT P(x)
287                    Ok(tm.mk_forall_with_patterns(
288                        var_names
289                            .iter()
290                            .map(|(s, sort): &(String, SortId)| (s.as_str(), *sort)),
291                        body_nnf,
292                        patterns,
293                    ))
294                } else {
295                    Ok(tm.mk_exists_with_patterns(
296                        var_names
297                            .iter()
298                            .map(|(s, sort): &(String, SortId)| (s.as_str(), *sort)),
299                        body_nnf,
300                        patterns,
301                    ))
302                }
303            }
304            TermKind::Eq(_, _)
305            | TermKind::Distinct(_)
306            | TermKind::Lt(_, _)
307            | TermKind::Le(_, _)
308            | TermKind::Gt(_, _)
309            | TermKind::Ge(_, _)
310            | TermKind::Apply { .. }
311            | TermKind::Ite(_, _, _) => {
312                // Atoms and other non-boolean-connective terms: just negate if needed
313                if negated {
314                    Ok(tm.mk_not(term))
315                } else {
316                    Ok(term)
317                }
318            }
319            // All remaining term kinds (arithmetic, bitvec, string ops, FP, etc.)
320            // are treated as atoms in the boolean sense
321            _ => {
322                if negated {
323                    Ok(tm.mk_not(term))
324                } else {
325                    Ok(term)
326                }
327            }
328        }?;
329
330        self.nnf_cache.insert((term, negated), result);
331        Ok(result)
332    }
333
334    /// Inner recursive Skolemization.
335    ///
336    /// Traverses the NNF term tree:
337    /// - Forall: pushes bound variables onto `outer_universals`, recurses on body, pops
338    /// - Exists: for each bound variable, creates a Skolem constant or function,
339    ///   adds mapping to `skolem_map`, recurses on body
340    /// - Variable: if in `skolem_map`, returns the replacement; otherwise returns as-is
341    /// - Other: recurses on children
342    fn skolemize_inner(
343        &mut self,
344        tm: &mut TermManager,
345        term: TermId,
346    ) -> Result<TermId, SkolemizationError> {
347        // Check cache
348        if let Some(&cached) = self.skolem_cache.get(&term) {
349            return Ok(cached);
350        }
351
352        let t = tm.get(term).ok_or(SkolemizationError::UnknownTerm(term))?;
353        let kind = t.kind.clone();
354
355        let result = match kind {
356            // Base cases: constants are unchanged
357            TermKind::True
358            | TermKind::False
359            | TermKind::IntConst(_)
360            | TermKind::RealConst(_)
361            | TermKind::BitVecConst { .. }
362            | TermKind::StringLit(_) => Ok(term),
363
364            // Variables: check if this var has a Skolem replacement
365            TermKind::Var(_) => {
366                if let Some(&replacement) = self.skolem_map.get(&term) {
367                    Ok(replacement)
368                } else {
369                    Ok(term)
370                }
371            }
372
373            // Universal quantifier: push vars onto outer_universals, recurse, pop
374            TermKind::Forall {
375                vars,
376                body,
377                patterns: _,
378            } => {
379                // Record how many universals we are pushing so we can pop exactly that many
380                let push_count = vars.len();
381
382                // Push each bound variable onto the outer_universals stack.
383                // We need the actual TermIds for these variables so that Skolem functions
384                // can reference them as arguments.
385                for (name_spur, sort) in &vars {
386                    let var_name = tm.resolve_str(*name_spur).to_string();
387                    let var_id = tm.mk_var(&var_name, *sort);
388                    self.outer_universals.push((*sort, var_id));
389                }
390
391                // Recurse on the body
392                let sk_body = self.skolemize_inner(tm, body)?;
393
394                // Pop the universal variables we pushed
395                for _ in 0..push_count {
396                    self.outer_universals.pop();
397                }
398
399                // Reconstruct the Forall with the Skolemized body (patterns are dropped
400                // since Skolemization may have changed the variable structure)
401                let var_names: Vec<(String, SortId)> = vars
402                    .iter()
403                    .map(|(s, sort)| (tm.resolve_str(*s).to_string(), *sort))
404                    .collect();
405
406                Ok(tm.mk_forall(
407                    var_names
408                        .iter()
409                        .map(|(s, sort): &(String, SortId)| (s.as_str(), *sort)),
410                    sk_body,
411                ))
412            }
413
414            // Existential quantifier: create Skolem constants/functions for each bound var
415            TermKind::Exists { vars, body, .. } => {
416                // For each existentially quantified variable, create a Skolem replacement
417                for (name_spur, sort) in &vars {
418                    let var_name = tm.resolve_str(*name_spur).to_string();
419                    let var_id = tm.mk_var(&var_name, *sort);
420
421                    let skolem_term = if self.outer_universals.is_empty() {
422                        // No outer universal variables: create a Skolem constant
423                        self.mk_skolem_constant(tm, *sort)?
424                    } else {
425                        // There are outer universal variables: create a Skolem function
426                        // applied to the outer universal variables
427                        self.mk_skolem_function(tm, *sort)?
428                    };
429
430                    self.skolem_map.insert(var_id, skolem_term);
431                }
432
433                // Recurse on the body (the existential quantifier is eliminated)
434                self.skolemize_inner(tm, body)
435            }
436
437            // Boolean connectives: recurse on children
438            TermKind::Not(arg) => {
439                let sk_arg = self.skolemize_inner(tm, arg)?;
440                Ok(tm.mk_not(sk_arg))
441            }
442            TermKind::And(args) => {
443                let mut sk_args = Vec::with_capacity(args.len());
444                for &a in &args {
445                    sk_args.push(self.skolemize_inner(tm, a)?);
446                }
447                Ok(tm.mk_and(sk_args))
448            }
449            TermKind::Or(args) => {
450                let mut sk_args = Vec::with_capacity(args.len());
451                for &a in &args {
452                    sk_args.push(self.skolemize_inner(tm, a)?);
453                }
454                Ok(tm.mk_or(sk_args))
455            }
456            TermKind::Implies(lhs, rhs) => {
457                let sk_lhs = self.skolemize_inner(tm, lhs)?;
458                let sk_rhs = self.skolemize_inner(tm, rhs)?;
459                Ok(tm.mk_implies(sk_lhs, sk_rhs))
460            }
461            TermKind::Xor(lhs, rhs) => {
462                let sk_lhs = self.skolemize_inner(tm, lhs)?;
463                let sk_rhs = self.skolemize_inner(tm, rhs)?;
464                Ok(tm.mk_xor(sk_lhs, sk_rhs))
465            }
466            TermKind::Ite(cond, then_br, else_br) => {
467                let sk_cond = self.skolemize_inner(tm, cond)?;
468                let sk_then = self.skolemize_inner(tm, then_br)?;
469                let sk_else = self.skolemize_inner(tm, else_br)?;
470                Ok(tm.mk_ite(sk_cond, sk_then, sk_else))
471            }
472
473            // Equality and comparison: recurse on children
474            TermKind::Eq(lhs, rhs) => {
475                let sk_lhs = self.skolemize_inner(tm, lhs)?;
476                let sk_rhs = self.skolemize_inner(tm, rhs)?;
477                Ok(tm.mk_eq(sk_lhs, sk_rhs))
478            }
479            TermKind::Distinct(args) => {
480                let mut sk_args = Vec::with_capacity(args.len());
481                for &a in &args {
482                    sk_args.push(self.skolemize_inner(tm, a)?);
483                }
484                Ok(tm.mk_distinct(sk_args))
485            }
486            TermKind::Lt(lhs, rhs) => {
487                let sk_lhs = self.skolemize_inner(tm, lhs)?;
488                let sk_rhs = self.skolemize_inner(tm, rhs)?;
489                Ok(tm.mk_lt(sk_lhs, sk_rhs))
490            }
491            TermKind::Le(lhs, rhs) => {
492                let sk_lhs = self.skolemize_inner(tm, lhs)?;
493                let sk_rhs = self.skolemize_inner(tm, rhs)?;
494                Ok(tm.mk_le(sk_lhs, sk_rhs))
495            }
496            TermKind::Gt(lhs, rhs) => {
497                let sk_lhs = self.skolemize_inner(tm, lhs)?;
498                let sk_rhs = self.skolemize_inner(tm, rhs)?;
499                Ok(tm.mk_gt(sk_lhs, sk_rhs))
500            }
501            TermKind::Ge(lhs, rhs) => {
502                let sk_lhs = self.skolemize_inner(tm, lhs)?;
503                let sk_rhs = self.skolemize_inner(tm, rhs)?;
504                Ok(tm.mk_ge(sk_lhs, sk_rhs))
505            }
506
507            // Arithmetic: recurse on children
508            TermKind::Neg(arg) => {
509                let sk_arg = self.skolemize_inner(tm, arg)?;
510                Ok(tm.mk_neg(sk_arg))
511            }
512            TermKind::Add(args) => {
513                let mut sk_args = Vec::with_capacity(args.len());
514                for &a in &args {
515                    sk_args.push(self.skolemize_inner(tm, a)?);
516                }
517                Ok(tm.mk_add(sk_args))
518            }
519            TermKind::Sub(lhs, rhs) => {
520                let sk_lhs = self.skolemize_inner(tm, lhs)?;
521                let sk_rhs = self.skolemize_inner(tm, rhs)?;
522                Ok(tm.mk_sub(sk_lhs, sk_rhs))
523            }
524            TermKind::Mul(args) => {
525                let mut sk_args = Vec::with_capacity(args.len());
526                for &a in &args {
527                    sk_args.push(self.skolemize_inner(tm, a)?);
528                }
529                Ok(tm.mk_mul(sk_args))
530            }
531            TermKind::Div(lhs, rhs) => {
532                let sk_lhs = self.skolemize_inner(tm, lhs)?;
533                let sk_rhs = self.skolemize_inner(tm, rhs)?;
534                Ok(tm.mk_div(sk_lhs, sk_rhs))
535            }
536            TermKind::Mod(lhs, rhs) => {
537                let sk_lhs = self.skolemize_inner(tm, lhs)?;
538                let sk_rhs = self.skolemize_inner(tm, rhs)?;
539                Ok(tm.mk_mod(sk_lhs, sk_rhs))
540            }
541
542            // Uninterpreted function application: recurse on arguments
543            TermKind::Apply { func, args } => {
544                let mut sk_args = Vec::with_capacity(args.len());
545                for &a in &args {
546                    sk_args.push(self.skolemize_inner(tm, a)?);
547                }
548                let func_name = tm.resolve_str(func).to_string();
549                let result_sort = tm
550                    .get(term)
551                    .ok_or(SkolemizationError::UnknownTerm(term))?
552                    .sort;
553                Ok(tm.mk_apply(&func_name, sk_args, result_sort))
554            }
555
556            // Array operations
557            TermKind::Select(arr, idx) => {
558                let sk_arr = self.skolemize_inner(tm, arr)?;
559                let sk_idx = self.skolemize_inner(tm, idx)?;
560                Ok(tm.mk_select(sk_arr, sk_idx))
561            }
562            TermKind::Store(arr, idx, val) => {
563                let sk_arr = self.skolemize_inner(tm, arr)?;
564                let sk_idx = self.skolemize_inner(tm, idx)?;
565                let sk_val = self.skolemize_inner(tm, val)?;
566                Ok(tm.mk_store(sk_arr, sk_idx, sk_val))
567            }
568
569            // Let bindings: recurse on bindings and body
570            TermKind::Let { bindings, body } => {
571                let mut sk_bindings = Vec::with_capacity(bindings.len());
572                for (name, val) in &bindings {
573                    let sk_val = self.skolemize_inner(tm, *val)?;
574                    let name_str = tm.resolve_str(*name).to_string();
575                    sk_bindings.push((name_str, sk_val));
576                }
577                let sk_body = self.skolemize_inner(tm, body)?;
578                Ok(tm.mk_let(
579                    sk_bindings
580                        .iter()
581                        .map(|(n, v): &(String, TermId)| (n.as_str(), *v)),
582                    sk_body,
583                ))
584            }
585
586            // For all other term kinds (BV ops, FP ops, String ops, DT ops, Match, etc.),
587            // we treat them as opaque and return them as-is. These do not contain
588            // quantifier-relevant substructure at the boolean level. If deeper
589            // Skolemization is needed in theory-specific terms, the theory solver
590            // should handle it.
591            _ => Ok(term),
592        }?;
593
594        self.skolem_cache.insert(term, result);
595        Ok(result)
596    }
597
598    /// Create a Skolem constant (no outer universals).
599    ///
600    /// Generates a fresh uninterpreted constant with the name `sk!N` where N
601    /// is the current counter value.
602    fn mk_skolem_constant(
603        &mut self,
604        tm: &mut TermManager,
605        sort: SortId,
606    ) -> Result<TermId, SkolemizationError> {
607        let counter = self.skolem_counter;
608        self.skolem_counter = self
609            .skolem_counter
610            .checked_add(1)
611            .ok_or(SkolemizationError::CounterOverflow)?;
612
613        let name = format!("sk!{}", counter);
614        let term = tm.mk_var(&name, sort);
615
616        self.skolem_symbols.push(SkolemSymbol {
617            name,
618            sort,
619            term,
620            arg_sorts: Vec::new(),
621        });
622
623        Ok(term)
624    }
625
626    /// Create a Skolem function applied to outer universals.
627    ///
628    /// Generates a fresh uninterpreted function `skf!N` and returns the
629    /// application `skf!N(y1, y2, ...)` where y1, y2, ... are the current
630    /// outer universal variables.
631    fn mk_skolem_function(
632        &mut self,
633        tm: &mut TermManager,
634        result_sort: SortId,
635    ) -> Result<TermId, SkolemizationError> {
636        let counter = self.skolem_counter;
637        self.skolem_counter = self
638            .skolem_counter
639            .checked_add(1)
640            .ok_or(SkolemizationError::CounterOverflow)?;
641
642        let name = format!("skf!{}", counter);
643
644        // Collect the argument sorts and term IDs from outer universals
645        let arg_sorts: Vec<SortId> = self.outer_universals.iter().map(|(s, _)| *s).collect();
646        let arg_terms: Vec<TermId> = self.outer_universals.iter().map(|(_, t)| *t).collect();
647
648        // Create a function application term: skf!N(y1, y2, ...)
649        let term = tm.mk_apply(&name, arg_terms, result_sort);
650
651        self.skolem_symbols.push(SkolemSymbol {
652            name,
653            sort: result_sort,
654            term,
655            arg_sorts,
656        });
657
658        Ok(term)
659    }
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665    use num_bigint::BigInt;
666
667    /// Helper: check that a term does not contain any Exists quantifiers
668    fn is_existential_free(tm: &TermManager, term: TermId) -> bool {
669        let Some(t) = tm.get(term) else {
670            return true;
671        };
672        match &t.kind {
673            TermKind::Exists { .. } => false,
674            TermKind::Not(arg) => is_existential_free(tm, *arg),
675            TermKind::And(args) | TermKind::Or(args) => {
676                args.iter().all(|&a| is_existential_free(tm, a))
677            }
678            TermKind::Implies(lhs, rhs) => {
679                is_existential_free(tm, *lhs) && is_existential_free(tm, *rhs)
680            }
681            TermKind::Forall { body, .. } => is_existential_free(tm, *body),
682            _ => true,
683        }
684    }
685
686    #[test]
687    fn test_skolemize_simple_exists() {
688        // exists x : Bool. x
689        // Should become: sk!0
690        let mut tm = TermManager::new();
691        let bool_sort = tm.sorts.bool_sort;
692        let x = tm.mk_var("x", bool_sort);
693        let exists = tm.mk_exists([("x", bool_sort)], x);
694
695        let mut ctx = SkolemizationContext::new();
696        let result = ctx.skolemize(&mut tm, exists);
697        assert!(result.is_ok());
698        let result_id = result.expect("skolemize should succeed");
699
700        // The result should be existential-free
701        assert!(is_existential_free(&tm, result_id));
702
703        // Should have generated one Skolem symbol
704        assert_eq!(ctx.skolem_count(), 1);
705        let sym = &ctx.skolem_symbols()[0];
706        assert_eq!(sym.name, "sk!0");
707        assert!(sym.arg_sorts.is_empty());
708    }
709
710    #[test]
711    fn test_skolemize_exists_with_body() {
712        // exists x : Int. x > 0
713        // Should become: sk!0 > 0
714        let mut tm = TermManager::new();
715        let int_sort = tm.sorts.int_sort;
716        let x = tm.mk_var("x", int_sort);
717        let zero = tm.mk_int(BigInt::from(0));
718        let gt = tm.mk_gt(x, zero);
719        let exists = tm.mk_exists([("x", int_sort)], gt);
720
721        let mut ctx = SkolemizationContext::new();
722        let result = ctx.skolemize(&mut tm, exists);
723        assert!(result.is_ok());
724        let result_id = result.expect("skolemize should succeed");
725
726        assert!(is_existential_free(&tm, result_id));
727        assert_eq!(ctx.skolem_count(), 1);
728    }
729
730    #[test]
731    fn test_skolemize_forall_exists() {
732        // forall y : Int. exists x : Int. x > y
733        // Should become: forall y : Int. skf!0(y) > y
734        let mut tm = TermManager::new();
735        let int_sort = tm.sorts.int_sort;
736        let x = tm.mk_var("x", int_sort);
737        let y = tm.mk_var("y", int_sort);
738        let gt = tm.mk_gt(x, y);
739        let exists = tm.mk_exists([("x", int_sort)], gt);
740        let forall = tm.mk_forall([("y", int_sort)], exists);
741
742        let mut ctx = SkolemizationContext::new();
743        let result = ctx.skolemize(&mut tm, forall);
744        assert!(result.is_ok());
745        let result_id = result.expect("skolemize should succeed");
746
747        assert!(is_existential_free(&tm, result_id));
748
749        // Should have generated one Skolem function
750        assert_eq!(ctx.skolem_count(), 1);
751        let sym = &ctx.skolem_symbols()[0];
752        assert_eq!(sym.name, "skf!0");
753        assert_eq!(sym.arg_sorts.len(), 1);
754        assert_eq!(sym.arg_sorts[0], int_sort);
755    }
756
757    #[test]
758    fn test_skolemize_nested_exists() {
759        // exists x : Int. exists y : Int. x > y
760        // Should become: sk!0 > sk!1
761        let mut tm = TermManager::new();
762        let int_sort = tm.sorts.int_sort;
763        let x = tm.mk_var("x", int_sort);
764        let y = tm.mk_var("y", int_sort);
765        let gt = tm.mk_gt(x, y);
766        let exists_y = tm.mk_exists([("y", int_sort)], gt);
767        let exists_x = tm.mk_exists([("x", int_sort)], exists_y);
768
769        let mut ctx = SkolemizationContext::new();
770        let result = ctx.skolemize(&mut tm, exists_x);
771        assert!(result.is_ok());
772        let result_id = result.expect("skolemize should succeed");
773
774        assert!(is_existential_free(&tm, result_id));
775        assert_eq!(ctx.skolem_count(), 2);
776        // Both should be constants (no outer universals)
777        assert!(ctx.skolem_symbols()[0].arg_sorts.is_empty());
778        assert!(ctx.skolem_symbols()[1].arg_sorts.is_empty());
779    }
780
781    #[test]
782    fn test_skolemize_negated_forall() {
783        // NOT(forall x : Bool. x) should become, after NNF:
784        // exists x : Bool. NOT x
785        // Then Skolemized to: NOT sk!0
786        let mut tm = TermManager::new();
787        let bool_sort = tm.sorts.bool_sort;
788        let x = tm.mk_var("x", bool_sort);
789        let forall = tm.mk_forall([("x", bool_sort)], x);
790        let neg_forall = tm.mk_not(forall);
791
792        let mut ctx = SkolemizationContext::new();
793        let result = ctx.skolemize(&mut tm, neg_forall);
794        assert!(result.is_ok());
795        let result_id = result.expect("skolemize should succeed");
796
797        assert!(is_existential_free(&tm, result_id));
798        assert_eq!(ctx.skolem_count(), 1);
799    }
800
801    #[test]
802    fn test_skolemize_multiple_universal_vars() {
803        // forall y : Int, z : Int. exists x : Int. x > y + z
804        // Should generate skf!0(y, z) with two argument sorts
805        let mut tm = TermManager::new();
806        let int_sort = tm.sorts.int_sort;
807        let x = tm.mk_var("x", int_sort);
808        let y = tm.mk_var("y", int_sort);
809        let z = tm.mk_var("z", int_sort);
810        let sum = tm.mk_add([y, z]);
811        let gt = tm.mk_gt(x, sum);
812        let exists = tm.mk_exists([("x", int_sort)], gt);
813        let forall = tm.mk_forall([("y", int_sort), ("z", int_sort)], exists);
814
815        let mut ctx = SkolemizationContext::new();
816        let result = ctx.skolemize(&mut tm, forall);
817        assert!(result.is_ok());
818        let result_id = result.expect("skolemize should succeed");
819
820        assert!(is_existential_free(&tm, result_id));
821        assert_eq!(ctx.skolem_count(), 1);
822        let sym = &ctx.skolem_symbols()[0];
823        assert_eq!(sym.name, "skf!0");
824        assert_eq!(sym.arg_sorts.len(), 2);
825        assert_eq!(sym.arg_sorts[0], int_sort);
826        assert_eq!(sym.arg_sorts[1], int_sort);
827    }
828
829    #[test]
830    fn test_skolemize_preserves_ground_terms() {
831        // A term with no quantifiers should be unchanged
832        let mut tm = TermManager::new();
833        let bool_sort = tm.sorts.bool_sort;
834        let p = tm.mk_var("p", bool_sort);
835        let q = tm.mk_var("q", bool_sort);
836        let and = tm.mk_and([p, q]);
837
838        let mut ctx = SkolemizationContext::new();
839        let result = ctx.skolemize(&mut tm, and);
840        assert!(result.is_ok());
841        let result_id = result.expect("skolemize should succeed");
842
843        // No Skolem symbols should be generated
844        assert_eq!(ctx.skolem_count(), 0);
845        // The result should be the same term
846        assert_eq!(result_id, and);
847    }
848
849    #[test]
850    fn test_skolemize_mixed_sorts() {
851        // forall y : Int. exists x : Bool. x AND (y > 0)
852        // The Skolem function should have Int argument sort and Bool result sort
853        let mut tm = TermManager::new();
854        let int_sort = tm.sorts.int_sort;
855        let bool_sort = tm.sorts.bool_sort;
856        let x = tm.mk_var("x", bool_sort);
857        let y = tm.mk_var("y", int_sort);
858        let zero = tm.mk_int(BigInt::from(0));
859        let gt = tm.mk_gt(y, zero);
860        let and = tm.mk_and([x, gt]);
861        let exists = tm.mk_exists([("x", bool_sort)], and);
862        let forall = tm.mk_forall([("y", int_sort)], exists);
863
864        let mut ctx = SkolemizationContext::new();
865        let result = ctx.skolemize(&mut tm, forall);
866        assert!(result.is_ok());
867        let result_id = result.expect("skolemize should succeed");
868
869        assert!(is_existential_free(&tm, result_id));
870        assert_eq!(ctx.skolem_count(), 1);
871        let sym = &ctx.skolem_symbols()[0];
872        assert_eq!(sym.sort, bool_sort);
873        assert_eq!(sym.arg_sorts.len(), 1);
874        assert_eq!(sym.arg_sorts[0], int_sort);
875    }
876
877    #[test]
878    fn test_nnf_conversion_via_skolemize() {
879        // NOT(p AND q) should be converted to (NOT p) OR (NOT q) before Skolemization
880        // (though no quantifiers present, the NNF step still runs)
881        let mut tm = TermManager::new();
882        let bool_sort = tm.sorts.bool_sort;
883        let p = tm.mk_var("p", bool_sort);
884        let q = tm.mk_var("q", bool_sort);
885        let and = tm.mk_and([p, q]);
886        let neg = tm.mk_not(and);
887
888        let mut ctx = SkolemizationContext::new();
889        let result = ctx.skolemize(&mut tm, neg);
890        assert!(result.is_ok());
891        let result_id = result.expect("skolemize should succeed");
892
893        // The result should be an OR (due to De Morgan)
894        let t = tm.get(result_id);
895        assert!(t.is_some());
896        assert!(matches!(t.map(|t| &t.kind), Some(TermKind::Or(_))));
897    }
898
899    #[test]
900    fn test_skolemize_error_on_unknown_term() {
901        let mut tm = TermManager::new();
902        let bogus = TermId::new(999_999);
903
904        let mut ctx = SkolemizationContext::new();
905        let result = ctx.skolemize(&mut tm, bogus);
906        assert!(result.is_err());
907    }
908}