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

Fix scope of else clause, and avoid duplication #14

Closed
wants to merge 4 commits into from

Conversation

ijackson
Copy link

@ijackson ijackson commented Mar 22, 2021

Hi. I really like this macro. I'm using it heavily. Rust needs lots of early return constructs (Maybe/Error monad, if you like to think of things that way) and this one composes nicely with ? :-).

I'm in the habit of rebinding the same variable name as I go through processing it. This works great in Rust. But there is a problem withif_chain!. Because the else clause is duplicated into the else of each nested if, it gets the wrong variable scope: bindings from earlier if let...; clauses are visible to each copy of the else.

Different copies of the else clause see different variable bindings! Usually these bindings have different types, so the result doesn't compile. See my new test case let_rebinding, which progressively unwraps; that does not compile without my substantive change.

But if the else clause is generic enough or uses into or the rebindings are different values rather than different types, the code can compile and produce (almost certainly) wrong answers.

The fix to this is to have only one copy of the else clause. This can be achieved by threading the return value of the then through as a Some, and unwrapping that and substituting the else for None at the end.

Intended effect of this change

For your edification and/or amusement I wrote a test case that compiles both before and after, and produces different results. This section of the diff demonstrates it:

ijackson@47fac88#r48559776

After this change:

  • The else always sees the original binding of dunno, ie, zero.
  • The first if let Some(dunno) ...; rebinding is not used, as indeed the code layout would seem to suggest.

Undesirable side-effect, and possible alternatives

I have discovered that indirecting the return value through a Some in this way breaks some autoderef that otherwise occurs. So this can break existing code with new type errors. The fix is simple - the sprinkling of new * (in the then clause). That makes this a clear semver break.

A possible alternative would be to try to use loop. That can provide nonlocal exits. However, loop cannot be used as a general facility for building this kind of thing mostly because break () is not legal in a for loop, which means that it is not possible to rethrow an inner break. RFC2046 looks like it would help but wouldn't because it forbids inner breaks.

Edited: Previously I discussed whether this was a breaking change under the assumption that the only effect on code which compiles today is the intended semantic change. However given this type problem this is a breaking change.

Example from my own code

+  let piece: Option<PieceId> = some_datastructure.get(vis)
+  let piece: Option<PieceId> = if_chain! {
+    if let Some(piece) = piece;
+    if let Some(gpc) = gs.pieces.get(piece);
+    if ... [ much elided ];
+    then { None }
+    else { piece }
+  };

In my actual code I have to write if let Some(p) = piece; so that I don't rebind piece.

You may think the above is poor style :-), but there are other similar situations. Anything where any of the if ...; rebinds a variable from the environment which is used in the else.

If you rebind variables in the various ifs, the else should have none
of them in scope.

As a bonus this means that the expansion contains only one copy of the
else clause.

There is a behavioural change in some code, but arguably a bugfix.

Signed-off-by: Ian Jackson <[email protected]>
Iterator::copied() is too now.

Signed-off-by: Ian Jackson <[email protected]>
@ijackson
Copy link
Author

This is worse than I thought. My new approach can confuse the borrow checker into rejecting valid code.

I am going to close this MR for now. Perhaps the loop alternative would be better. I may try that out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant