Skip to content

minimally implement is (RFC 3573), sans parsing #144174

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

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

Conversation

dianne
Copy link
Contributor

@dianne dianne commented Jul 19, 2025

This PR partially implements rust-lang/rfcs#3573 for experimentation. It's not yet suitable for general use and may not yet reflect the intended design, but this should hopefully make it easier to explore its design space. r? @joshtriplett for design concerns

Placeholder syntax

Since is isn't parsed as an infix operator, I'm using permanently-unstable builtin# syntax as a placeholder. Instead of expr is pat, write builtin # is(expr is pat) or define a macro expanding to that. One possible improvement if raw keywords (RFC 3098) were implemented would be to use k#is as an infix operator. My understanding is that this syntax is already reserved by RFC 3101.

Scoping

I've opted for a simple and restrictive interpretation of the scoping rules in the RFC, based on the rules for let-chains:

// For `is` in an `&&` operator chain at the top-level of an `if` or `while`'s
// condition, such that a `let` expression would be permitted, `is`'s bindings
// and temporaries are in-scope for the success branch of the condition, as if
// `let pat = expr` were written instead of `expr is pat`.
if mutex.lock().unwrap().test() is true {
    // Like with `if let`, the mutex guard isn't dropped, so it remains locked.
}

// Otherwise, an `is` expression's bindings and temporaries are in scope for the
// remainder of the `&&` operator chain it's in.
if (mutex.lock().unwrap().test() is true) {
    // `let` isn't allowed in parentheses. For this PR's interpretation of `is`,
    // that means it introduced a new scope. The mutex is unlocked here.
}

I don't think this is necessarily the intended scope for is (or the ideal scope for let expressions in that first case, honestly), but my hope is that it will be easier to refine given a concrete implementation1. Lints or errors to prevent shadowing mistakes are left for future work.

Desugaring

builtin # is(expr is pat) is desugared as follows when lowering to HIR:

  • In a condition where a let expression would be permitted, expr is pat desugars to let pat = expr.
  • Anywhere else, the &&-chain containing the is is wrapped in an if condition, then is is desugared to let: ... && expr is pat &&... becomes if ... && let pat = expr && ... { true } else { false }.

This results in non-ideal MIR in some cases, but jump-threading optimizations should hopefully clean it up. I haven't included any tests for that, since this implementation isn't meant for production use. This also doesn't currently enforce Rust 2024 scoping rules for ifs in its desugaring; how to handle older editions is left as an open question.

Behavior in macro expansions

I couldn't find discussion of macros on the RFC, so I've taken the simplest approach: it doesn't really work yet. Similar to let statements but unlike let expressions, you can put builtin # is($e is $p) in a macro and it will introduce its pattern's bindings into scope as if it were inlined into its expansion site. This doesn't work for let expressions because of how macros are parsed: whether let is allowed is determined when parsing, and the macro doesn't have the context of its expansion site to work with, so it doesn't know if it's being expanded into a condition or not. There's one catch though: because of this, most parts of the compiler that work with let expressions assume && operators associate to the left. Since macro expansions sites aren't re-parsed (cc #61733 (comment)), macros expanding to &&-chains containing is operators will break that assumption: putting one of those expansions on the right-hand-side of an && will cause this implementation to panic in check_match (and also likely make some incorrect assumptions in region_scope_tree).

I've also tried to handle attributes on is correctly. There's no tests since it would be impossible to put an attribute directly on an is operator expression currently without parentheses (cc #127436), but it may eventually be possible if attributes can apply to macro expansions (cc #63221).

Feature gate and tracking issue

None yet. I can add those if there's interest in merging these changes. Since the current builtin # is(expr is pat) syntax is permanently unstable, this PR does not stabilize anything.

Footnotes

  1. Since let-chains already exist, I'd be curious if it'd make sense to restrict the scope of is further, to avoid its scope depending on whether it appears in a condition. Possibly some of the questions around shadowing would be easier to resolve too. But it raises some questions around temporary lifetimes and how mixing is and let in &&-chains should work. Alternatively, I'd be happy with shortening the lifetime of let expressions' temporaries by default and keeping is consistent with let. I've been running into some trouble with let temporary lifetimes in designing if let guard patterns too.

@rustbot
Copy link
Collaborator

rustbot commented Jul 19, 2025

joshtriplett is not on the review rotation at the moment.
They may take a while to respond.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-clippy Relevant to the Clippy team. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jul 19, 2025
@rustbot
Copy link
Collaborator

rustbot commented Jul 19, 2025

Some changes occurred in src/tools/clippy

cc @rust-lang/clippy

@rust-log-analyzer

This comment has been minimized.

@rustbot
Copy link
Collaborator

rustbot commented Jul 19, 2025

Some changes occurred in src/tools/rustfmt

cc @rust-lang/rustfmt

@bors
Copy link
Collaborator

bors commented Jul 23, 2025

☔ The latest upstream changes (presumably #143897) made this pull request unmergeable. Please resolve the merge conflicts.

dianne added 2 commits July 24, 2025 06:13
Until it can be parsed properly, `expr is pat` is written
`builtin # is(expr is pat)`.

To be more concise, the included tests use a macro wrapper around it.
Outside of the top level of an `if`/`while` condition,
`... && expr is pat && ...`
becomes
`if ... && let pat = expr && ... { true } else { false }`.
@joshtriplett
Copy link
Member

joshtriplett commented Jul 25, 2025

@dianne Thank you for working on this!

I'm sad that k# didn't go over the finish line and stalled out. I would love to see this shipped as k#is, to allow trying it out with infix syntax, but this shouldn't block on that. (If there's some other way to allow writing it infix, that would be helpful to get a better sense of its value in the experiment, though.)

Regarding scope: The desugaring and scoping rule you have seems fine. I wonder if, rather than translating the containing && chain, you could use super let to translate just the expr is PAT into a block that uses super let to make the bindings in PAT last longer? I'm also wondering if that would allow using the same desugaring elsewhere, rather than special-casing places where a let would be allowed. (I don't know if super let has exactly the right scope, though; this change is only worth doing if it's a simplification.)

I think your logic for when is should bind, and when it shouldn't, is exactly correct.

I've added a bunch of additional test cases, which may help if further playing with the parsing and scoping.

@dianne
Copy link
Contributor Author

dianne commented Jul 25, 2025

I would love to see this shipped as k#is, to allow trying it out with infix syntax, but this shouldn't block on that. (If there's some other way to allow writing it infix, that would be helpful to get a better sense of its value in the experiment, though.)

If all that's needed is placeholder syntax for trying it out, I think there's some options. I'm admittedly not too familiar with the lexer or parser yet, but at a glance, reserved prefixes should make infix syntax with k#is or similar possible in Editions ≥ 2021 without breaking older Editions, and it should be possible to feature-gate it.. I think. I agree that having infix syntax would be more helpful for the experiment; my main reservation is that with raw keywords in limbo it's unclear whether the syntax would have a path to stabilization. Permanently-unstable syntax for experimenting with keywords would be just as useful for my purposes, but that feels like a different proposal to me (maybe more like a compiler MCP than an RFC?).

For stabilizable syntax on current Editions, I couldn't say if there's a way to do it without the raw keywords RFC or equivalent.

I wonder if, rather than translating the containing && chain, you could use super let to translate just the expr is PAT into a block that uses super let to make the bindings in PAT last longer?

Unfortunately, I don't think this works. IIUC, let x = expr is pat; would extend pat's bindings' lifetimes past the statement, and expr is Some(x) && condition(x) would drop x and expr before testing the condition. I think it would be possible to avoid wrapping the whole && chain in an if condition when desugaring, but so far the ways I've thought of doing that require changes further down in the compiler.

I've added a bunch of additional test cases, which may help if further playing with the parsing and scoping.

Thanks! I'll add those to the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-clippy Relevant to the Clippy team. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants