Skip to content

Ordering comparisons (<, >) on tuple/aggregate values emit ill-typed SMT (< Tuple Tuple) via the generic PartialOrd::lt/gt extern specs, so safe programs comparing tuples are falsely rejected #195

Description

@coord-e

Summary

Tuples implement PartialOrd (lexicographically), so a < b / a > b on (i64, i64) is valid, safe Rust. Thrust accepts the program but, when lowering the comparison, translates the model-level </> with the numeric term builders, producing an SMT-LIB atom like (< v6 v7) where v6/v7 have sort A0_Tuple<Int-Int>. In SMT-LIB </> are declared only for Int, so the query is ill-typed and the solver rejects it. As a result, any function that compares two tuples (or any aggregate value) with </> fails verification — even when it is trivially safe.

Like #136, this is an incompleteness bug (not a soundness hole): the solver error is surfaced as a generic verification error, so safe programs are falsely rejected, never wrongly accepted.

This is distinct from #136, which is specifically the bool case flowing through the Rvalue::BinaryOp arms in src/analyze/basic_block.rs:509-520. Tuples are not a primitive MIR BinOp; a < b on a tuple lowers to a PartialOrd::lt call that Thrust maps to the generic extern spec _extern_spec_partialord_lt<T> in std.rs, whose ensures body is where the ill-typed < is produced. The two bugs share a class ("ordering operators applied to a non-Int sort") but have different code paths and different fix sites; #136's suggested fix (splitting the Int | Bool BinaryOp arms) does not touch this path.

Reproduction

Minimal — symbolic tuple parameters, a bare a < b:

#[thrust::callable]
fn check(a: (i64, i64), b: (i64, i64)) {
    let _ = a < b;
}
fn main() {}
$ cargo run -- -Adead_code -C debug-assertions=false min.rs
error: verification error: Error { stdout: "(error \"line 23 column 313: Sort mismatch at argument #1 for function (declare-fun < (Int Int) Bool) supplied sort is A0_Tuple<Int-Int>\")\nsat\n", stderr: "" }
error: aborting due to 1 previous error

Realistic — lexicographic max of two pairs (never panics, so it should verify):

fn max_pair(a: (i64, i64), b: (i64, i64)) -> (i64, i64) {
    if a > b { a } else { b }
}

#[thrust::callable]
fn check(a: (i64, i64), b: (i64, i64)) {
    let m = max_pair(a, b);
    let _ = m;
}
fn main() {}

fails the same way (... Sort mismatch ... for function (declare-fun > (Int Int) Bool) supplied sort is A0_Tuple<Int-Int>).

Control — the identical program with i64 scalars instead of tuples verifies (safe), isolating the defect to aggregate operands:

fn max_i(a: i64, b: i64) -> i64 { if a > b { a } else { b } }
#[thrust::callable]
fn check(a: i64, b: i64) { let m = max_i(a, b); let _ = m; }
fn main() {}

Generated SMT-LIB evidence

THRUST_OUTPUT_DIR=… on the minimal example emits (every vN here is A0_Tuple<Int-Int>):

(declare-datatypes ((A0_Tuple<Int-Int> 0)) (...))
...
(assert (forall ((v0 A0_Tuple<Int-Int>) ... (v6 A0_Tuple<Int-Int>) (v7 A0_Tuple<Int-Int>) (v8 Bool))
  (=> (and ... (= v8 (< v6 v7))) (p3 v8 v0 v1 v2 v3 v4 v5))))

(< v6 v7) applies the numeric < (declare-fun < (Int Int) Bool) to two A0_Tuple<Int-Int> arguments → ill-typed, which z3 (and any compliant solver) rejects.

Root cause

a < b / a > b on a tuple is a PartialOrd::lt / PartialOrd::gt call, which Thrust resolves to the generic extern specs in std.rs (T: PartialOrd, no restriction to integer types):

https://github.com/coord-e/thrust/blob/6953863/std.rs#L998-L1022

#[thrust_macros::ensures(result == (*x < *y))]
fn _extern_spec_partialord_lt<T>(x: &T, y: &T) -> bool
  where T: thrust_models::Model + PartialOrd, T::Ty: PartialOrd
{ PartialOrd::lt(x, y) }

#[thrust_macros::ensures(result == (*x > *y))]
fn _extern_spec_partialord_gt<T>(x: &T, y: &T) -> bool
  where T: thrust_models::Model + PartialOrd, T::Ty: PartialOrd
{ PartialOrd::gt(x, y) }

When T is instantiated at an aggregate type (here (i64, i64)), *x/*y have an aggregate model sort, and the formula-level </> in the ensures is translated with the numeric Term::lt/Term::gt builders (Function::LT/GT, src/chc.rs:806-816), which print as the Int-only </>. The comparison-operator lowering does not check the operand sort, so the aggregate sort flows straight into a numeric </> application — exactly the same class of defect as #136, but originating in the extern-spec/formula path rather than the BinaryOp path.

Note on <= / >=

<=/>= on tuples do not hit this path — there are no partialord_le/ge extern specs, so the call is unresolved and instead hits an unimplemented!/panic! ("unknown def") in src/analyze/basic_block.rs. So aggregate ordering is currently: </> → ill-typed SMT (this issue); <=/>= → clean "unsupported" panic. Ideally all four behave consistently.

Suggested direction

Two complementary options:

  1. Encode lexicographic ordering for aggregate operands (recurse field-by-field), so </> on tuples/structs produce well-typed SMT that matches Rust's derived PartialOrd; add the missing <=/>= specs at the same time.
  2. If aggregate ordering is out of scope for now, at least make </> on non-Int operands fail with the same explicit "unsupported" diagnostic as <=/>=, instead of silently emitting ill-typed SMT that surfaces as an opaque solver error.

Whichever direction, the comparison lowering should guard on the operand sort so a non-Int sort never reaches the numeric </>/<=/>= term builders (mirrors the guard recommended in #136 for the BinaryOp arms).

Environment

  • thrust @ 6953863
  • rustc nightly-2025-09-08 (per rust-toolchain.toml)
  • z3 4.13.4

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions