Policy for panicking

Not what I meant — I agree that having a new type with restricted functionality makes sense. But the internal representation and precision/range makes less sense to me.

Totally agree with this — a panic is a lot better than undefined behaviour, and still better than C++ exceptions in most ways.

Frankly I think this would normally go the other way — by having to account for all non-compliant cases code would often be slower.

Yes, sorry. That was bad wording on my part. The ordinary case is a promise that you adhere to all the contracts, I agree. I'll see if I can edit and reformulate.

What do you feel about using generics to parameterise the internal storage to the range/precision that you require? A practical default storage type can always be used.

I do think that in an ideal world even index would return a Result, but it makes sense for it not to even if only for the fact that the way it currently works is what everyone coming to rust would expect.

In this case there are multiple correct implementations of the function that doesn't panic though, and I'm really against adding new functionality that's more or less treated as the 'default' way to do a certain thing and then have that not be completely correct when it would be incredibly easy to have it be correct.

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.