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
Incompleteness: a refinement on a generic type argument in #[param] position is dropped for multi-variant enums, so the payload refinement is never assumed in the callee #193
A refinement written on a generic type argument of an enum in #[thrust_macros::param(..)] position (e.g. o: Opt<{ v: i64 | v > 0 }>) is silently dropped when the enum has more than one variant. The refinement predicate for the payload is still generated and defined in the emitted CHC system, but it is never applied as a hypothesis to the destructured payload, so the callee cannot use the fact that the payload is refined. Safe programs are therefore rejected (verification error: Unsat).
The single-variant case works correctly (the payload predicate is applied), so the discrepancy is specifically about the variant count of the generic enum.
Reproduction (the bug)
//@compile-flags: -C debug-assertions=offpubenumOpt<T>{Som(T),Non}impl<T> thrust_models::ModelforOpt<T>{typeTy = Self;}// Precondition: if `o` is `Som(x)`, then `x > 0`.#[thrust_macros::param(o:Opt<{ v:i64 | v > 0}>)]fnf(o:Opt<i64>){match o {Opt::Som(x) => {assert!(x > 0);}// guaranteed by the preconditionOpt::Non => {}}}fnmain(){f(Opt::Som(5));// 5 > 0 satisfies the precondition; never panics}
True runtime behavior: never panics. Opt::Som(5) satisfies the precondition Opt<{ v | v > 0 }>, and inside f the only reachable assert!(x > 0) holds because x == 5.
Thrust verdict:error: verification error: Unsat (rejected). WRONG — this is an incompleteness.
Control that isolates it (single variant → correct)
Identical in every respect except the enum has one variant:
//@compile-flags: -C debug-assertions=offpubenumOne<T>{V(T)}impl<T> thrust_models::ModelforOne<T>{typeTy = Self;}#[thrust_macros::param(o:One<{ v:i64 | v > 0}>)]fnf(o:One<i64>){match o {One::V(x) => {assert!(x > 0);}}}fnmain(){f(One::V(5));}
Thrust verdict:safe. Correct. The only difference from the bug case is single- vs. multi-variant, so the payload refinement is being lost specifically for multi-variant generic enums.
Additional observations from narrowing:
A two-variant enum where both variants carry T (enum Two<T> { A(T), B(T) }, asserting on the payload in each arm) is also rejected — so it is the variant count, not the nullary Non.
The same multi-variant enum whose body does not rely on the payload refinement (e.g. Opt::Som(_x) => assert!(true)) verifies safe — so matching itself works; only the refinement is dropped.
The built-in Option<{ v: i64 | v > 0 }> in the same position is dropped identically.
Caller-side enforcement is still sound: passing a payload that violates the precondition is correctly rejected, so this is purely an incompleteness, not an unsoundness.
Root cause (SMT-level evidence)
Dumping the generated CHC (THRUST_OUTPUT_DIR=…) for the two programs above makes the difference precise.
Single variant — the precondition predicate p3 (defined as v1 > 0) is applied to the payload v1 in the hypothesis of the assert!-obligation clause, so the obligation is discharged:
With (p3 v1) ⟹ v1 > 0, the antecedent v6 = (v1 > 0) ∧ v6 = false is contradictory, so the clause is vacuously valid → safe.
Multi variant — the analogous payload predicate p5 is defined but never used as a hypothesis anywhere. The Som-arm payload v1 is left completely unconstrained:
Because nothing constrains v1, v9 = (v1 > 0) may be false, the antecedent is satisfiable, and the false head is unprovable → Unsat. (p5, and the similarly-declared p6/p8, are emitted but orphaned.)
So the type-argument refinement does reach the type of the payload field — install_refinement_at installs it into EnumType.args (src/rty.rs:1656), and subst_ty_params/instantiate_ty_params correctly merge an arg's refinement into a Type::Param field (src/rty.rs:1800, the Type::Param branch does self.refinement.push_conj(refinement)). The refinement is nonetheless not wired as a hypothesis on the payload when a multi-variant enum parameter is bound and destructured. The relevant machinery is Env::bind_enum (src/refine/env.rs:741), which builds each variant's field binding with instantiate_ty_params(ty.args.clone()) guarded by discr == variant.discr (src/refine/env.rs:766-778) and then assembles the matcher_pred assumption (:781-819); for multi-variant enums the payload's precondition predicate is not connected to the destructured value in the emitted clauses, whereas for the single-variant representation it is.
Why this is not an already-filed / excluded issue
Not a panic / unimplemented!: raw output is exactly error: verification error: Unsat; no panicked at / not implemented.
Not numeric-range/overflow: the only values involved are 5 and > 0; no MAX/MIN/casts.
Not a solver timeout/unknown: it is a deterministic Unsat (reproduced repeatedly), and the single-variant control is a deterministic safe.
Summary
A refinement written on a generic type argument of an enum in
#[thrust_macros::param(..)]position (e.g.o: Opt<{ v: i64 | v > 0 }>) is silently dropped when the enum has more than one variant. The refinement predicate for the payload is still generated and defined in the emitted CHC system, but it is never applied as a hypothesis to the destructured payload, so the callee cannot use the fact that the payload is refined. Safe programs are therefore rejected (verification error: Unsat).The single-variant case works correctly (the payload predicate is applied), so the discrepancy is specifically about the variant count of the generic enum.
Reproduction (the bug)
Opt::Som(5)satisfies the preconditionOpt<{ v | v > 0 }>, and insidefthe only reachableassert!(x > 0)holds becausex == 5.error: verification error: Unsat(rejected). WRONG — this is an incompleteness.Control that isolates it (single variant → correct)
Identical in every respect except the enum has one variant:
safe. Correct. The only difference from the bug case is single- vs. multi-variant, so the payload refinement is being lost specifically for multi-variant generic enums.Additional observations from narrowing:
T(enum Two<T> { A(T), B(T) }, asserting on the payload in each arm) is also rejected — so it is the variant count, not the nullaryNon.Opt::Som(_x) => assert!(true)) verifiessafe— so matching itself works; only the refinement is dropped.Option<{ v: i64 | v > 0 }>in the same position is dropped identically.Root cause (SMT-level evidence)
Dumping the generated CHC (
THRUST_OUTPUT_DIR=…) for the two programs above makes the difference precise.Single variant — the precondition predicate
p3(defined asv1 > 0) is applied to the payloadv1in the hypothesis of theassert!-obligation clause, so the obligation is discharged:With
(p3 v1)⟹v1 > 0, the antecedentv6 = (v1 > 0) ∧ v6 = falseis contradictory, so the clause is vacuously valid →safe.Multi variant — the analogous payload predicate
p5is defined but never used as a hypothesis anywhere. TheSom-arm payloadv1is left completely unconstrained:Because nothing constrains
v1,v9 = (v1 > 0)may befalse, the antecedent is satisfiable, and thefalsehead is unprovable →Unsat. (p5, and the similarly-declaredp6/p8, are emitted but orphaned.)So the type-argument refinement does reach the type of the payload field —
install_refinement_atinstalls it intoEnumType.args(src/rty.rs:1656), andsubst_ty_params/instantiate_ty_paramscorrectly merge an arg's refinement into aType::Paramfield (src/rty.rs:1800, theType::Parambranch doesself.refinement.push_conj(refinement)). The refinement is nonetheless not wired as a hypothesis on the payload when a multi-variant enum parameter is bound and destructured. The relevant machinery isEnv::bind_enum(src/refine/env.rs:741), which builds each variant's field binding withinstantiate_ty_params(ty.args.clone())guarded bydiscr == variant.discr(src/refine/env.rs:766-778) and then assembles thematcher_predassumption (:781-819); for multi-variant enums the payload's precondition predicate is not connected to the destructured value in the emitted clauses, whereas for the single-variant representation it is.Why this is not an already-filed / excluded issue
unimplemented!: raw output is exactlyerror: verification error: Unsat; nopanicked at/not implemented.5and> 0; noMAX/MIN/casts.Unsat(reproduced repeatedly), and the single-variant control is a deterministicsafe.main, so it is not Unsound: a function annotated only withsig/ret/paramwhose body violates the declared refinement verifies assafewhen it is not called (unspecified param precondition becomes an inference template that is discharged vacuously) #179 (uncalledsig/ret/parambody).#[param]only, with no#[ensures], so it is not Incompleteness: a#[param(name: { v | φ })]precondition is silently dropped when the function also has an#[ensures(..)], so trivially-correct functions are rejected #191 (param dropped whenensuresis present) — and unlike Incompleteness: a#[param(name: { v | φ })]precondition is silently dropped when the function also has an#[ensures(..)], so trivially-correct functions are rejected #191 the single-variant version verifies correctly.&{ v | φ }/&mut { v | φ }) in parameter position is not assumed by the callee #166 (reference-pointee refinements&{v|φ}/&mut{v|φ}), and unrelated to the enum-discriminant soundness items (Unsound: enum variants after an explicit discriminant get the wrongdatatype_discrvalue, making match arms vacuously verify #126 explicit discriminants; Unsound: negativeSwitchIntmatch targets are sign-truncated to large positives, making match arms verify under a wrong path assumption #132 negativeSwitchInt).Environment
6953863nightly-2025-09-08(perrust-toolchain.toml)fp.spacer.global=true fp.validate=true)