-
Notifications
You must be signed in to change notification settings - Fork 101
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
Implement let_chains. #152
Conversation
Instead of a separate `if_let_expression` construct, we make `if_expression` hold multiple `condition`s, each of which can be a `let_condition`. This also applies to `while let` and `match` pattern guards.
Overall, I really like the way you did this, and I'd like to merge it, since I know this feature is My only worry is that with this change, the It seems possible to address this (by introducing a new /cc @jorendorff - I'm curious about your opinion on the best way to model this. |
Some tests to include:
|
OK, how to model this... I like how this PR models the non-chaining case Now take
|
It'll be stable in 1.64 which is the next release. We've got about a month. |
Good point. This is not handled correctly right now. |
Yeah, I like the So for this code: if a && b let Some(c) = d && e && f {
} You'd get a tree like:
It seems doable. @goffrie Are you up for exploring that direction? |
So I really wanted to avoid allowing "let expressions" in arbitrary places, because they are not really allowed in Rust except in very specific places (you aren't even allowed to put parens around one). But having a separate construct for So I'm tempted to just put let conditions into the expression grammar. Which as you noted, is basically how rustc's parser works anyway. (It also emits a parse-time error in some cases, such as |
Feasibility aside, I don't think I agree with this parse tree. It implies that Also, I have no idea how this might be implemented :) |
Are you saying you wouldn't have the binary expression nodes at all, and instead I don't like that, because it seems unnecessarily inconsistent with the parse tree you'd get if you removed the let condition |
I think the only consistent parse tree would be one of:
|
I don't think we should use the first parse tree, because it makes it much harder to find all of the patterns (which are important because they introduce new bindings). I would be ok with the second parse tree. But I still think the parse tree that I suggested previously would be even better. Why not parse |
That's only correct insofar as Putting it another way: today, in the source |
Yeah, either way (your 2nd proposal or my proposal), the tree would change somewhat when adding a With my proposal, the hierarchy of the binary expressions would change, because you've introduced a Am i missing something? It seem like the behavior of 'expand selection' would be much more consistent if you kept the binary expressions under the let chain, as opposed to removing them all. |
Having said that, I'm fine with doing whichever is simpler from an implementation perspective. |
I implemented my version of the proposal (flattened structure) using dynamic precedence. I'm not at all sure if this is a "good idea", but the idea is that we prefer to parse the condition as just a regular expression (which can't have a let-condition in it), or a single I think all the precedence rules are roughly right now, at least for correct code. Unfortunately, now |
You are correct. For this use case the "nested let_chain" parse would be the most consistent, though (as it would expand the selection to the left, instead of to the right); but I agree with you that it's less practical for other uses. Anyhow, I actually have no idea how to implement your proposal also 😄 |
Oh, I disagree. This would seem super weird to me, as a user. The way you'd typically see this construct formatted is like if first_variable_name.is_pretty_long()
&& second_variable.nothing_to_sneeze_at_either()
&& let Some(cfg) = the_config_record_if_any
&& cfg.enable_rambutans
&& cfg.enable_arugula
{
...
} These should be treated as siblings, imho. I don't think a peculiar nesting will end up having more intuitive behavior in practice, and I think there's some minor risk in picking an interpretation that's so different from how the Rust team thinks of the feature. |
That's the same way that you format nested binary expressions though, and they are structured as a left-associative tree: if first_variable_name.is_pretty_long()
&& second_variable.nothing_to_sneeze_at_either()
&& the_config_record_if_any.is_some()
&& cfg.enable_rambutans
&& cfg.enable_arugula
{
// ...
}
I agree with that, but I thought it was an argument in favor of keeping the binary expressions. Doesn't |
I'm really sorry for letting this sit for a month. I didn't know it was my turn. Fortunately we got a short reprieve, since the feature was bumped back to unstable due to bugs. Rustc uses binary trees, but I worry more about how Rust programmers think about the structure of the code, because (1) we should ideally match what's already in people's brains, and (2) if our AST is idiosyncratic, future evolution of the language is a hazard. Left-associative binary trees vs. vectors in particular seems like a total implementation detail to me; parsers tend to use these almost interchangeably, even within a parser (including Taking up the example again, here's what I think about the possible parses:
|
Yeah, for me, the choice is between 1 (a flat sequence of expressions and let bindings, delimited by if a
&& b
&& c
&& d {
body()
} if let P(x) = a
&& b
&& c
&& d {
body()
} With option 1, the addition of one "let condition" changes the structure of the entire if condition. With option 4, it leaves more of the structure intact. So IDE features like extend selection will work more consistently with option 4 than they will with option 1. Just to clarify @jorendorff - do you agree that this is a benefit of option 4 over option 1? I'm not claiming that this one benefit trumps all other concerns, but I'm not totally clear on whether you're acknowledging this aspect of the tradeoff.
I agree with you about the two different precedence levels of The argument about the "complexity" of the AST is a bit unclear to me. The way that I'd assess the complexity is by thinking from the perspective of writing code (or queries) to consume this syntax tree. From the lense, I don't think that option 4 increases the complexity of that code at all, compared to option 1. Any code that's analyzing a TLDR, for me the decision between options 1 and 4 comes down to:
|
Apologies all day for how long this is...
I agree consistency of IDE behavior is as important a consideration as any other use case. I don't know anything about this IDE feature, so I'm liable to make mistakes trying to reason about it. But here goes.
...into deep nerd territory... One fairly rigorous way to understand "complexity" is in terms of information conveyed to a human when they read the output. That argument goes like this:
There's something subtle going on here where a human user has to look at a small number of examples and infer the behavior of the tool, and that is work for the human. (Maybe I am wrong that this is how normal people learn what kind of output tree-sitter produces, but personally I do this a lot!) They may end up writing exactly the same code to consume either construct, as you say, but they'll have extra bits in their brain afterwards if we choose the offbeat one. Expensive bits! Another way is the Kolmogorov complexity of the output: how well does it compress? The flatter AST would be shorter compressed (or uncompressed, come to think of it). I hesitated before writing "complexity" because, you know, it's pretty subjective. But I think it's correct here. |
Ok, well thanks for taking the time to make that argument. Overall, I do agree with your points about complexity. I still don't really agree about the 'extend selection' issue, but that's ok. The current structure is implemented well, and there are very reasonable arguments that it is the best structure. @goffrie, I spent a little time removing the |
* `if_let_expression` was removed in tree-sitter/tree-sitter-rust#152 * `attributes` were reworked in tree-sitter/tree-sitter-rust#163 (no more `meta-item`)
* `if_let_expression` was removed in tree-sitter/tree-sitter-rust#152 * `attributes` were reworked in tree-sitter/tree-sitter-rust#163 (no more `meta-item`)
Instead of a separate
if_let_expression
construct, we add alet_condition
node that can be the
condition
field of anif_expression
. Furthermore, weadd a
let_chain
node that can hold any number oflet_condition
nodes in achain (as well as regular expression nodes). This also applies to
while let
and
match
pattern guards.