Let chains
"Let chains" are what we call expressions like if let $pat1 = $expr1 && let $pat2 = $expr2.
In this step, we desugar these. Note that we also support || in let
chains.
In what follows, $expr1/$expr2 are expressions made of &&, ||, let $binding = $place, let binding;, and boolean expressions.
First, the base cases:
#![allow(unused)] fn main() { if let $binding = $place { $then } else { $else } // becomes if true { let $binding = $place; $then } else { $else } }
#![allow(unused)] fn main() { if let $binding; { $then } else { $else } // becomes if true { let $binding; $then } else { $else } }
Then the && case, using block-break to jump over the else branch:
#![allow(unused)] fn main() { if $expr1 && $expr2 { $then } else { $else } // becomes 'exit: { if $expr1 { if $expr2 { break 'exit $then; } } $else } }
And finally the || case, which we specify as follows, with a duplication:
#![allow(unused)] fn main() { if $expr1 || $expr2 { $then } else { $else } // becomes if $expr1 { $then } else if $expr2 { $then // duplicated :/ } else { $else } }
After this step, the only remaining branching construct is if on booleans.
Discussion
Avoiding duplication
We'd like to avoid duplicating user code, especially as in this case this can lead to exponential blowup of code size. The tricky part is preserving drop order, particularly on unwind. This section proposes a solution; it is quite involved, yet it's the simplest I found.
First we get rid of non-place bindings in two steps:
- Move binding declarations to the left by turning every
$bool_expr && let x;intolet x; && $bool_expr; - Move binding declarations out of
||as follows, wherex1is a fresh name. (This usesscope_end!)#![allow(unused)] fn main() { (let x; && $expr1) || $expr2 // becomes let x1; && (let place x = x1 && $expr1 || { scope_end!(x1); true } && $expr2) }#![allow(unused)] fn main() { $expr1 || (let x; && $expr2) // becomes let x1; && ({ scope_end!(x1); true } && $expr1 || let place x = x1 && $expr2) }
This produces new top-level &&-chains, onto which we recursively apply the && case above.
This takes care to declare a series of bindings in the correct order for each branch,
which ensures correct drop order even on unwind.
Now the only bindings left are let place bindings.
We first move these to the right by transforming let place p = $place && $expr
into $expr && let place p = $place.
If $expr mentioned p, we substitute $place in its stead.
This even allows swapping two let place bindings.
By the above the order of the let place bindings is unimportant,
so for any place p that has a let place alias
in both || alternatives, we can write the condition as follows:
#![allow(unused)] fn main() { ($expr1 && let place p = $place1) || ($expr2 && let place p = $place2) }
then transform it using conditional place aliases:
#![allow(unused)] fn main() { let branch; && ($expr1 && { branch = true; true } || $expr2 && { branch = false; true }) && let place p = if_place!(branch, $place1, $place2) }
At the end of this, the remaining ||-chains involve only boolean expressions,
which we can desugar like in Lazy Boolean Operators
without needing to care about binding scopes.
Worked example:
#![allow(unused)] fn main() { if (let Some(a) = foo() && let Some(b) = a.method()) || (let Some(b) = bar() && let Some(a) = b.method()) { .. } // becomes: { let foo_left; let a_left; let method_left; let b_left; let bar_right; let b_right; let method_right; let a_right; let branch; if ({ foo_left = foo(); true } && foo_left.is_some() && { a_left = foo_left.Some.0; true } && { method_left = a_left.method(); true } && method_left.is_some() && { b_left = method_left.Some.0; true } && { branch = true; true } ) || ({ scope_end!(b_left); true } && { scope_end!(method_left); true } && { scope_end!(a_left); true } && { scope_end!(foo_left); true } && { bar_right = bar(); true } && bar_right.is_some() && { b_right = bar_right.Some.0; true } && { method_right = b_right.method(); true } && method_right.is_some() && { a_right = method_right.Some.0; true } && { branch = false; true } ) { let place a = if_place!(branch, a_left, a_right); let place b = if_place!(branch, b_left, b_right); .. } } }
The hoops we have to jump through to avoid duplicating code are not great. The thing we're trying to express is (somewhat) simple, but expressing it in surface Rust is hard.
An alternative to the proposed approach would be to make all the scope ends and unwind paths
explicit beforehand, which would give us full control over
the order in which locals are dropped.
Attempts at writing this in a compositional way have so far failed,
hence the conditional let place approach.
An in-between solution could be a feature to control drop order of bindings dynamically.
Spec-wise we can possibly just keep the version that duplicates; it has the benefit of utmost simplicity.
Backlinks