You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
catch blocks are not Ok-wrapping their value #41414
cc @rust-lang/lang -- this is an interesting question. The catch { } blocks as written in the RFC would automatically "wrap" their resulting value, so one could do this:
fnmain(){let x:Result<i32,()> = do catch{22};}
However, the catch blocks are implemented do not have this behavior, so one must do:
fnmain(){let x:Result<i32,()> = do catch{Ok(22)};}
What behavior do we want here? I think the currently implemented behavior is natural, in some respects, in that inserting a fn foo() { ... } and fn foo() { do catch { ... } } both behave the same. That is, using a catch block changes the point to which ? propagates, but does not otherwise introduce any "new" behavior.
(Also, the do catch syntax is considered temporary, I'm just using it so that the examples are actually executable with the current implementation.)
To argue the opposite, I suspect the wrapping behaviour will be more convenient & consistent.
Manufactured example where I think the wrapping provides a nice consistency:
let r: Result<(), E> =
do catch {
foo()?;
bar()?;
qux()?;
};
That's certainly better than writing this:
let r: Result<(), E> =
do catch {
foo()?;
bar()?;
qux()?;
Ok(())
};
But I also think it's better than this no-wrapping version, despite this being the shortest of the three:
let r: Result<(), E> =
do catch {
foo()?;
bar()?;
qux()
};
That last call is obviously less consistent syntactically, but it's also less consistent semantically: it requires that qux return a Result<(), E>exactly, instead of just Result<T,F> where E:From<F>.
As for convenience, I'd gladly write the second ? in something like
do catch { x.checked_mul(y)?.checked_add(z)? }
in order to not have to write Some() in something like
do catch { x.checked_abs()? - 1 }
Reacted by Alex Burka, kennytm, Russell Johnston, David Weikersdorfer and Ludwig Stecher
@scottmcm but then by the same token why shouldn't a function that returns a Try type attempt to wrap the final argument? I'm very used to having to write Ok(()) at the end of functions and I feel like I would expect the same thing here.
I would like some way to support "auto-wrapping", indeed, but I worry that the inconsistency will be confusing. I think it'll be hard to remember when you should have Ok() as the final thing and when you can avoid it.
If we are going to have this, I think I would prefer some sort of syntactic "opt-in" that one can use on a fn or a catch in the same way. (The original ? RFC had a throws keyword sort of like this.)
Alternatively, the original RFC (IIRC) also did not have catch { ... }, but rather try { ... } catch { pat => arm }. This did in a sense employ auto-wrap, but in a very particular way, since effectively the result of the try was unified with the type of the catch patterns.
In other words, with try-catch, it would work something like this:
let result_code = try {write!(...)?;0}catch{Err(e) => 1,};
This doesn't really feel like autowrapping to me, though in some desugarings autowrapping may have been involved.
We discussed in the @rust-lang/lang meeting, and I think the general feeling was that it is important to keep catch { x } and fn main() { x } as equivalent. However, we would like to support "auto-wrapping". One thought we had was that a useful coercion would be something like this:
When propagating a value of type T "upward":
either as the tail expression in a catch block
or returning from a function
If the expected type E from context implements E: Try<Ok = T>, then we could do a Try::from_ok() "auto-coercion"
@scottmcm -- would you be interested in working with a @rust-lang/lang member (probably myself) on an RFC to that effect?
I think the general feeling was that it is important to keep catch { x } and fn main() { x } as equivalent.
But now I think I was wrong to argue for that, and that we should revert to the original intent. catch { ... } ought to invoke Try::from_ok on its "natural" return value. This seems quite easy to achieve.
Why, you ask? In short, given that our attempts to find more universal coercions have failed, it seems clear that if we are going to do some form of ok-wrapping, it has to have syntactic opt-in. And, in that case, I don't see a future where catch isn't the block-level form of that opt-in.
In more detailed form:
I think the need for some form of ok-wrapping is still fairly strong:
The subtle but important difference between (e.g.) catch { foo } and catch { Ok(foo?) } is both confusing and annoying -- the latter converts errors, the former doesn't.
the same applies, of course, to any function using ?, but leave that for now
Writing Ok/Some at each return site is readily forgotten, tedious, and (I believe) of little value
this last point can be contenious, I know
However, doing this coercion universally in a type-based fashion is too ambiguous and has far too many hazards, so we need some syntactic opt-in.
It seemed to me that proposal that garnered the most consensus in that thread and still met the objectives was to have some kind of "block-level" opt-in, much like catch.
Notably, fn foo() -> T { catch { .. } } is strikingly close to the original goals.
Also, applying to blocks and in the body does a good job of making it clear that this is an implementation detail and doesn't affect the "public interface".
If that gained currency, one can imagine fn foo() -> T catch { ... } being a shorthand.
We would probably want to extend to fn foo() -> T unsafe { ... } (and, I suppose both).
Reacted by kennytm, Pauan and Kornel
Reacted by Árpád Goretity
Reacted by scottmcm, Gábor Lehel, nielsle, Russell Johnston, Pauan, Alisa Sireneva and 1 more
catch blocks are processed during HIR lowering. This works by building on the HIR's support for a break that targets a labeled block carrying a value. So something like
let x = do catch{Ok(foo?)};
desugars to:
let x = 'a:{Ok(match foo {Ok(v) => v,Err(e) => break'a Err(e.into()),})};
The foo? expression, as you can see, has been converted into a match, where the Err arm uses break 'a to target the catch block.
'a: {
...; // any `?` in here will be handled as today
Try::from_ok(tail-expr)
}
This suggests that we want to take the return value from the function lower_block and post-process it, to insert the Try::from_ok call. I think that the existing code will change to something like:
then all we have to do is write this wrap_in_from_ok_call function. For that, we want it to look something like the existing call to from_error, which is generated here:
Activity
nikomatsakis commentedon Apr 20, 2017
cc @rust-lang/lang -- this is an interesting question. The
catch { }blocks as written in the RFC would automatically "wrap" their resulting value, so one could do this:However, the catch blocks are implemented do not have this behavior, so one must do:
What behavior do we want here? I think the currently implemented behavior is natural, in some respects, in that inserting a
fn foo() { ... }andfn foo() { do catch { ... } }both behave the same. That is, using a catch block changes the point to which?propagates, but does not otherwise introduce any "new" behavior.(Also, the
do catchsyntax is considered temporary, I'm just using it so that the examples are actually executable with the current implementation.)scottmcm commentedon Apr 21, 2017
To argue the opposite, I suspect the wrapping behaviour will be more convenient & consistent.
Manufactured example where I think the wrapping provides a nice consistency:
That's certainly better than writing this:
But I also think it's better than this no-wrapping version, despite this being the shortest of the three:
That last call is obviously less consistent syntactically, but it's also less consistent semantically: it requires that
quxreturn aResult<(), E>exactly, instead of justResult<T,F> where E:From<F>.As for convenience, I'd gladly write the second
?in something likein order to not have to write
Some()in something likewithoutboats commentedon Apr 21, 2017
@scottmcm but then by the same token why shouldn't a function that returns a
Trytype attempt to wrap the final argument? I'm very used to having to writeOk(())at the end of functions and I feel like I would expect the same thing here.scottmcm commentedon Apr 21, 2017
@withoutboats I think that'd be a win, yes. The
?RFC even described such a thing as a future possibility (0243-trait-based-exception-handling.md#throw-and-throws) and the newTrytrait proposal is explicitly set up to support both success-wrapping and error-wrapping (rust-lang/rfcs#1859 (comment)).And I strongly hope that
?-in-main(#1937) won't end up resulting in the guessing game tutorial needing anOk(())in order to use?.withoutboats commentedon Apr 21, 2017
I'm a bit sympathetic to the idea but wouldn't that be a very big & breaking change?
nikomatsakis commentedon Apr 21, 2017
@scottmcm
I would like some way to support "auto-wrapping", indeed, but I worry that the inconsistency will be confusing. I think it'll be hard to remember when you should have
Ok()as the final thing and when you can avoid it.If we are going to have this, I think I would prefer some sort of syntactic "opt-in" that one can use on a fn or a
catchin the same way. (The original?RFC had athrowskeyword sort of like this.)Alternatively, the original RFC (IIRC) also did not have
catch { ... }, but rathertry { ... } catch { pat => arm }. This did in a sense employ auto-wrap, but in a very particular way, since effectively the result of thetrywas unified with the type of the catch patterns.In other words, with try-catch, it would work something like this:
This doesn't really feel like autowrapping to me, though in some desugarings autowrapping may have been involved.
nikomatsakis commentedon Apr 28, 2017
We discussed in the @rust-lang/lang meeting, and I think the general feeling was that it is important to keep
catch { x }andfn main() { x }as equivalent. However, we would like to support "auto-wrapping". One thought we had was that a useful coercion would be something like this:T"upward":Efrom context implementsE: Try<Ok = T>, then we could do aTry::from_ok()"auto-coercion"@scottmcm -- would you be interested in working with a @rust-lang/lang member (probably myself) on an RFC to that effect?
scottmcm commentedon May 4, 2017
I'm definitely interested. I'd been pondering what a solution could look like, but wouldn't have dared propose a new coercion.
aturon commentedon May 7, 2017
@scottmcm Great! Ping me or @nikomatsakis on email or IRC to get things cooking?
nikomatsakis commentedon Mar 18, 2018
After the long discussion on internals, I've had a change of heart here. Earlier I wrote:
But now I think I was wrong to argue for that, and that we should revert to the original intent.
catch { ... }ought to invokeTry::from_okon its "natural" return value. This seems quite easy to achieve.Why, you ask? In short, given that our attempts to find more universal coercions have failed, it seems clear that if we are going to do some form of ok-wrapping, it has to have syntactic opt-in. And, in that case, I don't see a future where
catchisn't the block-level form of that opt-in.In more detailed form:
catch { foo }andcatch { Ok(foo?) }is both confusing and annoying -- the latter converts errors, the former doesn't.?, but leave that for nowOk/Someat each return site is readily forgotten, tedious, and (I believe) of little valuecatch.fn foo() -> T { catch { .. } }is strikingly close to the original goals.fn foo() -> T catch { ... }being a shorthand.fn foo() -> T unsafe { ... }(and, I suppose both).nikomatsakis commentedon Mar 19, 2018
Mentoring instructions
catch blocks are processed during HIR lowering. This works by building on the HIR's support for a
breakthat targets a labeled block carrying a value. So something likedesugars to:
The
foo?expression, as you can see, has been converted into amatch, where theErrarm usesbreak 'ato target the catch block.The code to desugar a catch block lives here:
rust/src/librustc/hir/lowering.rs
Lines 2772 to 2775 in aafe7d8
The
with_catch_scopemethod simply pushes the label for the catch block onto a stack, carried in a field of the builder:rust/src/librustc/hir/lowering.rs
Line 105 in aafe7d8
Then when we desugar the
?operator:rust/src/librustc/hir/lowering.rs
Lines 3197 to 3199 in aafe7d8
we can check this stack in the
Errcase and generate abreakif it is non-empty:rust/src/librustc/hir/lowering.rs
Lines 3273 to 3288 in aafe7d8
(We don't need to alter the
?code here, but it's useful to see.)What we want to change here is actually in the
do catchitself. To review, that was this code:rust/src/librustc/hir/lowering.rs
Lines 2772 to 2775 in aafe7d8
It seems like we want to lower:
to the equivalent of
This suggests that we want to take the return value from the function
lower_blockand post-process it, to insert theTry::from_okcall. I think that the existing code will change to something like:then all we have to do is write this
wrap_in_from_ok_callfunction. For that, we want it to look something like the existing call tofrom_error, which is generated here:rust/src/librustc/hir/lowering.rs
Lines 3264 to 3269 in aafe7d8
scottmcm commentedon Mar 25, 2018
I'm going to take a stab at this.
FIXME-laden progress so far: master...scottmcm:catch-wrappingAuto merge of #49371 - scottmcm:catch-wrapping, r=nikomatsakis