Policy for panicking

Or it would take an Index that is also a dependent type, i.e. dependent on the run-time (or compile-time) size of the container. Probably too idealistic, though. :smile:

I think a start is to sacrifice a PhD student like what happened with Richard Eisenberg's dissertation Dependent Types in Haskell: Theory and Practice. :wink:

2 Likes

Some sound unchecked indexing has been done; terribly limited, but possible.

Full dependent typing is far off in the future :unicorn: though, if Rust will ever be able to express it.

1 Like

I'd like to subscribe to your News Letter! :grin:

2 Likes

My goodness, @Centril, that paper is so symbol dense that I can only imagine the stupefying sacrifice that was required to bring it into existence. I’m really glad and relieved that I didn’t choose the CS path.

@CAD97 Thanks! Very interesting.

This is way, way, way too strong, since it prohibits things like panicking on integer overflow, which is already the case and very valuable.

(Also, all input values are valid values of their type, since otherwise one is already in the UB case, so the statement also means "one can never panic ever", which is clearly unhelpful.)

:+1: I really like the way <chrono> does this in C++, where you can choose your tradeoffs better.

It doesn't prohibit anything, I didn't say that they must not. But to be more explicit, if a function reasonably can handle all input values, it must do so. It should never be acceptable to panic in the "general-purpose" function when it wouldn't be prohibitively expensive or complicated to properly handle all inputs.

No, it means "one should never panic.". It's an ideal to strive toward whenever possible, not a mandate.

I like the proposal @repax made of being able to “mark” a function as “will-not-panic”. I’m not sure I agree at the moment with the proposed form (i.e. to use a new attribute, when perhaps a simple #[deny(panic)] would suffice), however I do think something should be “improved” in this regard.


My main use-case that prompted my interest in this subject is as follows: I am implementing a Scheme interpreter in Rust, and a large portion of my code is “contract enforcing” and “error conveying”.

Moreover I have to be extra careful to use only non-panicking Rust functions, else all reliability of my interpreter goes down the drain… (Imagine my interpreter being embedded into something like an editor or the like. One user error shouldn’t bring down the whole process…)

Therefore what would be most helpful (at least enough at first) – in my case, and I suspect in many other cases – would be a feature in the rustc (or a cargo tool), that would report which functions use “panicking” code.


Regarding the original posters topic, a guide of when panicking should be allowed, my take would be simple:

  • if possible always have non-panicking variants (usually of the form try_something); panicking variants should be simple wrappers around try_something().unwrap() for convenience;
  • if the inputs could be an invalid, always return Result<T, SomeError>, else always use Option<T>;
  • always panic if something is seriously “wrong” that would hamper the sanity of the process; (especially when dealing with misbehaving system calls;)
1 Like

So, regarding #[effect(no_panic)] or #[deny(panic)], while it may seem simple from a surface syntax perspective, what this implies is an effect system which amounts to no_panic fn (much like const fn), because you have to ensure that the function being type checked can’t call other things that may panic. When you introduce traits this becomes quite hairy. If you don’t have some way to use traits, then the mechanism becomes mostly useless as a immense part of rust becomes unusable.


Let’s try to sketch what such a system might be and think of possible problems…

Thinking in terms of effects, no_panic is a restriction of the base effect impure + partial where we have that panic <: partial, i.e: panic is a sub-effect of partial diverging by a single mechanism (as opposed to other forms of non-termination…).

Now like with const, it is desirable to be polymorphic in the question “can this panic?” such that given:

?no_panic fn twice_ptr<T>(x: T, fun: ?no_panic fn(T) -> T) -> T {
    fun(fun(x))
}

?no_panic fn twice_gen<T, F: ?no_panic Fn(T) -> T>(x: T, fun: F) -> T {
    fun(fun(x))
}

… the bodies of the HoFs above can’t panic by themselves, but if you call twice_ptr with fun : fn(T) -> T, then twice_ptr(x, fun) has the panic effect and if fun : no_panic fn(T) -> T then twice_ptr(x, fun) is also no_panic.

However, this gets complicated once you throw in the generic methods of traits themselves. Take for example: Iterator defined as (relevant parts only…):

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    fn for_each<F>(self, f: F) where F: FnMut(Self::Item);
}

We would like to reuse use this trait and define:

struct Foo;
impl ?no_panic Iterator for Foo {
    type Item = Foo;
    fn next(&mut self) -> Option<Self::Item> { Some(Foo) }
    fn for_each<F>(self, f: F) where F: FnMut(Self::Item) {
        ...
    }
}

This desugars to:

struct Foo;
impl Iterator for Foo {
    type Item = Foo;
    ?no_panic fn next(&mut self) -> Option<Self::Item> { Some(Foo) }
    ?no_panic fn for_each<F>(self, f: F) where F: ?no_panic FnMut(Self::Item) {
        ...
    }
}

This works fine so far. Let’s try something more bold (directly desugared):

struct Bar;
impl Iterator for Foo {
    type Item = Foo;
    no_panic fn next(&mut self) -> Option<Self::Item> { Some(Foo) }
    no_panic fn for_each<F>(self, f: F) where F: no_panic FnMut(Self::Item) {
        ...
    }
}

We have a problem now. Given this definition, if we write:

fn baz<It: Iterator, F: FnMut(It::Item)>(iter: It, fun: F) {
    iter.for_each(fun);
}

Then we have one of two problems:

  1. You can’t call baz with an It: no_panic Iterator because if that were allowed, then you could have executed a panic inside for_each since fun can panic according to the signature of baz. If you did permit it, then the type system would be unsound.
  2. Because calling baz with It: no_panic Iterator can’t be permitted to preserve soundness, we give up the property that T: no_panic Trait ⊢ T: Trait. Giving that property up means less code reuse.

How to deal with this I’m not sure.

EDIT: I hope this was somewhat comprehensible… apologies if it was gibberish :wink:

3 Likes

I think another way of putting this, that works for me mentally, is:

Don't design API's that build-in that the only sensible thing to do in some situations is to force a panic when a slightly different API would make the forced panic unnecessary!

This would make, for example, the question of whether Duration::as_millis() should return an f64, f128, BigInteger, or a Result<T,E> much more clearly decidable. Having it return f64 forces you to have a panic condition, whereas, f128 makes it effectively unnecessary, and BigInteger or Result<T,E> guarantees a forced panic is unnecessary (aside from OOM or other kind of problem completely outside the control of the program). Panics should not be used instead of Result<T,E> (or a better return type) where it can be reasonably done. No, efficiency (in most cases), is not a good enough argument to bake-in guaranteed panics.

That would be my take on it.

1 Like

If we are really considering to introduce a no_panic then I think we should also consider an always_returns. This always_returns would at least imply no_panic and not to contain infinite loops.

It would have some theoretical usage. If True is a type with at least one element and False is a type with zero elements then the function always_returns True->False would not be implementable at all. Indeed one could only implement true propositions as always_returns. It would be almost as having the Calculus of Constructions (see the Coq language) into Rust.

However, I thing that any of these two elements (no_panic and always_returns) would incur in many problems.

I think this always_returns is extremely problematic, because as of today (and to my knowledge) solving the halting problem is NP hard. ( Halting problem - Wikipedia )


I think it would be lovely to have a way to say "this function will definitively return at some point", but I think it's impossible to implement such a thing.

Just take the following simple example (I write in Python as it's easier to see the point):

def read() :
    while True :
        input = input()
        if input == "exit" :
            break

Now this function will definitively "end" once the user enters exit or presses Ctrl+D. However the user -- an evil user that is -- might just keep feeding this program and never letting it "end"...

Therefore, by applying always_returns marker, one might interpret that as one of the following:

  • it will never panic -- thus a synonym for no_panic; (provided that input() does not panic;)
  • it will always return something -- which is silly, as any function does actually "return" something, even if it is () or "void";
  • it will always end because it can be formally proven that the code doesn't loop forever; (which is the case in this example;) however it doesn't state when!

Moreover always_returns doesn't actually imply no_panic because if you mean always_returns as always_terminates, then the function can return a value or panic, and it would still "terminate".

The way this is often handled is to have a non Turing complete sub-language. This can be used to ensure, for example, that a dependent type system is decideable. It's difficult to see how this would work in Rust, though.

It is more than NP-hard, it is undecidable in general, but as @steven099 says it is not necessary to work in general. Indeed the situation is similar for the no_panic. For a generic function containing a panic! it is undecidable to know if there are some input with executes it. However for a function with no panic! statement (and without calls to panicable functions) it is obvious that the function cannot panic. The always_returns requires considerable more restrictions to make obvious the return but it may be done.

Your semantics concerns are not blockers, but just some decisions which should be made to give always_returns a concrete meaning. As I originally stated I would be interested in it meaning no panic and eventually returning for any input. Input/Output could also be forbidden, but I have not thought it thoroughly.

I just thought that as no_panic already introduces multitude of problems. If someone is going to make effort to solve them then they could well get all the way into it and implement always_returns as well. It seems an enormous effort anyway.

Even if there was way to make always_returns work, would it be of any use? What if it returns after 7.5 million years?

Yeah, I think the ideas that would be useful to express would be: Always Diverges vs Never Returns vs Might Return. Always returns seems like something that is pretty meaningless.

I would find it useful for myself. I cannot talk for others.

As a contract similar to no_panic I would find it more useful than Always Diverges, Never Returns, or Might Return.

Besides its consideration as contract it has other interesting properties. Mainly, when types are considered as propositions it would admit implementability only of true propositions. In this way it equates programming with theorem proving. A program would be a proof, as in Coq.

We could try then to exploit the existence of proofs as objects in the language. This would make explicit the contracts of some structures. I think this would only be actually useful if we additionally had dependant types. Then it could be possible to write code like this:

struct BoundedPair{
	x: usize,
	y: usize,
	bound: LESS_THAN(x+y,5),
}

or this:

fn reduce_exploiting_commutativity(list:&Vec<usize>, f:Fn(usize,usize)->usize, proof:COMMUTES(f) ) -> usize{
	...
}

LESS_THAN and COMMUTES would include an always_return in their definition. Just the presence of bound and proof would already give a guarantee about the integrity of the data. One only would operate with them to build proofs for derivate facts.

Would help someone to have proofs embedded in their data structures? Although the hardness of building proofs would scare many people away.

Thus, from the properties that one could want to check in a function, always_returns is for me the one with the deepest meaning.

Possibly it is senseless to implement all of this in Rust. And there are other languages that implement these ideas. But I would never call it pretty meaningless.

1 Like

What keeps me from lying and annotating a function as “Always Returns”? How would the compiler know (for sure) that I was right (halting problem)? Always diverges and might return and never returns are solvable by the compiler on the other hand so the compiler can verify I’m not lying. I guess, if you could only annotate a function with always returns where the compiler can verify that it will actually return (at least at some point) it may be useful, but, it seems unlikely. For example: If I had a function that generated a random u128 and then went into a loop and kept generating random u128’s until there was a match and then broke out of the loop, that function would technically “always return”. It might be the heat death of the universe before it does though. Could the compiler “know” that it could return at some point? Maybe. Would that be useful? Unlikely.

1 Like

Some practical applications of ensuring totality:

1 Like

What keeps me from lying and annotating a functions a “Always Returns”?

A termination checker.

How would the compiler know (for sure) that I was right (halting problem)?

A termination checker, ideally, would be both sound (never allows a non-terminating function) and complete (never forbids a terminating function). This is impossible, however - this is, in fact, the core distinction between the complexity classes R and RE being unequal.

However, you can choose which to give up. Modern termination checkers are sufficiently powerful that they will permit pretty much any deterministically-terminating function you care to come up with. They are sound, and they are close enough to completeness that it is not much of an issue in practice.

For example: If I had a function that generated a random u128 and then went into a loop and kept generating random u128’s until there was a match and then broke out of the loop, that function would technically “always return”.

No, it would not. The probability of returning would approach unity, but it'd never reach it - for example, your RNG might enter a cycle, and never return to that first u128.

"Arbitrarily small possibility of not returning" is still different from "zero possibility of not returning".

4 Likes