Summary
Unsigned integer parameters are translated to the plain mathematical-integer sort (rty::Type::Int, SMT Int) with no >= 0 refinement. As a result, Thrust models a usize/u32/u64 value as an arbitrary integer that may be negative, and cannot discharge obligations that depend on the non-negativity that the Rust type system guarantees. Trivially-safe programs are rejected with Unsat.
The compiler itself considers x >= 0 on a usize a tautology (it emits warning: comparison is useless due to type limits), yet Thrust reports Unsat for it.
This is a soundness-preserving incompleteness (safe programs are rejected), analogous in flavor to #157 / #128. Non-negativity of unsigned values is needed for a large class of real programs (index/length arithmetic, loop bounds, etc.), so the gap is fairly fundamental.
Reproduction
Minimal:
#[thrust::callable]
fn nonneg(x: usize) {
assert!(x >= 0);
}
fn main() {}
$ cargo run -- -Adead_code -C debug-assertions=false nonneg.rs
warning: comparison is useless due to type limits // <- rustc knows x >= 0 always holds
--> nonneg.rs:3:13
|
3 | assert!(x >= 0);
| ^^^^^^
error: verification error: Unsat
error: aborting due to 1 previous error; 1 warning emitted
A realistic case that does not trip the lint (does not rely on any overflow reasoning, since Int is mathematical):
#[thrust::callable]
fn check(a: usize, b: usize) {
let c = a + b;
assert!(c >= a); // always true for non-negative a, b
}
fn main() {}
also fails with error: verification error: Unsat.
Reproduced on 6adb56d (Make seq concatenation operate on sequence tuples (#162)) with z3 4.16.0.
Expected vs. actual
- Expected: both programs verify (
safe); a usize/u32/u64 value is >= 0 by definition.
- Actual: both are rejected with
Unsat.
Root cause
usize, u32, u64 are declared via the int_model! macro in std.rs exactly like the signed types, mapping their model to model::Int:
https://github.com/coord-e/thrust/blob/6adb56d/std.rs#L307-L312
int_model!(isize);
int_model!(i32);
int_model!(i64);
int_model!(usize); // <- collapsed to model::Int, losing unsignedness
int_model!(u32);
int_model!(u64);
The Model projection erases signedness, so by the time the type reaches TemplateBuilder::model_adt it is just the int model and is translated to an unrefined rty::Type::int():
https://github.com/coord-e/thrust/blob/6adb56d/src/refine/template.rs#L176-L178
No { v: int | v >= 0 } refinement is ever attached to values of unsigned type.
Confirmation of root cause
Adding the missing lower bound by hand makes both programs verify, which pinpoints the missing constraint:
#[thrust_macros::requires(a >= 0 && b >= 0)] // <- restores the bound the type already guarantees
fn check(a: usize, b: usize) {
let c = a + b;
assert!(c >= a);
}
fn main() {}
$ cargo run -- -Adead_code -C debug-assertions=false check_fixed.rs && echo safe
safe
Likewise #[thrust_macros::requires(x >= 0)] on the minimal nonneg example makes it verify.
Suggested direction
Attach a >= 0 refinement to values of unsigned integer type. Because signedness is erased at the Model-projection step, the natural place to reintroduce it is where the Rust type is lowered — e.g. special-case TyKind::Uint(_) in TemplateBuilder::build/model_adt (src/refine/template.rs) to produce a refined { v: int | v >= 0 } instead of a bare rty::Type::int(), or introduce a distinct unsigned model type carrying that invariant. Whichever mechanism is used, the invariant must be assumed for unsigned inputs/results (and it would then also let Thrust reason about unsigned subtraction/underflow more faithfully).
Note: u8/u16 (and i8/i16) are a separate, orthogonal matter — they are not in int_model! at all and currently hit unimplemented!("unrefined_ty: ..") in src/refine/template.rs.
Summary
Unsigned integer parameters are translated to the plain mathematical-integer sort (
rty::Type::Int, SMTInt) with no>= 0refinement. As a result, Thrust models ausize/u32/u64value as an arbitrary integer that may be negative, and cannot discharge obligations that depend on the non-negativity that the Rust type system guarantees. Trivially-safe programs are rejected withUnsat.The compiler itself considers
x >= 0on ausizea tautology (it emitswarning: comparison is useless due to type limits), yet Thrust reportsUnsatfor it.This is a soundness-preserving incompleteness (safe programs are rejected), analogous in flavor to #157 / #128. Non-negativity of unsigned values is needed for a large class of real programs (index/length arithmetic, loop bounds, etc.), so the gap is fairly fundamental.
Reproduction
Minimal:
A realistic case that does not trip the lint (does not rely on any overflow reasoning, since
Intis mathematical):also fails with
error: verification error: Unsat.Reproduced on
6adb56d(Make seq concatenation operate on sequence tuples (#162)) with z3 4.16.0.Expected vs. actual
safe); ausize/u32/u64value is>= 0by definition.Unsat.Root cause
usize,u32,u64are declared via theint_model!macro instd.rsexactly like the signed types, mapping their model tomodel::Int:https://github.com/coord-e/thrust/blob/6adb56d/std.rs#L307-L312
The
Modelprojection erases signedness, so by the time the type reachesTemplateBuilder::model_adtit is just the int model and is translated to an unrefinedrty::Type::int():https://github.com/coord-e/thrust/blob/6adb56d/src/refine/template.rs#L176-L178
No
{ v: int | v >= 0 }refinement is ever attached to values of unsigned type.Confirmation of root cause
Adding the missing lower bound by hand makes both programs verify, which pinpoints the missing constraint:
Likewise
#[thrust_macros::requires(x >= 0)]on the minimalnonnegexample makes it verify.Suggested direction
Attach a
>= 0refinement to values of unsigned integer type. Because signedness is erased at theModel-projection step, the natural place to reintroduce it is where the Rust type is lowered — e.g. special-caseTyKind::Uint(_)inTemplateBuilder::build/model_adt(src/refine/template.rs) to produce a refined{ v: int | v >= 0 }instead of a barerty::Type::int(), or introduce a distinct unsigned model type carrying that invariant. Whichever mechanism is used, the invariant must be assumed for unsigned inputs/results (and it would then also let Thrust reason about unsigned subtraction/underflow more faithfully).Note:
u8/u16(andi8/i16) are a separate, orthogonal matter — they are not inint_model!at all and currently hitunimplemented!("unrefined_ty: ..")insrc/refine/template.rs.