Skip to content

Allow non-return ZST places to appear without proceding defs in BB - #42

Merged
coord-e merged 1 commit into
mainfrom
coord-e/fix-closure-field-1
Feb 7, 2026
Merged

Allow non-return ZST places to appear without proceding defs in BB#42
coord-e merged 1 commit into
mainfrom
coord-e/fix-closure-field-1

Conversation

@coord-e

Copy link
Copy Markdown
Owner

fix#38

Closures without upvars are treated as a ZST. Given the MIR below, _1 is a ZST and is used without proceeding defs in bb0.

// MIR for `main` after runtime-optimizedfnmain() -> (){letmut _0:();let _1:S<{closure@closure_field.rs:7:12:7:20}>;letmut _3:&{closure@closure_field.rs:7:12:7:20};letmut _4:(i32,);letmut _5: !;scope1{
debug s => constS::<{closure@closure_field.rs:7:12:7:20}> {{ f:ZeroSized:{closure@closure_field.rs:7:12:7:20}}};let _2:i32;
scope 2{
debug x => _2;}}
bb0:{
_3 = &(_1.0:{closure@closure_field.rs:7:12:7:20});
_4 = (const1_i32,);
_2 = <{closure@closure_field.rs:7:12:7:20}asFn<(i32,)>>::call(move _3,move _4) -> [return: bb1, unwind continue];}bb1:{
switchInt(_2) -> [2: bb2, otherwise: bb3];}bb2:{
return;}bb3:{
_5 = core::panicking::panic(const"assertion failed: x == 2") -> unwind continue;}}

Because _1 has no preceding definitions, bb0 has the type (_0: (), _1: (own (), )) → (), where _1 is left as a live local. However, since _1 is not a function parameter in this case, local_def::Analyzer::elaborate_unused_args needs to ignore _1 within the arguments of bb0.

DEBUG crate:def: thrust::analyze::local_def: assert_entry before expected=({ () | p0 }) → { () | p1 } entry=(_0: (), _1: { (own (), ) | p10 }) → { () | p11 } krate=closure_field def=main
DEBUG crate:def: thrust::analyze::local_def: assert_entry after expected=({ () | p0 }) → { () | p1 } entry=({ (own (), ) | p10 }) → { () | p11 } krate=closure_field def=main
DEBUG crate:def: thrust::rty::subtyping: relate_sub_closed_type got=({ (own (), ) | p10 }) → { () | p11 } expected=({ () | p0 }) → { () | p1 } krate=closure_field def=main
DEBUG crate:def: thrust::rty::subtyping: sub_type got=({ (own (), ) | p10 }) → { () | p11 } expected=({ () | p0 }) → { () | p1 } krate=closure_field def=main
DEBUG crate:def: thrust::rty::subtyping: sub_refined_type got={ () | p0 } expected={ (own (), ) | p10 } krate=closure_field def=main
DEBUG crate:def: thrust::rty::subtyping: sub_type got=() expected=(own (),
) krate=closure_field def=main

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes issue #38 where optimized MIR can contain ZST locals (e.g., no-capture closures stored in struct fields) that are used in bb0 without any preceding definition, which previously caused local_def::Analyzer::assert_entry to fail during entry-type elaboration.

Changes:

  • Add UI regression tests covering calling a closure stored in a struct field (pass + fail cases).
  • Update local_def::Analyzer::elaborate_unused_args to ignore certain locals in START block parameter elaboration.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

FileDescription
tests/ui/pass/closure_field_1.rsAdds a passing regression test for closure-in-struct-field call pattern.
tests/ui/fail/closure_field_1.rsAdds a failing (Unsat) counterpart regression test.
src/analyze/local_def.rsAdjusts entry basic-block parameter elaboration to skip additional locals (intended for ZST-without-def cases).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/analyze/local_def.rs Outdated
Comment on lines 723 to 725
// BBs may use locals without preceeding def when they're ZST
if param_local == mir::RETURN_PLACE || param_local > self.body.arg_count.into() {
subst.extend(

CopilotAIFeb 7, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition param_local > self.body.arg_count.into() skips all non-argument locals, but the comment/PR intent says this should only apply to locals that can legally appear without a preceding definition (i.e., ZSTs like no-capture closures). As written, if a non-ZST local ever appears here, it will be treated as skippable and can break the param index substitution (later refinements may still reference that param). Consider gating this branch on an explicit ZST check (e.g., via tcx.layout_of(...).size == 0 / is_zst) in addition to param_local > arg_count (and keeping RETURN_PLACE as the special case).

Copilot uses AI. Check for mistakes.
Comment threadsrc/analyze/local_def.rs Outdated
@coord-e
coord-eforce-pushed the coord-e/fix-closure-field-1 branch from 3e9396f to e00fe7fCompareFebruary 7, 2026 16:21
@coord-ecoord-e changed the title Allow non-return ZST places to appear without proceeding defs in BBAllow non-return ZST places to appear without proceding defs in BBFeb 7, 2026
@coord-e
coord-e merged commit aa20154 into mainFeb 7, 2026
6 checks passed
@coord-e
coord-e deleted the coord-e/fix-closure-field-1 branch February 7, 2026 16:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Calling a closure in a struct field causes a panic

2 participants

@coord-e