Summary
A refinement on a Box (own pointer) pointee in return position — Box<{v | v > 0}> — is parsed and built into the function type correctly, but the corresponding subtyping/CHC obligation is generated incorrectly, so it is not enforced. This is a genuine verification-logic bug (unsound), not an annotation-semantics issue.
Enum / datatype nested type-argument refinements (Opt<{v | v > 0}>) are fine — see "Not a bug" below. (An earlier revision of this issue incorrectly flagged the enum case; that part was an annotation-semantics artifact, not unsoundness.)
Repro: genuine unsoundness (Box pointee)
//@compile-flags: -C debug-assertions=off
#[thrust_macros::param(n: { v: i32 | v > 0 })] // satisfiable precondition
#[thrust_macros::ret(Box<{ v: i32 | v > 0 }>)]
fn mk(n: i32) -> Box<i32> { Box::new(-1) } // ignores n, always returns -1
fn main() {
let b = mk(7); // precondition 7 > 0 holds, so mk is callable
assert!(*b > 0); // *b == -1, so this is FALSE
}
- Expected: verification failure (
Unsat).
- Actual: verifies successfully (exit 0, no
Unsat).
Because the precondition n > 0 is satisfiable and the body ignores n, no inferred precondition can "rescue" this — the body unconditionally returns -1, which violates Box<{v > 0}>. The obligation is simply not generated/enforced correctly.
The bug is specific to the Box pointee, not to zero-arg functions or nested positions in general:
| function |
body |
called + asserted |
result |
fn h() -> i32 ret {r > 0} (top-level) |
-1 |
yes |
Unsat ✅ |
fn mk(n: i32) -> Opt<i32> ret Opt<{v > 0}> (enum arg) |
Some(-1) |
yes |
Unsat ✅ |
fn mk(n: i32) -> Box<i32> ret Box<{v > 0}> (box pointee) |
Box::new(-1) |
yes |
accepted ❌ |
The constraint also misfires in the opposite direction (incompleteness): fn mk(x: i32) -> Box<i32> { Box::new(x) } with param x > 0 / ret Box<{v > 0}> is wrongly rejected (Unsat) though it is valid. So the box-pointee obligation appears to relate the wrong term rather than merely being absent.
The function type itself is built correctly:
register_def ...::mk rty=({ () | p0 }) → own { int | ν > 0 }
Likely area: subtyping / CHC generation for own pointer pointees in return-value relating (src/rty/subtyping.rs, src/analyze/basic_block.rs return-place handling), and/or how the Box::new extern-spec post-state (result == box(arg)) is related to a pointee refinement.
Not a bug: enum/datatype nested args (annotation-semantics note)
Opt<{v | v > 0}> is enforced soundly. A function annotated only on the return (#[thrust::ret(Opt<{v>0}>)]) with an unannotated parameter appears to "verify in isolation":
#[thrust::ret(Opt<{v: int | v > 0}>)]
fn g(x: i64) -> Opt<i64> { Opt::Some(x) } // verifies on its own
…but this is because an unannotated parameter in a partially-annotated function receives a fresh predicate variable (inferred precondition), not true. The solver infers g's precondition to x > 0, so g is internally consistent, and any violating call is correctly rejected:
fn main() { let r = g(-5); match r { Opt::Some(i) => assert!(i > 0), Opt::None => {} } }
// => verification error: Unsat (correctly rejected)
So the verifier is sound here. Possible improvement (separate from the bug above): give unannotated parameters of a partially-annotated function the true refinement instead of a fresh pvar, so such functions are checked against an unconstrained input rather than an inferred precondition.
Aside (separate, minor)
The legacy parse_rty parser panics on Box<{..}> syntax (#[thrust::ret(Box<{v: int | v > 0}>)]) at src/analyze/local_def.rs:97, so the box-pointee gap above is currently only reachable through the thrust_macros annotations (which build the equivalent type).
Environment
- Z3 4.13.0; nightly per
rust-toolchain.toml. Reproduced on e25bfb0.
Summary
A refinement on a
Box(own pointer) pointee in return position —Box<{v | v > 0}>— is parsed and built into the function type correctly, but the corresponding subtyping/CHC obligation is generated incorrectly, so it is not enforced. This is a genuine verification-logic bug (unsound), not an annotation-semantics issue.Enum / datatype nested type-argument refinements (
Opt<{v | v > 0}>) are fine — see "Not a bug" below. (An earlier revision of this issue incorrectly flagged the enum case; that part was an annotation-semantics artifact, not unsoundness.)Repro: genuine unsoundness (
Boxpointee)Unsat).Unsat).Because the precondition
n > 0is satisfiable and the body ignoresn, no inferred precondition can "rescue" this — the body unconditionally returns-1, which violatesBox<{v > 0}>. The obligation is simply not generated/enforced correctly.The bug is specific to the
Boxpointee, not to zero-arg functions or nested positions in general:fn h() -> i32ret{r > 0}(top-level)-1Unsat✅fn mk(n: i32) -> Opt<i32>retOpt<{v > 0}>(enum arg)Some(-1)Unsat✅fn mk(n: i32) -> Box<i32>retBox<{v > 0}>(box pointee)Box::new(-1)The constraint also misfires in the opposite direction (incompleteness):
fn mk(x: i32) -> Box<i32> { Box::new(x) }withparam x > 0/ret Box<{v > 0}>is wrongly rejected (Unsat) though it is valid. So the box-pointee obligation appears to relate the wrong term rather than merely being absent.The function type itself is built correctly:
Likely area: subtyping / CHC generation for
ownpointer pointees in return-value relating (src/rty/subtyping.rs,src/analyze/basic_block.rsreturn-place handling), and/or how theBox::newextern-spec post-state (result == box(arg)) is related to a pointee refinement.Not a bug: enum/datatype nested args (annotation-semantics note)
Opt<{v | v > 0}>is enforced soundly. A function annotated only on the return (#[thrust::ret(Opt<{v>0}>)]) with an unannotated parameter appears to "verify in isolation":…but this is because an unannotated parameter in a partially-annotated function receives a fresh predicate variable (inferred precondition), not
true. The solver infersg's precondition tox > 0, sogis internally consistent, and any violating call is correctly rejected:So the verifier is sound here. Possible improvement (separate from the bug above): give unannotated parameters of a partially-annotated function the
truerefinement instead of a fresh pvar, so such functions are checked against an unconstrained input rather than an inferred precondition.Aside (separate, minor)
The legacy
parse_rtyparser panics onBox<{..}>syntax (#[thrust::ret(Box<{v: int | v > 0}>)]) atsrc/analyze/local_def.rs:97, so the box-pointee gap above is currently only reachable through thethrust_macrosannotations (which build the equivalent type).Environment
rust-toolchain.toml. Reproduced one25bfb0.