Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guard Patterns #3637

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open

Conversation

max-niederman
Copy link

@max-niederman max-niederman commented May 15, 2024

text/3637-guard-patterns.md Outdated Show resolved Hide resolved
text/3637-guard-patterns.md Show resolved Hide resolved
@kennytm
Copy link
Member

kennytm commented May 15, 2024

the keyword if is currently in the follow-set of a $:pat:

macro_rules! check_pat {
    ($p:pat) => { 1 };
    ($p:pat if $e:expr) => { 2 };
}

fn main() {
    assert_eq!(check_pat!(Some(3) | None), 1);
    assert_eq!(check_pat!(Some(e) if e == 3), 2);
    // assert_eq!(check_pat!((Some(e) if e == 3)), 1); // <- workaround for existing editions
}

this means at minimum this RFC will require a new Edition, or require that $:pat can only match PatternNoTopGuard


The only bindings available to guard conditions are
- bindings from the scope containing the pattern match, if any; and
- bindings introduced by identifier patterns _within_ the guard pattern.
Copy link
Member

Choose a reason for hiding this comment

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

with #2294 (rust-lang/rust#51114) the guard can be an if let clause that introduces binding to the match arm:

#![feature(if_let_guard)]

fn main() {
    match Some(Some(4)) {
        Some(a) if let Some(b) = a => assert_eq!(b, 4),
        _ => unreachable!(),
    }
}

how will if-let guard interact with this RFC?

// error?
(Some(a) if let Some(b) = a.foo()) => using(b),

// error?
Some(a if let Some(b) = a.foo()) => using(b),

// error? assuming the `b` on both branches have the same type
(Some(Some(a)) if let Ok(b) = a.foo()) | (Some(Some(a)) if let Err(b) = a.bar()) => using(b),

// definitely error
(Some(a) if let Some(b) = a.foo()) | None => using(b),

Copy link
Author

Choose a reason for hiding this comment

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

Hmm, I wasn't aware of this feature. If you look in the Future Possibilities section I do mention the possibility of allowing if let, but I hadn't realized it would be necessary for this RFC. I think probably all three of those patterns should be allowed, but to be cautious it might be better to restrict it to work only on the top-level for now.

