This was discussed on Zulip where at least two very different use cases for knowing whether something might unwind were identified:
optimizations: e.g., what this intrinsic would solve, the merge-sort example is a good one
correctness
An example for correctness might be something like this:
// Doubles the size of the vector, mapping the first half into the second one
//
// Pre-condition: `f` shall not unwind
fn double_map<F: Fn(i32)->i32>(x: &mut Vec<i32>, f: F) {
if can_panic_unwind(f) { panic!("f can unwind"); }
// Make sure we have enough space
x.reserve(x.len() * 2);
unsafe {
let offset = x.len();
x.set_len(x.len() * 2); // break vector invariants
let ptr = x.as_ptr_mut();
// look ma, no DropGuard-like code because `f` cannot unwind
for i in 0..offset {
x.add(i + offset).write(f(x.add(i).read()));
}
// vector invariants are restored
}
}
I think for this particular example, using this intrinsic would be better than not be able to use anything, but it wouldn't be great because the error due to a pre-condition violation would be a run-time error that depends on compiler optimizations. Ideally, this should be diagnosed at compile-time, as a type error. take_mut is a crate that might use something like this.
Another place where DropGuards are also commonly used is in the standard library, where it hasn't been easy to get them all to be correct.
I don't think adding a feature for solving the "optimization" problem is incompatible with adding features for solving the "correctness" problem, they would just be two different features, like needs_drop vs T: ConstDrop.
I think the discussion really got derailed with the discussion of types. Obviously if a function can panic or not is not part of it's type.
That doesn't make this usecase invalid, the question is how to support it. Today no-panic does something related in a super hacky way. I've used it but it's not simple, and almost anything can panic before the optimizer runs.
The question is what could this look like as a general feature, not as a one off for this perticular problem, and how to do it without affecting any other code not using the feature.
As a strawman I can imagine some sort of construct like this:
Here two functions with identical signatures are supplied. If the line containing "nope!()" can be proven unreachable, agressive_impl is used as written. If it cannot, the implementation is ignored and replaced with a call to safe_impl()
To implement that with a proc macro, would require an API to allow a macro to be able to call back into the compiler. (That may be non-trivial) Additionally the compiler itself would need to understand the nope!() built-in. For compilation purposes it can treat it as a call to an external library. Then it can fail the build if the call is still there when the compilation phase is over.
@HeroicKatora I was thinking a bit more about this today, and was wondering, why can't sort be optimized today ? IIUC, it should be possible for sort to violate this invariant, using a DropGuard that restores it in case of a panic. Is there a reason why using a DropGuard is not possible for sort ?
Perhaps unsurprisingly, the code does use drop guards to do exactly that sort of restoring in case of panics. But my thought was that this possibly adds some overhead to keep track of the drop guard state, in the innermost critical loop. A review of the code reveals that at least the drop guard in the merge code only tracks state that seems functionally required anyways, probably with no overhead or less than I had assumed it might be.
The optimization opportunity might is more obvious in the last Box and Vec::map example that consume parts by-value, I may want to edit the top post.
I see. It would be interesting to know if the DropGuard adds any overhead at all. For the cases for which can_panic_unwind returns false, the Cmp::partial_ord will have the nounwind LLVM-IR attribute, and I would expect for LLVM to remove the overhead of the DropGuard in this case. If LLVM doesn't remove it in some cases, then those are probably LLVM bugs worth filling anyways.
The optimization opportunity might is more obvious in the last Box and Vec::map example that consume parts by-value, I may want to edit the top post.
No, I don't think so (maybe someone finds arcane ways?). Once f(val) has been called, you can not safely fill back val during unwinding since it should have been f's job to decide whether to drop it (or leak if it wants) but having a second instance is a double-drop and only sound if val: Copy. Basically, having unwinding return is similar to instead having a non-unwinding equivalent function that returns a result:
FnOnce(T) -> Result<T, Unwind>;
It is straightforward to see that in the Err case there is no instance of T left to put back.