Skip to content

fix: reject missing return values - #444

Open
gmemuriuki wants to merge 8 commits into
rust-lang:mainfrom
gmemuriuki:return-validation
Open

fix: reject missing return values#444
gmemuriuki wants to merge 8 commits into
rust-lang:mainfrom
gmemuriuki:return-validation

Conversation

@gmemuriuki

@gmemuriuki gmemuriuki commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

closes #392

Adds return validation that rejects non-unit functions whose bodies can reach end without them returning a value.

How does it work, what questions do you have?

This PR introduces a control flow summary containing:

  • Whether execution can fall through to the next statement.
  • labels targeted by break.
  • labels targeted by continue

The return checker sequentially analyzes statements and blocks:

  • Sequential statements are composed using then.
  • Alternative branches are combined using join.
  • ...

For functions returning (), falling through remains valid. For non-unit functions, validation succeeds only when the function body cannot reach its end. This includes functions where every reachable path returns, as well as loops that cannot exit.

Q: Did this implementation halfway solve what this issue needs? #209

AI disclosure

  • Other: I used an AI to help reason through the control-flow design and test. I wrote the implementation.

@rustbot

rustbot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Thanks for contributing to formality! :)
A reviewer will take a look at your PR within a week or two. If not, come talk to us on https://rust-lang.zulipchat.com/#narrow/channel/402470-t-types.2Fformality

@nikomatsakis nikomatsakis 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.

Take a look @gme-muriuki at the examples I raised

View changes since this review

label: label.clone(),
state: this.current.clone(),
});
if !this.diverged {

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.

This is interesting. We could probably make a good borrow checker test here too, something like

let mut a = 1;
let mut b = 2;

let mut p = &a;
loop {
    break;
    p = &b;
    break;
}

b += 1;
use(p);

I think that before your changes, we would reject this -- but not after.

@gmemuriuki gmemuriuki Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure if this is a good thing or bad that it has changed. But I'll rework the whole approach and preserve the desired behavior.

label: label.clone(),
state: this.current.clone(),
});
if !this.diverged {

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.

we can make a similar test here, I didn't see any test for this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adding it rn 👍

.partition(|lfs| Some(&lfs.label) == scope_label.as_ref());
let mut successor = current;
// A break targeting this scope is live control arriving after it.
let diverged = diverged && this_label.is_empty();

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.

This condition doesn't seem quite right to me. For example

fn foo() {
    let mut x = 1;
    let mut y = 1;
    let mut p = &x;
    'b: {
        'a: {
            break 'b;
        }
        // I think we would conclude "diverged = false" here
        p = &y;
    }
    y += 1; // ...and hence incorrectly report an error here
    use(p);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This made me rethink the entire approach. Take a look and tell me whether it does still fail for this case. Happy to keep iterating.

@rustbot

This comment has been minimized.

Track divergent flow across branches and loops.
Validate implicit `()` against fn output on fallthrough.

fixes rust-lang#392.
@rustbot

rustbot commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@rustbot

This comment has been minimized.

  - track fallthrough, break, and continue outcomes
  - compose sequential and branching control flow
  - model block and loop exits
  - add initial return, break, and continue statement rules and unit
    tests.
  - classify expression, print, and let statements as fallthrough
  - add focused tests for each statement outcome
Propagate fallthrough, break, and continue information through blocks,
conditionals, loops, and existential blocks.

Add tests covering block sequencing, branch joins, loop exists, nested
control flow, and ignoring unreachable control-flow transfers.
Add a return-check judgment that permits unit functions to fall through
and rejects non-unit functions when any path reaches the end of the
body.

Run return validation before borrow checking and add coverage for
accepted and rejected function bodies.
Use the dedicated return-validation pass to detect function fallthrough
instead of tracking divergence in borrow-checker flow state.

Update return-validation diagnostics and add integration coverage for
branches, loops, labels, continues, and existential blocks.
Remove unused directive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make sure we errror for type mismatch when nothing is returned

3 participants