I don’t think you have to do any of this trickery.
Parse it as let <pat> = (<expr> || <expr>);. Have a transformation that depending on context makes the || expand to { let temp = <lhs>; if temp { temp } else { <rhs> } or let <pat> = match <lhs> { <pat> => <pat>, _ => <rhs>.
Or if we’re ok with || only working with Try types rather than with refutable patterns, add impl Try for bool and desugar || to (approx) match <lhs>.as_result() { Ok(t) => typeof::<lhs>::from_ok(t), => <rhs> }.
We have a lot of power to (ab)use || (and &&) since they’re only magic for bool-typed expressions currently. I’m not sure when they’re “desugared”, but it’s definitely “possible” to intertwine it with type resolution, if not ideal. And the Try version doesn’t require it.
In any case, I personally prefer magic semantics for || over magic semantics for let ||.
Edit bad thought:
What keeps us from “just” introducing a new statement of form let <pat> = <expr - ||> $(|| <expr - ||>)* ; that “just” works for bools?
The benefit of parsing it as (let <pat> = <expr>) || expr is that the desired semantics then become a natural consequence of existing precedent:
if let is effectively precedent for treating let as a bool-typed expression. Of course, if let is currently parsed as a special case, but with let as an expression, if let Foo(x) = bar can be retconned as just an if followed by the expression (let Foo(x) = bar).
|| already works with a diverging expression on the right.
The compiler will have to produce an error if you try to use the variables bound in pat when the expression on the right of || doesn’t diverge – but that can be seen as just a special case of the existing initializedness checks. This already works:
let x;
if let Foo(x_) = bar {
x = x_;
} else {
continue
}
println!("{}", x);
But if you replace the continue with a non-diverging expression, the compiler complains about a “borrow of possibly uninitialized variable”, as expected.
Thus the language becomes more uniform overall.
Furthermore, it would naturally enable other boolean expressions involving let, such as this often-requested one:
if let Foo(x) = get_foo() && let Bar(y) = get_bar(x) {
...
}
Or even:
let Foo(x) = get_foo() || let Bar(x) = get_bar() || continue;
although that’s a bit ugly.
That said, I think your first proposal is largely equivalent to mine, other than not supporting the other boolean expressions I just mentioned. You would special-case || rather than changing the overall operator precedence, which avoids the .. problem, but you can do the same thing in the parser – it’s just nicer to keep a consistent precedence. Doing it as a late transformation would allow delaying the decision until after name resolution, so in theory you wouldn’t have the problem with constant boolean-typed patterns – you could allow this to work:
let x1 = Some(3);
let x2 = None;
let y1 = x1 || 10; // y1 == 3
let y2 = x2 || 10; // y2 == 10
let z = x2 || z1; // z == Some(3)
This is perfectly consistent with bool under the type bool = Option<()>; const true: bool = Some(()); const false: bool = None; isomorphism. And it's lazy just like foo() || bar() doesn't call bar if foo returns true. And it has the exact same precedence as it does with bool.
Hehe; my basic idea was that this would "fall out" from let p = e just being an expression and the same mechanism would make if !let p = e work. (let p = e) || continue is then mostly a matter of style.
Yeah, that's reasonable at least as a stopgap solution that may (or not) be relaxed later.
Yup!
Indeed; I'm working on this atm partially for let_chains.
Oh? It's quite significant that if !let would work as well, as it mitigates the "Perlism" issue. But I remember from previous discussions that scoping compared to if let was a sticking point; I'm interested by the idea of using || in part because it sidesteps the scoping issue. How do you intend to address it?
It wouldn’t have to be based on types. In the post you quoted I was trying to explain how it could be done based on the syntactic form of the pattern. But maybe I can be more clear:
If the pattern is a single identifier, let would parse as it currently does. Such patterns are normally irrefutable variable assignments which wouldn’t be useful to use with let-as-expression. They can also be names of constants, in which case they’re generally refutable, but also generally unnecessary to use with let since you can just use == instead. You could also use parentheses in that case.
If the pattern is a struct/enum literal like Foo(bar) or Foo { bar: baz }, let would parse with different precedence – just stronger than &&. This is okay / not a breaking change because the altered precedence would only make a difference in situations that couldn’t possibly have type-checked in the past – with the possible exception of building and then immediately destructuring a Range, which is unlikely to be seen in real code. But it’s not necessary to actually do type-checking to make this determination.
Other types of patterns: Foo::Bar should probably be parsed the old way but there are some counterarguments; tuple, slice, and reference patterns should be parsed the new way; _ should be parsed the old way; ref shouldn’t affect things.