Skip to content

Optional else branch - #430

Open
gme-muriuki wants to merge 5 commits into
rust-lang:mainfrom
gme-muriuki:optional_else
Open

Optional else branch#430
gme-muriuki wants to merge 5 commits into
rust-lang:mainfrom
gme-muriuki:optional_else

Conversation

@gme-muriuki

Copy link
Copy Markdown
Contributor

What does this PR do?

Closes #429

How does it work, what questions do you have?

Change the behavior of an if statement requiring an else statement by making the else statement optional through changing the grammar definition for the If statement.
Create a small implementation for the Block in order to create an empty block for desugaring the empty else block.

Add a else_block variable in the judgement_fn! for the if rule to ensure that the block is not an option but either an empty block or a block in nll. Same thing to the codegen judgement_fn!

Change the macro since with the old macro, after else was consumed the parser tried Option which can succeed with either Some(block) or None, so if c {} else {} had two valid parses ( the {} could be a separate block statement totally unrelated with else). Now the block is mandatory when else is seen.

I've also changed the formality core parser regarding the expect_keyword() and expect_keyword_in() functions since they 'ate' the identifier even when not a keyword such that if {} let z .. broke. So fix to restore position on mismatch. I'm not sure if that is right. Is it?

I also did a textual Option<T> detection in macro, Is it ok?

AI disclosure

  • I used an AI tool for research, autocomplete, or in other minimal ways

@rustbot

rustbot commented Jul 9, 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

@rustbot

This comment has been minimized.

Guard hit now parses T mandatorily and wraps in Some; guard miss yields
None. Parsing Option <T> as a nonterminal forked into Some/None after
the keyword, making grammars lke 'if c {..} else {..}' ambiguous.
expect_keyword consumed the identifier even when it didn't match, so a
$:guard miss corrupted the parse position when followed by an identifier
(e.g. a let statement after an else-less if).

Latent until now: existing $:where guards are always followed by
punctuation.

Fix expect_keyword_in the same way for consistency (its only current
caller runs on a discarded clone, so no behavior change there).
Grammar: '"'"'if $condition $then_block $:else $else_block'"'"' with
else_block: Option<Block>. Codegen and borrowck desugar None to an
empty block, so the implicit else path carries the pre-if state to
the join. to_rust omits the else when absent.

Adds borrowck tests (init/move only in then-branch) and a mir_typeck
test.

Closes rust-lang#429
Add "if-no-else" rules to borrowck and codegen that reduce Stmt::If {
else_block: None } to an if with an empty else block.

Update tests
@rustbot

rustbot commented Jul 21, 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.

@tiif tiif left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks! I am not comfortable with reviewing the parsing part, @nikomatsakis's input will be appreciated. For the rest, it looks good to me, I just have two questions.

View changes since this review

Comment on lines 603 to +612
let text0 = self.current_text;
match self.identifier_like_string() {
Ok(ident) if &*ident == expected => Ok(()),
_ => Err(ParseError::at(
skip_whitespace(text0),
format!("expected `{}`", expected),
)),
_ => {
self.current_text = text0;
Err(ParseError::at(
skip_whitespace(text0),
format!("expected `{}`", expected),
))
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't understand the change here 🤔

why are we adding self.current_text = text0; when we already have let text0 = self.current_text; on top? isn't self.current_text = text0; just a no-op?

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.

identifier_like_string() is mutating self.current_text -- it moves the cursor past the identifier before we compare it to expected (which happens in the match). So the self.current_text = text0 is to return it to its original state when the match fails.

(I thought to include it so that if it is a desired behavior we do incorporate it if not, I can rework and get it out.)

Comment thread tests/mir_typeck.rs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's ok to not have this test in mir_typeck, we already have plenty of optional else branch test in tests/borrowck.

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.

That's fine, I'll remove it.

@gme-muriuki

Copy link
Copy Markdown
Contributor Author

Thank you @tiif

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

I'm of two minds. On the one hand, I think these simplifications should take place elsewhere in a-mir-formality (i.e., be handled by an explicit desugaring step).

On the other hand, I think it's convenient, and fairly easy to support this kind of case,so maybe we should keep it. But what do you think about this simpler implementation? I'd be happier if it was "hidden" from the IR.

View changes since this review

condition: Expr,
then_block: Block,
else_block: Block,
else_block: Option<Block>,

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.

Suggested change
else_block: Option<Block>,
else_block: ElseBlock,

You know, we could do this:

#[term]
struct ElseBlock {
    block: Block,
}

impl Default for ElseBlock {
    fn default() -> Self { Block::empty() }
}

and then the rest of the code basically just do else_block.block and doesn't have to know if it was present or not.

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 else branch optional in if statements

4 participants