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
A locally-declared struct/enum never gets a Model instance, so Vec<P>/Box<P>/&[P]/[P; N]/&mut P abort in TypeBuilder::build — Vec<P> dies inside alloc's private fields (*const u8), and a one-line impl Model for P { type Ty = Self; } makes every case verify #266
Thrust decides a Rust type's logical model by normalizing <T as thrust_models::Model>::Ty (TypeBuilder::resolve_model_ty, src/refine/template.rs:154-169). std.rs gives Vec<T>, Box<T>, [T], [T; N] and &mut Tconditional impls that require T: Model — but no Model instance is ever created for a user-defined struct/enum. For a local ADT P the projection therefore fails to normalize, resolve_model_ty silently returns the unresolved type, and TypeBuilder::build then either
has no arm for it (&mut P, &[P], [P; N]) and hits unimplemented!, or
— worse — takes the def.is_struct()structural arm for Vec<P> / Box<P> and starts expanding alloc's private representation, aborting with not implemented: ty: *const u8.
This is not "Vec is unsupported" or "&mut is unsupported": Vec<i64>, Box<i64>, &mut i64, &P, &self, by-value P, Option<P> and (P, i64) all verify today, and adding the semantically vacuous line impl thrust_models::Model for P { type Ty = Self; } makes every failing program below verify with the correct verdict in both directions. The analysis fully supports these types; what is missing is the Model instance that a local ADT should have by construction. A &mut P that is created and consumed inside one body already verifies without the impl (see the matrix) — only the positions that must go through the Model projection fail.
The failure surfaces as an ICE naming a std implementation detail (*const u8, *const P), which gives a user no way to work out that the fix is one line of undocumented boilerplate — thrust_models::Model appears nowhere in README.md, and inside a mod the workaround must additionally be spelled impl crate::thrust_models::Model for … (thrust_models is injected at the crate root; cf. #252/#256).
Minimal reproduction
repro.rs — no &mut, no annotations, a supported container holding a supported struct:
$ cargo run -q -- -Adead_code -C debug-assertions=false repro.rsthread 'rustc' (5807) panicked at src/refine/template.rs:440:21:not implemented: ty: *const u8stack backtrace: 2: thrust::refine::template::TemplateTypeBuilder<R,S>::build at ./src/refine/template.rs:440:21 3: thrust::refine::template::TemplateTypeBuilder<R,S>::build::{{closure}} at ./src/refine/template.rs:432:56 ...
*const u8 is alloc::raw_vec::RawVecInner's pointer field — Thrust walked into Vec's internals instead of using its Seq model. The program is ordinary safe Rust:
struct Point { x: i64, y: i64 }
+impl thrust_models::Model for Point { type Ty = Self; }
$ cargo run -q -- -Adead_code -C debug-assertions=false repro.rs &&echo safesafe
$ # and the negation is still correctly rejected:
$ # assert!(points[0].x == 2) -> error: verification error: Unsat
The same one-liner is what makes &mut Point work:
structP{x:i64}fnset(p:&mutP,n:i64){ p.x = n;}// not implemented: ty: &'{erased} mut Pfnmain(){letmut p = P{x:1};set(&mut p,5);assert!(p.x == 5);}
Evidence matrix
All rows -Adead_code -C debug-assertions=false --edition 2021, thrust @ 5c74a7f, Z3 5.0.0, default solver config. "no impl" = the program exactly as written; "with impl" = the same program plus impl thrust_models::Model for P { type Ty = Self; } (resp. for the enum).
#
program (struct P { x: i64 } / enum E { A(i64), B } unless noted)
no impl
with impl
1
fn get(p: P) -> i64 { p.x }, by value
safe
safe
2
fn get(p: &P) -> i64 { p.x }
safe
safe
3
impl P { fn get(&self) -> i64 { self.x } }
safe
safe
4
fn get(e: &E) -> i64 { match e { … } }
safe
safe
5
let o = Some(P{x:1}); match o { Some(p) => assert!(p.x == 1), … }
safe (neg: Unsat)
safe
6
let t = (P{x:1}, 2i64); assert!(t.0.x == 1 && t.1 == 2)
safe (neg: Unsat)
safe
7
let r = &mut p; r.x = 5; assert!(p.x == 5) — borrow created and consumed in one body
safe (neg: Unsat)
safe
8
fn set(p: &mut P, n: i64)
ICEty: &'{erased} mut P
safe (neg: Unsat)
9
impl P { fn set(&mut self, n: i64) }
ICEty: &'{erased} mut P
safe
10
fn id(p: &mut P) -> &mut P { p }
ICEty: &'{erased} mut P
safe
11
fn bump(e: &mut E) (enum)
ICEty: &'{erased} mut E
safe
12
impl Bump for P { fn bump(&mut self) } (trait method)
ICEty: &'{erased} mut P
safe
13
fn f(o: Option<&mut P>)
ICEty: &'{erased} mut P
safe
14
fn f(t: (&mut P, i64))
ICEty: &'{erased} mut P
safe
15
let f = |q: &mut P| { q.x = 5; }; f(&mut p); (closure param)
ICEty: &'{erased} mut P
safe
16
fn set<T>(r: &mut T, v: T) { *r = v; } called at T = P
ICEty: &'{erased} mut P
safe
17
let mut v = Vec::new(); v.push(P{x:1}); assert!(v[0].x == 1)
ICEty: *const u8
safe (neg: Unsat)
18
fn f(v: &Vec<P>) -> i64 { v[0].x } (shared ref)
ICEty: *const u8
safe
19
fn f(v: &mut Vec<P>) { v[0].x = 5; }
ICEty: &'{erased} mut Vec<P, Global>
safe
20
v.push(E::A(1)) (Vec of a local enum)
ICEty: *const u8
safe
21
let b = Box::new(P{x:1}); assert!(b.x == 1)
ICEunrefined_ty: *const P
safe
22
fn f(s: &[P]) -> i64 { s[0].x }
ICEty: [P]
safe
23
let a = [P{x:1}, P{x:2}]; assert!(a[0].x == 1)
ICEunrefined_ty: [P; 2_usize]
safe
Controls with a type that does have a Model impl in std.rs — all safe, no boilerplate needed:
program
verdict
let mut v = Vec::new(); v.push(1i64); assert!(v[0] == 1)
safe
let mut b = Box::new(1i64); *b += 1; assert!(*b == 2)
safe
fn set(r: &mut i64, v: i64) { *r = v; }
safe
fn set<T>(r: &mut T, v: T) { *r = v; } at T = i64
safe
Rows 1-7 are the decisive ones. They show that a local ADT is not unsupported anywhere build has a structural arm — including a mutable borrow of it, as long as that borrow never has to be turned into a type (row 7, which also rejects its negation, so the prophecy machinery is working on P already). Rows 8-23 are the same ADT in the positions that must go through the Model projection.
Note also the asymmetry between rows 2 and 18: a shared&P verifies, but &Vec<P> does not — so this is not a &mut-only story, and Vec is not "unsupported": the single variable is whether P: Model holds.
Root cause
resolve_model_ty (src/refine/template.rs:154-169) tries to normalize the Model::Ty projection and falls back to the original type when that fails, with no diagnostic:
and nothing ever supplies impl Model for P for a locally-declared P. So for a local ADT:
&mut P. Both build impls (TypeBuilder::build, src/refine/template.rs:206-260; TemplateTypeBuilder::build, :392-440) have an arm for TyKind::Ref(_, elem, Mutability::Not) that builds PointerType::immut_to structurally, but no arm for Mutability::Mut — mutable references are handled only by resolve_model_ty rewriting them to model::Mut<..>, which model_adt's mut_model() branch then turns into PointerType::mut_to. When the rewrite does not fire, the type falls through to unimplemented!("ty: {:?}"). The shared-reference arm shows the structural fallback was intended here; it was just never written for Mut.
Vec<P> / Box<P>. These are TyKind::Adt and def.is_struct(), so the unresolved type lands in the struct arm and is expanded field-by-field into alloc::raw_vec::RawVec → RawVecInner → Unique<u8> → NonNull<u8> → *const u8 (resp. Unique<P> → *const P). The abort message therefore names a std private field, not the user's type.
&[P] / [P; N].TyKind::Slice / TyKind::Array have no arm at all, so they reach unimplemented! directly.
Option<P> and (P, i64) escape all of this because build reaches them through its own is_enum() (nominal) and TyKind::Tuple arms, which never consult Model.
Because 62 of the 336 files under tests/ui/ already carry impl thrust_models::Model for … { type Ty = Self; } by hand, CI never exercises a local ADT without one.
Scope / when it bites
Every idiomatic Rust program hits this immediately: a &mut self method on a user type (rows 9, 12), a Vec of user structs (rows 17-19), Box (21) and slices (22-23) of them. It is the first thing encountered when pointing Thrust at real code, and the message gives no hint about the cause. Not a numeric-range/overflow/unsigned issue — every reproducer uses the literals 1, 2, 5.
Suggested direction
Give a locally-declared ADT a Model instance rather than requiring the user to write one:
(a) In resolve_model_ty, when the <T as Model>::Ty projection does not normalize and T is a local ADT, treat T as its own model (type Ty = Self) and re-try the enclosing projection — i.e. make the container impls' T: Model bound discharge the way the 62 hand-written test impls make it discharge today. This fixes all of rows 8-23 at once, because it restores Vec<P> → Seq<P>, &mut P → Mut<P>, etc.
(b) Or synthesize the impl Model for P { type Ty = Self; } item for every local ADT the way std.rs is injected, which is the same thing at the source level.
Independently worth doing, since both make the failure mode legible:
add the missing TyKind::Ref(_, _, Mutability::Mut) arm to both build impls, mirroring the Mutability::Not arm (PointerType::mut_to(self.build(elem)));
stop the def.is_struct() arm from expanding an ADT that carries a #[thrust::def::*] model (Vec, Box, …) — expanding alloc's private fields can only ever produce a confusing abort — and report a proper diagnostic naming the user's type instead.
If requiring the impl is deliberate, then at minimum it needs to be in README.md (it is currently undocumented), and the abort should name the user's type and the missing impl rather than *const u8.
Summary
Thrust decides a Rust type's logical model by normalizing
<T as thrust_models::Model>::Ty(TypeBuilder::resolve_model_ty,src/refine/template.rs:154-169).std.rsgivesVec<T>,Box<T>,[T],[T; N]and&mut Tconditional impls that requireT: Model— but noModelinstance is ever created for a user-definedstruct/enum. For a local ADTPthe projection therefore fails to normalize,resolve_model_tysilently returns the unresolved type, andTypeBuilder::buildthen either&mut P,&[P],[P; N]) and hitsunimplemented!, ordef.is_struct()structural arm forVec<P>/Box<P>and starts expandingalloc's private representation, aborting withnot implemented: ty: *const u8.This is not "
Vecis unsupported" or "&mutis unsupported":Vec<i64>,Box<i64>,&mut i64,&P,&self, by-valueP,Option<P>and(P, i64)all verify today, and adding the semantically vacuous lineimpl thrust_models::Model for P { type Ty = Self; }makes every failing program below verify with the correct verdict in both directions. The analysis fully supports these types; what is missing is theModelinstance that a local ADT should have by construction. A&mut Pthat is created and consumed inside one body already verifies without the impl (see the matrix) — only the positions that must go through theModelprojection fail.The failure surfaces as an ICE naming a
stdimplementation detail (*const u8,*const P), which gives a user no way to work out that the fix is one line of undocumented boilerplate —thrust_models::Modelappears nowhere inREADME.md, and inside amodthe workaround must additionally be spelledimpl crate::thrust_models::Model for …(thrust_modelsis injected at the crate root; cf. #252/#256).Minimal reproduction
repro.rs— no&mut, no annotations, a supported container holding a supported struct:*const u8isalloc::raw_vec::RawVecInner's pointer field — Thrust walked intoVec's internals instead of using itsSeqmodel. The program is ordinary safe Rust:Adding one line fixes it:
struct Point { x: i64, y: i64 } +impl thrust_models::Model for Point { type Ty = Self; }The same one-liner is what makes
&mut Pointwork:Evidence matrix
All rows
-Adead_code -C debug-assertions=false --edition 2021, thrust @5c74a7f, Z3 5.0.0, default solver config. "no impl" = the program exactly as written; "with impl" = the same program plusimpl thrust_models::Model for P { type Ty = Self; }(resp. for the enum).struct P { x: i64 }/enum E { A(i64), B }unless noted)fn get(p: P) -> i64 { p.x }, by valuesafesafefn get(p: &P) -> i64 { p.x }safesafeimpl P { fn get(&self) -> i64 { self.x } }safesafefn get(e: &E) -> i64 { match e { … } }safesafelet o = Some(P{x:1}); match o { Some(p) => assert!(p.x == 1), … }safe(neg:Unsat)safelet t = (P{x:1}, 2i64); assert!(t.0.x == 1 && t.1 == 2)safe(neg:Unsat)safelet r = &mut p; r.x = 5; assert!(p.x == 5)— borrow created and consumed in one bodysafe(neg:Unsat)safefn set(p: &mut P, n: i64)ty: &'{erased} mut Psafe(neg:Unsat)impl P { fn set(&mut self, n: i64) }ty: &'{erased} mut Psafefn id(p: &mut P) -> &mut P { p }ty: &'{erased} mut Psafefn bump(e: &mut E)(enum)ty: &'{erased} mut Esafeimpl Bump for P { fn bump(&mut self) }(trait method)ty: &'{erased} mut Psafefn f(o: Option<&mut P>)ty: &'{erased} mut Psafefn f(t: (&mut P, i64))ty: &'{erased} mut Psafelet f = |q: &mut P| { q.x = 5; }; f(&mut p);(closure param)ty: &'{erased} mut Psafefn set<T>(r: &mut T, v: T) { *r = v; }called atT = Pty: &'{erased} mut Psafelet mut v = Vec::new(); v.push(P{x:1}); assert!(v[0].x == 1)ty: *const u8safe(neg:Unsat)fn f(v: &Vec<P>) -> i64 { v[0].x }(shared ref)ty: *const u8safefn f(v: &mut Vec<P>) { v[0].x = 5; }ty: &'{erased} mut Vec<P, Global>safev.push(E::A(1))(Vecof a local enum)ty: *const u8safelet b = Box::new(P{x:1}); assert!(b.x == 1)unrefined_ty: *const Psafefn f(s: &[P]) -> i64 { s[0].x }ty: [P]safelet a = [P{x:1}, P{x:2}]; assert!(a[0].x == 1)unrefined_ty: [P; 2_usize]safeControls with a type that does have a
Modelimpl instd.rs— allsafe, no boilerplate needed:let mut v = Vec::new(); v.push(1i64); assert!(v[0] == 1)safelet mut b = Box::new(1i64); *b += 1; assert!(*b == 2)safefn set(r: &mut i64, v: i64) { *r = v; }safefn set<T>(r: &mut T, v: T) { *r = v; }atT = i64safeRows 1-7 are the decisive ones. They show that a local ADT is not unsupported anywhere
buildhas a structural arm — including a mutable borrow of it, as long as that borrow never has to be turned into a type (row 7, which also rejects its negation, so the prophecy machinery is working onPalready). Rows 8-23 are the same ADT in the positions that must go through theModelprojection.Note also the asymmetry between rows 2 and 18: a shared
&Pverifies, but&Vec<P>does not — so this is not a&mut-only story, andVecis not "unsupported": the single variable is whetherP: Modelholds.Root cause
resolve_model_ty(src/refine/template.rs:154-169) tries to normalize theModel::Typrojection and falls back to the original type when that fails, with no diagnostic:Every container model in
std.rsis conditional on the element being a model:and nothing ever supplies
impl Model for Pfor a locally-declaredP. So for a local ADT:&mut P. Bothbuildimpls (TypeBuilder::build,src/refine/template.rs:206-260;TemplateTypeBuilder::build,:392-440) have an arm forTyKind::Ref(_, elem, Mutability::Not)that buildsPointerType::immut_tostructurally, but no arm forMutability::Mut— mutable references are handled only byresolve_model_tyrewriting them tomodel::Mut<..>, whichmodel_adt'smut_model()branch then turns intoPointerType::mut_to. When the rewrite does not fire, the type falls through tounimplemented!("ty: {:?}"). The shared-reference arm shows the structural fallback was intended here; it was just never written forMut.Vec<P>/Box<P>. These areTyKind::Adtanddef.is_struct(), so the unresolved type lands in the struct arm and is expanded field-by-field intoalloc::raw_vec::RawVec→RawVecInner→Unique<u8>→NonNull<u8>→*const u8(resp.Unique<P>→*const P). The abort message therefore names astdprivate field, not the user's type.&[P]/[P; N].TyKind::Slice/TyKind::Arrayhave no arm at all, so they reachunimplemented!directly.Option<P>and(P, i64)escape all of this becausebuildreaches them through its ownis_enum()(nominal) andTyKind::Tuplearms, which never consultModel.Because 62 of the 336 files under
tests/ui/already carryimpl thrust_models::Model for … { type Ty = Self; }by hand, CI never exercises a local ADT without one.Scope / when it bites
Every idiomatic Rust program hits this immediately: a
&mut selfmethod on a user type (rows 9, 12), aVecof user structs (rows 17-19),Box(21) and slices (22-23) of them. It is the first thing encountered when pointing Thrust at real code, and the message gives no hint about the cause. Not a numeric-range/overflow/unsigned issue — every reproducer uses the literals1,2,5.Suggested direction
Give a locally-declared ADT a
Modelinstance rather than requiring the user to write one:resolve_model_ty, when the<T as Model>::Typrojection does not normalize andTis a local ADT, treatTas its own model (type Ty = Self) and re-try the enclosing projection — i.e. make the container impls'T: Modelbound discharge the way the 62 hand-written test impls make it discharge today. This fixes all of rows 8-23 at once, because it restoresVec<P> → Seq<P>,&mut P → Mut<P>, etc.impl Model for P { type Ty = Self; }item for every local ADT the waystd.rsis injected, which is the same thing at the source level.Independently worth doing, since both make the failure mode legible:
TyKind::Ref(_, _, Mutability::Mut)arm to bothbuildimpls, mirroring theMutability::Notarm (PointerType::mut_to(self.build(elem)));def.is_struct()arm from expanding an ADT that carries a#[thrust::def::*]model (Vec,Box, …) — expandingalloc's private fields can only ever produce a confusing abort — and report a proper diagnostic naming the user's type instead.If requiring the impl is deliberate, then at minimum it needs to be in
README.md(it is currently undocumented), and the abort should name the user's type and the missing impl rather than*const u8.Distinct from the known issues
TypeBuilder::build: a struct is always expanded structurally with no cycle cut, so any struct that reaches itself (struct Node { next: Option<Box<Node>> },struct Tree { kids: Vec<Tree> }) aborts the compiler #249 (stack overflow inTypeBuilder::buildfor self-reaching structs) is the otherbuildissue, but its trigger is a type-level cycle and every one of its reproducers already writesimpl thrust_models::Model for S { type Ty = Self; }; here there is no recursion at all (struct Point { x: i64, y: i64 }) and the impl's absence is the trigger.Rvalue::Aggregatefor a newtype struct ignoresModel::Ty, causing call-site sort mismatch #73 (Rvalue::Aggregatefor a newtype struct ignoresModel::Ty) is about a call-site sort mismatch when aModelimpl exists; this is about there being none.EnumDefs, so any struct with an enum-typed field (struct W { o: Option<i32> }) aborts verification #221 (an enum reachable only through an ADT field is never registered) is a registration gap for a type thatbuilddoes handle.thrust_models::…paths, so no item inside amodcan be specified —requires/ensures/param/ret/sig/predicate/invariant!/ghost!all die withE0433#252/Anchor macro-emitted model paths at the crate root #256 are about macro-emittedthrust_models::…paths inside amod; they only make the workaround here harder to spell (impl crate::thrust_models::Model for …), they are not the cause.&mutinto anenum/Optionwhose payload is aVec(anySeq-modeled container) defeats the default solver configuration — a five-lineOption<Vec<i64>>program times out, while the same code with the container in a struct field, or moved out by value, verifies in 0.5 s #263/Incompleteness: a&mutstored in aVec(Seq/array-backed container) never has its prophecy resolved at drop, so safe programs are wrongly rejected asUnsat#202/Unsound:Vecequality (==) is modeled as structural equality of the whole(array, length)representation, so vectors equal in Rust but differing in stale slots pastlength(afterpop/truncate) compare unequal — dead-branch panics verify assafe#203 concernVec/Seqsemantics once theSeqmodel is in use; here theSeqmodel is never selected at all.Environment
5c74a7fnightly-2025-09-08(perrust-toolchain.toml),--edition 2021x64-glibc-2.39, the version.github/actions/setup-z3pins), defaultTHRUST_SOLVER_ARGS, default 30 s timeout — never reached in the ICE rows