_ => {
// the user doesn't have enough credit, return an error message
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same example using is expressions:

if plan is Plan::Regular && credit >= 100 || plan is Plan::Premium && credit >= 80 {
  // complete the transaction
} else {
  // return an error
}

Copy link
Member

Choose a reason for hiding this comment

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

that's not the same, the example uses user.subscription_plan() and user.credit() here rather than plan and credit. this rewrite will evaluate plan twice.

plus you don't even need is expression here as there is no binding, matches!(plan, Plan::Regular) is sufficient, and if Plan derives PartialEq you could even use plan == Plan::Regular.

@ehuss ehuss added the T-lang Relevant to the language team, which will review and decide on the RFC. label May 15, 2024
@max-niederman
Copy link
Author

the keyword if is currently in the follow-set of a $:pat:

macro_rules! check_pat {
    ($p:pat) => { 1 };
    ($p:pat if $e:expr) => { 2 };
}

fn main() {
    assert_eq!(check_pat!(Some(3) | None), 1);
    assert_eq!(check_pat!(Some(e) if e == 3), 2);
    // assert_eq!(check_pat!((Some(e) if e == 3)), 1); // <- workaround for existing editions
}

this means at minimum this RFC will require a new Edition, or require that $:pat can only match PatternNoTopGuard

True, forgot to check that. I think I'd prefer making pat fragments match only PatternNoTopGuard to gating it on a new edition, since you can always just wrap the guard in parentheses to invoke a pattern with it.

@programmerjake
Copy link
Member

True, forgot to check that. I think I'd prefer making pat fragments match only PatternNoTopGuard to gating it on a new edition, since you can always just wrap the guard in parentheses to invoke a pattern with it.

I think a better plan is to make :pat match PatternNoTopGuard on edition 2021, and then the next edition (2024 if we're not too late already, otherwise whatever edition comes after that) would have :pat match Pattern. There would also be a :pat_no_top_guard (or some shorter name) that works on all editions.

This means this feature can be stabilized without waiting for a new edition.

This is just like what happened with :pat and :pat_param
https://doc.rust-lang.org/edition-guide/rust-2021/or-patterns-macro-rules.html

@petrochenkov
Copy link
Contributor

This may be a bit vague, but running an arbitrary logic for conditions in patterns sort of goes against the idea of a "pattern".

Pattern is a "leaf" element of a condition and if you need to follow up on it using executable logic, you either

  • open a new block {}, like in if-let
  • or use some sugar to resyntax that block into a chain, ideally like expr is pat(binding) && condition(binding), or with if-let chains as a handicapped variant of that.

(This mostly applies to nonexhaustive matching, but matches involving if guards are by definition non-exhaustive and have to have fallback "else" clauses at some level.)

The subscription_plan example in particular looks like an if-else chain forced into a match, and would look better either without a match, or without if guards.
Basically, my general conclusion is that not everything should be a match, regular ifs and elses are good too.


I have encountered the specific issue of two match arms being the same but not mergeable, but not too often.
Maybe a compromise solution extending match arms but not extending patterns would be suitable (since match arms already allow executable conditions anyway).
There are other situations where it's useful, not involving if guards, for example

match foo {
  A(_want_to_name_this_binding_explicitly_for_exhaustiveness_even_if_it_is_not_used) => {
    // same logic
  }
  B /* no equivalent binding here */ => {
   // same logic
  }
}

@kennytm
Copy link
Member

kennytm commented May 16, 2024

There are other situations where it's useful, not involving if guards, for example

match foo {
  A(_want_to_name_this_binding_explicitly_for_exhaustiveness_even_if_it_is_not_used) => {
    // same logic
  }
  B /* no equivalent binding here */ => {
   // same logic
  }
}

if you need to merge the two branches you could just use a comment instead of a real name?

match foo {
    A(_ /* name this binding explicitly for exhaustiveness */) | B => {
        // logic
    }
}

@max-niederman
Copy link
Author

the keyword if is currently in the follow-set of a $:pat:

macro_rules! check_pat {
    ($p:pat) => { 1 };
    ($p:pat if $e:expr) => { 2 };
}

fn main() {
    assert_eq!(check_pat!(Some(3) | None), 1);
    assert_eq!(check_pat!(Some(e) if e == 3), 2);
    // assert_eq!(check_pat!((Some(e) if e == 3)), 1); // <- workaround for existing editions
}

this means at minimum this RFC will require a new Edition, or require that $:pat can only match PatternNoTopGuard

@kennytm @programmerjake

I've added a section explaining how we can handle this the same way it was handled for or-patterns.

> _PatternNoTopGuard_ :\
> &nbsp;&nbsp; &nbsp;&nbsp; `|`<sup>?</sup> _PatternNoTopAlt_ ( `|` _PatternNoTopAlt_ )<sup>\*</sup>
With `if let` and `while let` expressions now using `PatternNoTopGuard`. `let` statements and function parameters can continue to use `PatternNoTopAlt`.
Copy link
Member

Choose a reason for hiding this comment

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

A for expression also accepts a Pattern, but the pattern is required to be irrefutable.

    for Ok(x) | Err(x) in vec![Ok(5), Err(6), Ok(7), Err(8)] {
        println!("{x}");
    }

I think this be changed to PatternNoTopGuard too?

Expression Irrefutable Current grammar This RFC
let(/else) PatternNoTopAlt PatternNoTopAlt
Function & closure parameter PatternNoTopAlt PatternNoTopAlt
match Pattern MatchArmGuard? Pattern
if/while let Pattern PatternNoTopGuard
for Pattern PatternNoTopGuard

Copy link
Author

Choose a reason for hiding this comment

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

They could be, but wouldn't it be better to give an error saying the pattern must be refutable rather than a parse error?

@dlight
Copy link

dlight commented May 17, 2024

This may be a bit vague, but running an arbitrary logic for conditions in patterns sort of goes against the idea of a "pattern".

I think that allowing guards in nested patterns is a natural step after allowing | in nested patterns, which is done since 1.53.

Guards are already part of the match construct, and allowing it in inner patterns doesn't actually bring a new functionality, it's just syntax sugar. The compiler is presumably allowed to hoist the guards outside the patterns (if they guarantee the guard only runs once), like you would do manually today.

(Outside match, adding an inner guard means just wrapping the whole thing inside an enclosing if; again, just syntax sugar)

The only issue with guards for me is that it trips the exhaustiveness checking (the compiler doesn't know if two or more guards cover all possibilities, so you need to delete the last guard to make it happy), but that's already an issue for the current guard feature in match, and allowing inner guards don't make it any worse.

(There is an analogous issue with bare if: the compiler can't detect if two or more successive if /else if are exhaustive and you need to replace the last one with else, and this is relevant when analyzing control flow)

But anyway I think that this language feature is just syntax sugar and should be approached as such.

@clarfonthey
Copy link
Contributor

This feels like a direct counter to the proposal in #3573: instead of allowing patterns as conditions, allow conditions as patterns. And I'll be frank, for the reasons mentioned, I don't like this.

Adding a condition to a pattern essentially means that the pattern isn't covered at all, and so, it's just adding the pattern and condition together as a single condition, with the minor difference that if the pattern was covered completely in a previous branch, it'd be noticed as dead code.

So... I would much rather have this just as the ability to combine patterns in an if statement beforehand, rather than a way of making match arms more confusing.

@coolreader18
Copy link

Would this allow disjoint sets of bindings? e.g.

(A(x, y) if x == y) | B(x) => ...

To me it seems intuitive that this would be allowed (as it would now have a reason to exist, whereas before there would be no reason to bind a variable in only one branch of an or-pattern). Trying to access x would give a this variable was not bound in all branches error, or maybe allow accessing an x from an outer scope? Not sure about that.

@programmerjake
Copy link
Member

(A(x, y) if x == y) | B(x) => ...

x is bound in both branches, maybe you meant y since that's not mentioned in the B branch?

text/3637-guard-patterns.md Outdated Show resolved Hide resolved
[drawbacks]: #drawbacks

Rather than matching only by structural properties of ADTs, equality, and ranges of certain primitives, guards give patterns the power to express arbitrary restrictions on types. This necessarily makes patterns more complex both in implementation and in concept.

Copy link

Choose a reason for hiding this comment

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

Another drawback: patterns may now have arbitrary side-effects by evaluating expressions. Currently, no kind of pattern executes arbitrary code, and therefore in the current language version, unsafe code could rely on either using a caller-supplied $p:pat inside a macro, or using a pattern containing a pattern macro from another crate, while a safety invariant is broken but unobservable.

This is probably fine as a change because no guarantee has been written, but it rules out deciding later that patterns won't ever have side-effects.

Copy link
Member

Choose a reason for hiding this comment

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

in current language versions, unsafe code can not rely on user-supplied patterns not executing arbitrary code: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=1f3c26c1c0ae0732148ad5987321d2ce
matching against a const may call PartialEq::eq

Copy link
Member

Choose a reason for hiding this comment

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

@programmerjake your sample code generated the indirect_structural_match lint, suggesting this is not intended to be compilable:

warning: to use a constant of type `Vec<u8>` in a pattern, `Vec<u8>` must be annotated with `#[derive(PartialEq)]`
  --> src/main.rs:17:9
   |
17 |         C => {}
   |         ^
   |
   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
   = note: for more information, see issue #120362 <https://github.com/rust-lang/rust/issues/120362>
   = note: the traits must be derived, manual `impl`s are not sufficient
   = note: see https://doc.rust-lang.org/stable/std/marker/trait.StructuralPartialEq.html for details
   = note: `#[warn(indirect_structural_match)]` on by default

basically the manual impl StructuralPartialEq for S violated the definition of structural equality by having a PartialEq that does not compare structurally (also AFAIK StructuralPartialEq is perma-unstable).

Deref pattern rust-lang/rust#87121 is another place that user code may be called during pattern matching, but the hole is sealed by another perma-unstable DerefPure marker trait.

@joshtriplett
Copy link
Member

This seems reasonable. I've wanted this a few times myself, and it's always inconvenient to work around.

It looks like the issue of edition handling has been addressed, and the issue of if let guards is covered in "future work" (which seems sufficient since if let guards aren't stable yet).

Let's see if we have a consensus on this:

@rfcbot merge

@joshtriplett joshtriplett added the I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. label Jun 13, 2024
@rfcbot
Copy link
Collaborator

rfcbot commented Jun 13, 2024

Team member @joshtriplett has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Jun 13, 2024

```rust
// Not allowed:
let x if guard(x) = foo() {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the syntax would be invalid even without the guard, it could make this ambiguous. It'd be perhaps better to demonstrate otherwise-valid syntax, e.g.:

Suggested change
let x if guard(x) = foo() {}
let x if guard(x) = foo() else { loop {} };

while let x if guard(x) = foo() {}

// Allowed:
let (x if guard(x)) = foo() {} // Note that this would still error after parsing, since guard patterns are always refutable.
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps it would be better to demonstrate a valid example, e.g.:

Suggested change
let (x if guard(x)) = foo() {} // Note that this would still error after parsing, since guard patterns are always refutable.
let (x if guard(x)) = foo() else { loop {} };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet