Stream: ideas

Topic: crash


view this post on Zulip Richard Feldman (Oct 03 2022 at 05:38):

I've been thinking more about having a crash keyword (like panic! in Rust, except with a name that more strongly discourages reaching for it) since that discussion last week about why Roc isn't (and shouldn't be) total

view this post on Zulip Richard Feldman (Oct 03 2022 at 05:46):

I've convinced myself that the package ecosystem would most likely be fine in practice if the language had a crash keyword, because:

  1. Rust has panic! but it's successfully become a cultural norm that packages typically base APIs around Result instead of panicking. I think this is in significant part because panic doesn't have try/catch, so Result is actually the nicest way to make an API where callers can do custom error handling. The same incentive structure would be true in Roc.
  2. It's already the case that most Roc code can crash (e.g. heap allocations can fail, stack overflows can happen), so it's not like we can have a "no crash" guarantee anyway. People ought to be aware of that possibility even if it's rare in practice.
  3. Because Roc involves more custom data structures than Elm, it seems to come up more often that having an unreachable equivalent is optimal for performance, and there's value in making that more debuggable than the "build your own crash using overflow" approach

view this post on Zulip Richard Feldman (Oct 03 2022 at 05:52):

so I'm curious what people think of the idea of adding this to the language:

view this post on Zulip Richard Feldman (Oct 03 2022 at 05:55):

what do people think about the idea of adding this keyword to the language?

view this post on Zulip Gabriel Pickl (Oct 03 2022 at 09:32):

Whew, this is actually not easy to answer.
Coming to Roc from Elm (with previous experience in Haskell and Rust among others), I really like the fact that it is hard to crash Elm.
The escape hatches in Elm are basically the same but I have not seen it abused there, and it does not seem to be common to run into stack overflows or infinite loops in practice. I am also not sure how Roc has more custom types that would need failing (or an unreachable) than Elm? I generally use a lot of custom types in Elm and being unable to fail in some cases more often than not made me aware of a weakness in my modeling.
So I was going to write my opinion against adding this keyword, but then...

Thinking about how Roc integrates with platforms, the escape hatches there are much simpler. As far as I understand it, there is nothing stopping you from implementing your own thisDefinitelyWontFail function that calls panic! in the background, and many operations might do this implicitly. Given this, it might not be bad to have one method to panic inside Roc, so the compiler safeguards you mentioned in your post can be implemented there, giving at least some guidance.

I do, however, think that this mainly applies to code written specifically for interacting with a platform. So I would suggest thinking about doing crash somewhat like Elm does ports. Allowed in application code, but not in library code.
This would give library users the confidence that their calls most likely won't spontaneously combust the runtime and, like in Elm, the downsides for modeling seem minor. This seems especially important for a language that might handle resources with the need for cleanup (say, open files) but does not support a drop mechanism for when the runtime panics.

view this post on Zulip Brendan Hansknecht (Oct 03 2022 at 13:45):

Just to give a simple example of a library that would make sense to have crash. I have a Roc flat hash map library that attempts to implement the equivalent of the c++ absl::flat_hasj_map. This is so users could have the option to use an extremely fast hash map. The roc standard library does/will have it's own dictionary, but it will make specific tradeoffs around ordering and hash flooding that will effect performance in some cases. As such, a package with a hash map (or other similar data structure could be quite useful).

With a hash map library, there are many list looks up and checks that can never fail (assuming no bugs). So this is a library where crash makes a lot of sense. Without crash, there are 3 other options:

  1. Use some other way to force a crash. I generally underflow an unsigned number type. (This is the current state)
  2. Dealing with bubbling up results. (This has a performance cost and makes the API much worse).
  3. Use some sort of default value (many cases this may not be possible due to not knowing how it construct one. Even if it is possible, when this happens, you would be hiding bugs)

view this post on Zulip Brendan Hansknecht (Oct 03 2022 at 13:49):

I think generally speaking, when I want crash in an application, it is because I don't feel like dealing with errors or bubbling up errors got complex (lists in lists with results). Occasionally it is a truely impossible state, but that is pretty rare.

When I want crash in a library, it is because a state is unreachable and it doesn't make sense to return an error. Also, handling the case would have more of a performance cost than crashing.

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 13:53):

When I want crash in a library, it is because a state is unreachable and it doesn't make sense to return an error. Also, handling the case would have more of a performance cost than crashing.

This is my thought process too - either this, or the user of my API has used it in a way that breaks the API contract. A rich type system does it make it easier to express an exact API surface and helps make the barrier-to-entry for something like crash high, but in the end all type systems are over-estimations and so you can't always express exactly what you want without allowing for some wiggle room. The wiggle room is where you want to disallow usage at runtime by crashing

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 13:54):

I do feel like the entry cost should be pretty high though. The compiler warnings/restrictions are a good idea, but I think it needs to be a social (ecosystem-driven) thing too to have a certain discipline about what crashing means.

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 13:57):

Another advantage to explicitly crashing, in my eyes, is it makes the DX of handling states you thought were unreachable, or not exposed by your API surface, much, much easier to find. With something like crash the compiler can insert instructions to print back where a crash occurred, and why. By forcing an infinite loop or integer overflow to induce a crash, it's much harder to get that. You can get it to some extent with an expect, but it would be a silent notification by default - you'd need to set up log collection and alerts on logs to see when an expect did not meet its condition.

view this post on Zulip Brendan Hansknecht (Oct 03 2022 at 14:09):

I would question whether an API contract should be an expect or a crash, but otherwise totally agree with the sentiment.

view this post on Zulip Tommy Graves (Oct 03 2022 at 14:54):

Does the type system know about crash? E.g. can I write this?

unwrap : Result ok err -> ok
unwrap = \result ->
  when result is
    Ok value -> value
    Err e -> crash e

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 15:09):

I think ideally we would have

crash : Str -> a

so @Tommy Graves your example would work only if err = Str. Or maybe there is some ability like Display (https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/display.20and.20log) for err, and crash is crash : msg -> a | msg has Display

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 15:13):

worth noting you can kind of emulate this today by forcing an infinite loop:

crash : Str -> a
crash = \msg ->
  expect msg != msg

  diverge = \{} -> diverge {}
  diverge {}

but this is silent

view this post on Zulip Tommy Graves (Oct 03 2022 at 15:16):

Oh yeah -- I was less interested in that part and more in making sure the compiler knows the branch with a crash doesn't affect the return type of the function

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 15:23):

yeah if the return type of crash is an unbound type variable it adds no information so it won't make the return type any larger

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 15:24):

» crash : * -> *
…
… unwrap : Result ok err -> ok
… unwrap = \result ->
…   when result is
…     Ok value -> value
…     Err e -> crash e
…
… unwrap

<function> : Result ok err -> ok

view this post on Zulip Richard Feldman (Oct 03 2022 at 17:06):

yeah this is how Elm's old Debug.crash worked

view this post on Zulip Richard Feldman (Oct 03 2022 at 17:06):

I think it would be better to have it be a keyword instead of a first class function though

view this post on Zulip Richard Feldman (Oct 03 2022 at 17:07):

because it doesn't really work like other functions in terms of control flow, which makes it easy to accidentally make mistakes with (e.g. Result.withDefault (crash "blah"))

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 17:09):

Other than making it easier to restrict where you can type it, a keyword vs function would not change the semantics though right

view this post on Zulip Richard Feldman (Oct 03 2022 at 17:33):

right

view this post on Zulip Richard Feldman (Oct 03 2022 at 17:34):

from a type system perspective I think we can treat it like a function with that type

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 17:36):

what if you make the rule really strict, like you can only execute crash as a unary expression in a scope (that is, as the only expression in a function, a when branch or conditional branch)? I think that covers the cases you would actually want to use this.

view this post on Zulip Richard Feldman (Oct 03 2022 at 17:40):

that would mean you couldn't build up a message string using intermediate defs?

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 17:47):

what if it was like expects:

crash
  prefix = ..
  msg = ...
  Str.concat prefix msg

?

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 17:48):

It's not as convenient but maybe it shouldn't be?

view this post on Zulip Qqwy / Marten (Oct 03 2022 at 18:04):

I think this is a good idea. It happens very commonly that e.g. something is constructed using a constant value as parameter, so you know beforehand whether the outcome will be OK.

I have no strong preferences over it being a keyword or a function. I'm not entirely convinced yet that restricting its usage to a particular place (with it as a keyword) really has enough advantages to make it worthwhile. If you have more examples of where it should/shouldn't be allowed to help user friendlyness, I'd be very interested in them.

I do think that we need a longer and more scary name than crash.

view this post on Zulip Qqwy / Marten (Oct 03 2022 at 18:06):

I've always liked the name accursedUnutterablePerformIO for example, (though arguably it has gone slightly too far in its naming :sweat_smile: ).

view this post on Zulip Qqwy / Marten (Oct 03 2022 at 18:07):

That you write panic! in Rust rather than plain panic is another example of helping readers to see that there is something special going on here and that it should not be taken lightly.

view this post on Zulip Qqwy / Marten (Oct 03 2022 at 18:14):

What about a name like unreachableOrCrash?

view this post on Zulip Qqwy / Marten (Oct 03 2022 at 18:22):

A related thing to think about: When we introduce this, we inevitably need a way to recover from them. Not for everyday code, but for:

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 18:25):

I think roc_panic is where the host can handle recovery (e.g. in the web server scenario) as needed. Imo the roc programmer should not have any way to manage recovery here

view this post on Zulip Qqwy / Marten (Oct 03 2022 at 18:31):

Also not for testing?

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 18:35):

Probably roc expect can tell you when a test crashed. But I don't think you'd want to test for a crash occurring; that seems like a smell to me. If you can get your program into such a state in a realistic scenario, you should probably use error handling there?

view this post on Zulip Folkert de Vries (Oct 03 2022 at 18:52):

did we discuss yet how crash would expose evaluation order, and whetehr/how we should deal with that?

e.g if I have

a = crash "foo"
b = crash "bar"

then according to our semantics the result is non-deterministic

view this post on Zulip Qqwy / Marten (Oct 03 2022 at 18:54):

Ayaz Hafiz said:

Probably roc expect can tell you when a test crashed. But I don't think you'd want to test for a crash occurring; that seems like a smell to me. If you can get your program into such a state in a realistic scenario, you should probably use error handling there?

Once you use crash in cases of "someone is using my API wrong", then that is something that should be tested IMHO.

view this post on Zulip Qqwy / Marten (Oct 03 2022 at 18:55):

Folkert de Vries said:

did we discuss yet how crash would expose evaluation order, and whetehr/how we should deal with that?

e.g if I have

a = crash "foo"
b = crash "bar"

then according to our semantics the result is non-deterministic

I'm in the camp of "we should not deal with it" (but of course document that this indeed is non-deterministic).

view this post on Zulip Richard Feldman (Oct 03 2022 at 19:58):

the "ordering semantics" point instantly sold me on Ayaz's proposed restriction on where it can be used

view this post on Zulip Richard Feldman (Oct 03 2022 at 19:58):

it can't come up if you don't allow crash in defs in the first place!

view this post on Zulip Richard Feldman (Oct 03 2022 at 20:00):

I wonder if the rule could be simplified to "you can only use crash immediately after then, else, or ->"

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 20:08):

Hm, it still could come up in the following case, no?

x1 = when y is
          _ -> crash ""
x2 = when y is
          _ -> crash ""

view this post on Zulip Ayaz Hafiz (Oct 03 2022 at 20:09):

even if you say that it can only happen as the sole expression in a function, the problem remains:

crasher = \{} -> crash ""
x1 = crasher {}
x2 = crasher {}

view this post on Zulip Richard Feldman (Oct 03 2022 at 20:18):

hm, yeah true

view this post on Zulip Richard Feldman (Oct 03 2022 at 20:19):

:thinking: could we give a warning about it?

view this post on Zulip Richard Feldman (Oct 03 2022 at 20:21):

I guess this same problem already happens with integer overflow etc

view this post on Zulip Brendan Hansknecht (Oct 03 2022 at 20:26):

If something is going to crash, do we really care about this? Either way it is a crash, the app state is broken, and the platform has do deal with it.

view this post on Zulip Richard Feldman (Oct 04 2022 at 01:56):

yeah maybe it doesn't matter

view this post on Zulip Richard Feldman (Oct 04 2022 at 03:15):

here's a draft PR for crash in the tutorial, to see what it would look like: https://github.com/roc-lang/roc/pull/4190

view this post on Zulip Qqwy / Marten (Oct 04 2022 at 10:30):

As soon as ordering is well-defined, people will start depending on this ordering and we will need to make the compilation process (esp. some of the optimization passes) more complex to ensure that this guarantee is always upheld.

I do not think that it is worth it.

view this post on Zulip Zeljko Nesic (Oct 04 2022 at 12:59):

I have to say that I really don't like crash! in packages!

Yes, we should have some construct like this for iterating purposes, but in the code that we would distribute, I don't know, at least then each package should have GLARING RED each place where they use escape hatches.

view this post on Zulip Zeljko Nesic (Oct 04 2022 at 12:59):

It makes me almost like a hole in a stomach to think that my Roc might have crashes sprinlked around.

view this post on Zulip Zeljko Nesic (Oct 04 2022 at 13:00):

Especially then they can be very non-informative crashes. Like "operation successfully failed, CRASH!"

view this post on Zulip Zeljko Nesic (Oct 04 2022 at 13:00):

UGA BUGA, but why!?!?

view this post on Zulip Zeljko Nesic (Oct 04 2022 at 13:01):

And to enforce nice crash culture i think would be really hard.

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 00:14):

Is there a reason hitting a roc_panic doesn't stack-unwind today? (I couldn't find anything in zulip but I know im missing historical context)

view this post on Zulip Brendan Hansknecht (Oct 06 2022 at 00:17):

No platform has implemented it? They just print and exit. Theoretically the platform would have to deal with it.

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 00:19):

how would the platform implement it? Im pretty sure you would need Roc compiler support

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 00:20):

Namely, I'm referring to making sure reference counts are appropriately decremented when a roc_panic happens. I guess you could do that by chasing through roc_alloc

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:07):

so from what I've read, the general way to do this is using libunwind

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:08):

which exposes C functions you can call to get a backtrace of the current call stack, including function names if the currently running executable has debug info (and function addresses if not)

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:09):

which means it's something hosts can do, and there's an advantage to having it be done in the host which is that it means hosts can decide whether or not they want to pay for walking the current stack to assemble that info

view this post on Zulip Brendan Hansknecht (Oct 06 2022 at 01:11):

Though even if a host walked the currently stack, they wouldn't really have the information to clean up.

view this post on Zulip Brendan Hansknecht (Oct 06 2022 at 01:11):

Really the only way for a host to clean up currently is to use an arena allocator or some separate allocator for roc that they can dump when panic is called

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:12):

right, that ended up being the design intentionally

view this post on Zulip Brendan Hansknecht (Oct 06 2022 at 01:12):

I was thinking of just getting a stack trace to log a better error message, which a host should be able to do

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:13):

basically if hosts want to clean up on panic, they need to implement roc_alloc (and other resources, e.g. file handles - which also can only be created by running host code) to write down a list of what needs cleaning up

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:14):

and then on panic they can traverse that list and perform whatever cleanups are necessary

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 01:15):

Okay so it sounds like the current design is to sweep through roc_alloc

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:15):

doesn't have to be a sweep

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:17):

like for example let's say you implement roc_alloc as a custom variation on malloc that's the same as normal malloc in every way except that it can give you pointers to all the pages it's allocated

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 01:17):

Right, I just mean what you were saying - record somewhere the things that were allocated, and need to be freed eventually

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:17):

ah, in that case, yes!

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 01:17):

Even with libunwind it looks like you need some compiler support. At the very least DWARF info for some platforms. I don't really know much about this but it looks like you need to either use the C ABI everywhere for libunwind to do its thing (with DWARF info) or compile with unwinding directives, e.g. via llvm.

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 01:18):

could very well be wrong though

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 01:18):

these compiler things are all so magical

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:19):

yeah that's correct

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:20):

the compiler needs to output the necessary metadata for libunwind to do its thing, including both llvm and dev backends - although I think llvm might already do it automatically

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:21):

I looked into this a couple of years ago, and I remember concluding this design would work, but I don't remember whether llvm did it automatically or not

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 01:21):

well.. we emit almost no debug information today, even with llvm :sneezing:

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 01:22):

I think you have to instruct llvm to do so explicitly. There are debug and unwind directives that it doesn't look like we inject today

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 01:22):

also with the surgical linker things probably get trickier too

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:26):

as I recall it's something along the lines of storing some metadata at the end (or maybe right before) each function

view this post on Zulip Richard Feldman (Oct 06 2022 at 01:27):

I suspect it'll be the same sort of thing as what we currently do for surgical linking, just with a slightly different format than ELF/Mach-O/COFF

view this post on Zulip Ayaz Hafiz (Oct 06 2022 at 01:29):

yeah both of those make sense, re metadata and the linker. thinking from first principles you probably just need to know the address of the previous stack frame, instruction pointer, and size of the current stack frame. and maybe where to jump in the current stack frame to do your cleanup

view this post on Zulip Brendan Hansknecht (Oct 06 2022 at 01:40):

Yeah, surgical linker will bash debug info and at a minimum lead to it being misaligned. That being said, it should be supportable, just hasn't been done.

view this post on Zulip Richard Feldman (Oct 06 2022 at 02:35):

I haven't tried it, but https://crates.io/crates/backtrace might be a quick way to try it out in the CLI platform

view this post on Zulip Richard Feldman (Oct 06 2022 at 02:37):

and the readme of https://crates.io/crates/mini-backtrace mentions some libunwind-llvm interactions

view this post on Zulip Brian Carroll (Oct 06 2022 at 06:36):

I understand the need to print out a stack trace. That makes sense.
But do we really need to decrement refcounts and free memory? Your process is going to terminate, so the OS will free all of its memory anyway.

view this post on Zulip Richard Feldman (Oct 06 2022 at 07:00):

depends on the platform - for a CLI app, totally! For a webserver, it probably wants to just respond with HTTP 500 for that request and keep handling the others as normal.

view this post on Zulip Richard Feldman (Oct 06 2022 at 07:00):

if an editor plugin crashes, it probably just wants to display an error and continue running all the other plugins as normal

view this post on Zulip Brian Carroll (Oct 06 2022 at 07:10):

Ah, OK. Yes that was an assumption I was making without realising it, thanks!


Last updated: Jun 16 2026 at 16:19 UTC