Skip to content

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

Merged
coord-e merged 1 commit intomainfrom
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 intomainfrom
coord-e/fix-closure-field-1

Conversation

@coord-e
Copy link
Owner

@coord-e coord-e commented Feb 7, 2026

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-optimized

fn main() -> () {
    let mut _0: ();
    let _1: S<{closure@closure_field.rs:7:12: 7:20}>;
    let mut _3: &{closure@closure_field.rs:7:12: 7:20};
    let mut _4: (i32,);
    let mut _5: !;
    scope 1 {
        debug s => const S::<{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 = (const 1_i32,);
        _2 = <{closure@closure_field.rs:7:12: 7:20} as Fn<(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

Copy link

Copilot AI left a comment

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.

File Description
tests/ui/pass/closure_field_1.rs Adds a passing regression test for closure-in-struct-field call pattern.
tests/ui/fail/closure_field_1.rs Adds a failing (Unsat) counterpart regression test.
src/analyze/local_def.rs Adjusts 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 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(
Copy link

Copilot AI Feb 7, 2026

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.
@coord-e coord-e force-pushed the coord-e/fix-closure-field-1 branch from 3e9396f to e00fe7f Compare February 7, 2026 16:21
@coord-e coord-e changed the title Allow non-return ZST places to appear without proceeding defs in BB Allow non-return ZST places to appear without proceding defs in BB Feb 7, 2026
@coord-e coord-e merged commit aa20154 into main Feb 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 free to 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

1 participant