Summary
A value whose sort is Null — a fn(..) pointer or a &str — aborts the analysis when it is stored into an aggregate (tuple or struct) that also carries at least one non-singleton-sorted field.
Env::dependencies drops every local whose sort is_singleton(), so such a local is never registered as a clause variable. The enclosing aggregate, however, is not singleton-sorted (Tuple([Box(Null), Int]) contains an Int), so it survives as a dependency — and its term still references the dropped local. Whichever consumer reaches that reference first panics.
Two panic sites are reachable, depending on whether the field is called:
| trigger | panic |
|---|
calling through the aggregate's fn field | src/chc/clause_builder.rs:113 — unbound var _2 |
| merely holding the aggregate live across a basic-block boundary | src/analyze/basic_block.rs:123 — no entry found for key |
Neither is an unimplemented! on an unsupported construct: both are unwrap-style aborts deep in constraint construction, on programs Thrust otherwise handles.
Minimal reproducers
Calling through a tuple field — unbound var:
fnadd1(x:i64) -> i64{ x + 1}fnmain(){let p:(fn(i64) -> i64,i64) = (add1,3);let a = (p.0)(0);assert!(a == 1);}$ cargo run -- -Adead_code -C debug-assertions=false v1.rsthread 'rustc' panicked at src/chc/clause_builder.rs:113:32:unbound var _2
No call at all, and no function pointer either — a &str beside an i64 is enough:
fnmain(){let p:(&str,i64) = ("hi",3);assert!(p.1 == 3);}$ cargo run -- -Adead_code -C debug-assertions=false v7.rsthread 'rustc' panicked at src/analyze/basic_block.rs:123:84:no entry found for key
Behavior matrix
All with -Adead_code -C debug-assertions=off. Every program is trivially correct.
| Program | Thrust |
|---|
let f: fn(i64)->i64 = add1; let a = f(0); (bare local) | safe ✔ |
let p: (fn(i64)->i64,) = (add1,); let a = (p.0)(0); (fn-only tuple) | safe ✔ |
struct S { f: fn(i64)->i64 } … (s.f)(0) (fn-only struct) | safe ✔ |
let p: (&str, &str) = ("a","b"); let _q = p.1; (all-null tuple) | safe ✔ |
let u = (); let p = (u, 3); assert!(p.1 == 3); (unit beside int) | safe ✔ |
let p: (fn(i64)->i64, i64) = (add1, 3); let a = (p.0)(0); | panicunbound var _2 ❌ |
struct S { f: fn(i64)->i64, n: i64 } … (s.f)(0) | panicunbound var _2 ❌ |
let p: (fn(i64)->i64, i64) = (add1, 3); assert!(p.1 == 3); (no call) | panicno entry found for key ❌ |
let p: (&str, i64) = ("hi", 3); assert!(p.1 == 3); | panicno entry found for key ❌ |
let s = "hi"; let p = (s, 3); assert!(p.1 == 3); | panicno entry found for key ❌ |
let p: (&str, i64, i64) = ("a", 3, 4); assert!(p.1 + p.2 == 7); | panicno entry found for key ❌ |
The fn-only and all-null rows isolate the trigger: it is the mixture of a null-sorted field with a non-singleton one, not the function pointer, the call, or the aggregate kind. A unit field is unaffected.
Root cause
Env::dependencies (src/refine/env.rs) excludes every local of a singleton sort:
pubfndependencies(&self) -> implIterator<Item = (Var, chc::Sort)> + '_{self.locals.iter().map(|(local, rty)| (Var::Local(*local), rty.ty.to_sort())).filter(|(_, s)| !s.is_singleton()).chain(...)}Type::Function and Type::String both lower to chc::Sort::Null, and Sort::is_singleton is true for Null and for Box/Tuple built only from singletons — so a fn(..) local, or a &str local, is never a dependency. That is fine on its own: such a value carries no logical content, and a bare fn(..) local verifies (row 1).
It stops being fine once the value is moved into a mixed aggregate. From the MIR of the first reproducer:
_2 = add1 as fn(i64) -> i64 (PointerCoercion(ReifyFnPointer(Safe), Implicit));
_1 = (move _2, const 3_i64);
_4 = copy (_1.0: fn(i64) -> i64);
_3 = move _4(const 0_i64) -> [return: bb1, unwind continue];
_1 has sort Tuple([Box(Null), Int]), which is not singleton, so _1 is a dependency and its term — which references _2 — reaches clause construction. _2 itself was filtered out. The two consumers then fail in their own way:
type_call → relate_fn_sub_type maps the env's free vars onto clause variables through ClauseBuilder::mapped_var, which panics on the unregistered _2:
thrust::chc::clause_builder::ClauseBuilder::mapped_var
thrust::rty::Formula<RefinedTypeVar<FV>>::map_free_var::{{closure}}
thrust::analyze::basic_block::Analyzer::relate_fn_sub_type
thrust::analyze::basic_block::Analyzer::type_call
type_goto → install_inherited_bb_ty → PrecondCapture::finish builds substs from env.dependencies() and then indexes it for every free var of the captured body, so the missing entry is a bare HashMap index panic:
thrust::analyze::basic_block::PrecondCapture::finish::{{closure}} // substs[&v]
thrust::analyze::basic_block::Analyzer::install_inherited_bb_ty
thrust::analyze::basic_block::Analyzer::type_goto
thrust::analyze::basic_block::Analyzer::type_switch_int
The second path needs only a block boundary, which is why the plain assert!(p.1 == 3) variants abort without any call.
Notes
Environment
- branch
main @ cd33fbf - solver: Z3 5.0.0 (HORN / Spacer)
Summary
A value whose sort is
Null— afn(..)pointer or a&str— aborts the analysis when it is stored into an aggregate (tuple or struct) that also carries at least one non-singleton-sorted field.Env::dependenciesdrops every local whose sortis_singleton(), so such a local is never registered as a clause variable. The enclosing aggregate, however, is not singleton-sorted (Tuple([Box(Null), Int])contains anInt), so it survives as a dependency — and its term still references the dropped local. Whichever consumer reaches that reference first panics.Two panic sites are reachable, depending on whether the field is called:
fnfieldsrc/chc/clause_builder.rs:113—unbound var _2src/analyze/basic_block.rs:123—no entry found for keyNeither is an
unimplemented!on an unsupported construct: both areunwrap-style aborts deep in constraint construction, on programs Thrust otherwise handles.Minimal reproducers
Calling through a tuple field —
unbound var:No call at all, and no function pointer either — a
&strbeside ani64is enough:Behavior matrix
All with
-Adead_code -C debug-assertions=off. Every program is trivially correct.let f: fn(i64)->i64 = add1; let a = f(0);(bare local)let p: (fn(i64)->i64,) = (add1,); let a = (p.0)(0);(fn-only tuple)struct S { f: fn(i64)->i64 } … (s.f)(0)(fn-only struct)let p: (&str, &str) = ("a","b"); let _q = p.1;(all-null tuple)let u = (); let p = (u, 3); assert!(p.1 == 3);(unit beside int)let p: (fn(i64)->i64, i64) = (add1, 3); let a = (p.0)(0);unbound var _2❌struct S { f: fn(i64)->i64, n: i64 } … (s.f)(0)unbound var _2❌let p: (fn(i64)->i64, i64) = (add1, 3); assert!(p.1 == 3);(no call)no entry found for key❌let p: (&str, i64) = ("hi", 3); assert!(p.1 == 3);no entry found for key❌let s = "hi"; let p = (s, 3); assert!(p.1 == 3);no entry found for key❌let p: (&str, i64, i64) = ("a", 3, 4); assert!(p.1 + p.2 == 7);no entry found for key❌The fn-only and all-null rows isolate the trigger: it is the mixture of a null-sorted field with a non-singleton one, not the function pointer, the call, or the aggregate kind. A unit field is unaffected.
Root cause
Env::dependencies(src/refine/env.rs) excludes every local of a singleton sort:Type::FunctionandType::Stringboth lower tochc::Sort::Null, andSort::is_singletonis true forNulland forBox/Tuplebuilt only from singletons — so afn(..)local, or a&strlocal, is never a dependency. That is fine on its own: such a value carries no logical content, and a barefn(..)local verifies (row 1).It stops being fine once the value is moved into a mixed aggregate. From the MIR of the first reproducer:
_1has sortTuple([Box(Null), Int]), which is not singleton, so_1is a dependency and its term — which references_2— reaches clause construction._2itself was filtered out. The two consumers then fail in their own way:type_call→relate_fn_sub_typemaps the env's free vars onto clause variables throughClauseBuilder::mapped_var, which panics on the unregistered_2:type_goto→install_inherited_bb_ty→PrecondCapture::finishbuildssubstsfromenv.dependencies()and then indexes it for every free var of the captured body, so the missing entry is a bareHashMapindex panic:The second path needs only a block boundary, which is why the plain
assert!(p.1 == 3)variants abort without any call.Notes
unimplemented!(unrefined_ty: FnDef(..))when passing a named function (function item) to a higher-order function #140 (unimplemented!(unrefined_ty: FnDef(..))when passing a function item to a higher-order function): here the aggregate holds afn(..)value, and the abort is anunwrap/index panic in constraint construction rather than anunimplemented!.fn(..)pointer value called outside the basic block that created it loses the callee's refinement spec (result is havoc'd), so valid programs verify asUnsat#201 (afn(..)pointer local losing its spec across a block boundary): that is a wrongUnsat, and it does not involve aggregates. A bare fn-pointer local is row 1 above and is fine.inconsistent typeswhen calling a closure in a struct field), which was fixed by Allow non-return ZST places to appear without proceding defs in BB #42.&strreproduces both panics.Type::Neveralso lowers toSort::Nulland is presumably affected the same way, though I did not construct a case for it.Environment
main@cd33fbf