You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Quantifier (forall/exists) binder sorts are never declared in SMT-LIB, so any spec quantifying over a datatype/tuple-sorted variable emits an undefined sort #142
collect_sorts (src/chc/format_context.rs:209-225) gathers the sorts that need datatype declarations in the emitted SMT-LIB only from (a) pred_vars signatures, (b) clause.vars, and (c) the argument terms of atoms (via atom_sorts/term_sorts). It never visits the binder sorts of Formula::Exists / Formula::Forall. Compounding this, term_sorts ignores the sort carried by a quantified variable term:
As a result, when a forall/exists quantifies over a variable whose sort is a datatype, tuple, or any other non-builtin/composite sort that does not otherwise appear in the same clause, that sort is never collected, so no declare-datatype is emitted for it. The generated SMT then references an undeclared sort inside the quantifier, and any SMT solver rejects the query as ill-typed.
This is an incompleteness bug, not a soundness hole: the malformed SMT is rejected at parse time (before solving), surfaced as a generic verification error, so safe programs are falsely rejected rather than wrongly accepted. Because it fails during SMT parsing, it is independent of which CHC backend is used — the PCSat solver normally required for quantified specs would reject it just the same.
Reproduction
repro.rs — a postcondition that quantifies over an enum-sorted variable:
//@compile-flags: -C debug-assertions=offuse thrust_models::exists;enumE{A(i32),B,}#[thrust_macros::ensures(exists(|e:E| result >= 0))]fnf() -> i32{0}fnmain(){}
The body need not even reference the bound variable — and referencing it does not help, because term_sorts drops the sort of a FormulaQuantifiedVar (line 60 above) and DatatypeCtor only recurses into its arguments without adding its own datatype sort.
Also reproduces for tuple-sorted binders
use thrust_models::exists;#[thrust_macros::ensures(exists(|p:(i32,i32)| result >= 0))]fnf() -> i32{0}fnmain(){}
with the emitted assert containing (exists ((p Tuple<Int-Int>)) ...) and zero declare-datatype.
Control: the sort is declared when it appears outside the quantifier
Adding a parameter of the same sort makes E reach clause.vars, so it is collected and declared, and the unknown sort error disappears:
use thrust_models::exists;enumE{A(i32),B}#[thrust_macros::ensures(exists(|e:E| result >= 0))]fnf(used:E) -> i32{0}// `used: E` => E now appears as a clause varfnmain(){}
This isolates the defect to quantifier-binder-only sorts.
Root cause
src/chc/format_context.rs:
fncollect_sorts(system:&chc::System) -> BTreeSet<chc::Sort>{letmut sorts = BTreeSet::new();for def in&system.pred_vars{
sorts.extend(def.sig.clone());}for clause in&system.clauses{
sorts.extend(clause.vars.clone());atom_sorts(clause,&clause.head,&mut sorts);for a in clause.body.iter_atoms(){// walks atoms, but NOT quantifier bindersatom_sorts(clause, a,&mut sorts);}}
sorts
}
iter_atoms() (src/chc.rs:1504-1506) does descend into Exists/Forallbodies, so atoms inside a quantifier are visited — but the binder list Vec<(String, Sort)> is metadata that no atom references, and term_sorts discards the sort of the only term that mentions a bound variable (FormulaQuantifiedVar). So a binder-only sort is invisible to collect_sorts, hence never declared by FormatContext::from_system (format_context.rs:263-280).
Expected behavior / fix direction
collect_sorts must also collect the sorts of Exists/Forall binders (and, defensively, the sort of FormulaQuantifiedVar in term_sorts). Concretely, walk the formula structure of clause.head/clause.body and, for each Formula::Exists(vars, _) / Formula::Forall(vars, _), sorts.extend(vars.iter().map(|(_, s)| s.clone())), and change the FormulaQuantifiedVar(sort, _) arm of term_sorts to insert sort. Either change alone fixes the binder case; doing both is robust. (Composite binder sorts then also need their monomorphized/builtin datatype declarations, which from_system already derives from the collected sort set, so collecting the sort is sufficient.)
Related but distinct
Undefined sort name in SMT2 when predicate argument is a struct containing a closure #47 ("Undefined sort name in SMT2 when predicate argument is a struct containing a closure") is also an "undefined sort" symptom, but its cause is the predicate define-fun emitting a Tuple sort for a closure-containing struct argument — a different code path with a different trigger. The bug here is specifically about forall/exists binder sorts in collect_sorts/term_sorts and triggers on plain enums/tuples with no closures or predicates involved.
Summary
collect_sorts(src/chc/format_context.rs:209-225) gathers the sorts that need datatype declarations in the emitted SMT-LIB only from (a)pred_varssignatures, (b)clause.vars, and (c) the argument terms of atoms (viaatom_sorts/term_sorts). It never visits the binder sorts ofFormula::Exists/Formula::Forall. Compounding this,term_sortsignores the sort carried by a quantified variable term:As a result, when a
forall/existsquantifies over a variable whose sort is a datatype, tuple, or any other non-builtin/composite sort that does not otherwise appear in the same clause, that sort is never collected, so nodeclare-datatypeis emitted for it. The generated SMT then references an undeclared sort inside the quantifier, and any SMT solver rejects the query as ill-typed.This is an incompleteness bug, not a soundness hole: the malformed SMT is rejected at parse time (before solving), surfaced as a generic
verification error, so safe programs are falsely rejected rather than wrongly accepted. Because it fails during SMT parsing, it is independent of which CHC backend is used — the PCSat solver normally required for quantified specs would reject it just the same.Reproduction
repro.rs— a postcondition that quantifies over an enum-sorted variable:The generated
/tmp/out/thrust_output.smt2usesEin the quantifier binder but never declares it (nodeclare-datatypeforEanywhere in the file):The body need not even reference the bound variable — and referencing it does not help, because
term_sortsdrops the sort of aFormulaQuantifiedVar(line 60 above) andDatatypeCtoronly recurses into its arguments without adding its own datatype sort.Also reproduces for tuple-sorted binders
with the emitted assert containing
(exists ((p Tuple<Int-Int>)) ...)and zerodeclare-datatype.Control: the sort is declared when it appears outside the quantifier
Adding a parameter of the same sort makes
Ereachclause.vars, so it is collected and declared, and theunknown sorterror disappears:This isolates the defect to quantifier-binder-only sorts.
Root cause
src/chc/format_context.rs:iter_atoms()(src/chc.rs:1504-1506) does descend intoExists/Forallbodies, so atoms inside a quantifier are visited — but the binder listVec<(String, Sort)>is metadata that no atom references, andterm_sortsdiscards the sort of the only term that mentions a bound variable (FormulaQuantifiedVar). So a binder-only sort is invisible tocollect_sorts, hence never declared byFormatContext::from_system(format_context.rs:263-280).Expected behavior / fix direction
collect_sortsmust also collect the sorts ofExists/Forallbinders (and, defensively, the sort ofFormulaQuantifiedVarinterm_sorts). Concretely, walk the formula structure ofclause.head/clause.bodyand, for eachFormula::Exists(vars, _)/Formula::Forall(vars, _),sorts.extend(vars.iter().map(|(_, s)| s.clone())), and change theFormulaQuantifiedVar(sort, _)arm ofterm_sortsto insertsort. Either change alone fixes the binder case; doing both is robust. (Composite binder sorts then also need their monomorphized/builtin datatype declarations, whichfrom_systemalready derives from the collected sort set, so collecting the sort is sufficient.)Related but distinct
define-funemitting aTuplesort for a closure-containing struct argument — a different code path with a different trigger. The bug here is specifically aboutforall/existsbinder sorts incollect_sorts/term_sortsand triggers on plain enums/tuples with no closures or predicates involved.Environment
70846d7nightly-2025-09-08(perrust-toolchain.toml)THRUST_SOLVER=z3)