-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Exceptions and error handling #578
Comments
Sorry, didn't read your whole post yet, but I wanted to comment on this paragraph:
This isn't really accurate; maybe the docs need to be updated to make this more clear. Just because You should be just as scared of adding integers together as you are of dereferencing pointers; both can cause undefined behavior, and both are necessary for even very simple applications. |
Yes, I'm not sure why I prefer corrupting data over an actual crash. Both are pretty bad though. Hope it would be possible to disable all those overflow checks on release, once I'm finished debugging and testing. These overflow checks must come at a price of extra conditional jump per every arithmetical operation, I'd prefer to not have those in release. |
It is. Did you read the docs? I actually haven't read them very thoroughly, so I'm not sure, but I think this is answered in the docs. Maybe we should work on making them more accessible or something. |
Yes. They are full of TODOs. |
Yeah, understandable, the docs are not complete yet. But there is information about release modes: |
I'll try to use goto instead of actual exceptions for now, they should be faster anyway. Goto and giant functions probably. |
If possible, please post a link to your use of goto over in #346 when you get something working. We're considering removing goto, and we need compelling usecases for evidence that it should stay. |
if you don't need to restore from exception, compiler don't need to store all register values on exception. if exception is in the same function, compiler can just turn it into goto. It will change instruction pointer and stack pointer. if there is nothing you need to clean up in stack (no destructors), compiler can free it all at once. if there is only one exception destination, and you don't need chaining, compiler can only store old stack pointer and instruction pointer (for an exception handler). The problem with defer is that you can't fire it before the function ends. I've seen a code where two files were freed manually, and for the last one the defer was used. |
Just trying to figure out how errors can be possibly handled. Nesting:
|
Stumbled upon "using exceptions as goto", this seems like the best use of exceptions. This is the one I talk about when I say they can speed program up. https://en.wikibooks.org/wiki/Python_Programming/Exceptions
|
I'd like to do the cheapest case of exception: the multi function goto one. Goto is only possible in one function. If you'll try to goto into the other function, function arguments will be uninitialized, and stack will be corrupted. I keep calling exceptions "multifunction goto", because that's exactly what they are. They can only go back to some function that was already visited, because going forward will leave stack corrupted. Exceptions need to store current stack pointer to go back, and they allow to skip a lot of code at once. But they can only go back. This is an important optimization, you shouldn't ignore it. To do this, I need: on "try":
on "throw":
Will only work if there is nothing important in the local variables (no exception nesting, no "finally"), and you don't need to recover from an "exception". It's very lightweight, because it's literally just 4 operations, unlike usual exceptions which also save every register. I tried googling "c get label address", looks like there is a non-standard feature just for that in gcc. Read here https://stackoverflow.com/questions/1777990/is-it-possible-to-store-the-address-of-a-label-in-a-variable-and-use-goto-to-jum . I don't know how exactly to do this in zig, maybe I should use inline assembly for this? Don't think that exceptions are always super heavy and should be avoided, this is an example of a good use of exception. |
With large functions you can just goto everywhere you want, without all those troubles with exceptions. To use large functions though, deleting variables mid function is almost necessary, it's very uncomfortable overwise. #594 I though that using inline functions will fix it, but you can't goto from them. At least, writing a compiler for it is complicated. I can try to write a simple demonstration of multi-function-goto-exception-like-thing in assembly, should I? I'm trying to compile ffmpeg right now, maybe I'll learn something in the process. |
There is one possibility that exeption will be slower than just error codes. It needs further investigation. I think exceptions will still be faster, if they jump through 5 functions at once. Which is rare. http://www.agner.org/optimize/#manuals optimizing_assembly.pdf 9.6 Jumps and calls
|
It is received wisdom that exceptions in C++ cost much more than function calls or returns. I say "received" because I personally have not done any direct measurements. Exceptions do have to unroll the stack and other fairly heavy things. They are not what I reach for when I am trying to write performant code. |
This is more lightweight than C++ exceptions #578 (comment) Good compiler probably can compile standard exceptions to lightweight ones and even to gotos. Same code, but better implementation. |
You can override the panic behavior by specifying a public panic function in the root source file (next to main): pub fn panic(msg: []const u8) -> noreturn {
// Here is the default implementation:
if (builtin.os == builtin.Os.freestanding) {
while (true) {}
} else {
@import("std").debug.panic("{}", msg);
}
}
It just calls panic. The default implementation of panic (seen above) calls os.abort.
This is the reality of programming. Zig does not cause it; rather it brings the potential issues to attention of programmers.
Panic is unrecoverable. It is
See #632 for the current discussion on error handling. It's looking like we're going to do this, as you said.
We're not going to have exceptions. |
But I still have to close the program after that. I need to take the proper action, and continue working.
Why? |
You can do any behavior that doesn't return from the panic function. Maybe you kill the current thread, which is sorta like the behavior of an uncaught exception in Java. Maybe you longjmp out to a known safe state, which is sorta like a caught exception in Java. Whatever you do, though, it's not going to be pretty. All of these options have problems like not running deferred code. The language is designed so that panics are indicative of programmer error, not runtime user-input error, so panic should never be called. There's no zig-approved recovery strategy from a panic. Whatever you choose to do, you're responsible for the ugliness. And you can't just ignore the panic, because the rest of the calling function depends on panic not returning. This isn't just for when
The short answer is that throw/catch style exceptions do hidden control flow. Instead use explicit Re-reading your OP, it actually sounds like we're talking about different kinds of exceptions. I was thinking Java/JavaScript/C++ -style throw/catch, but you mention hardware exceptions and registering handlers with the OS. If that's what you mean by "exceptions", i'll need to reinterpret this whole discussion. For example implementing a memory watcher that suspends the program in a debugger when an address gets written to: for that you should probably be interacting with the OS (or at least the MMU in some way) about triggering and handling segfaults on the containing page. I don't know if those kinds of exceptions and exception handlers belong in the zig std library or builtin functions or language keywords or leave it to userspace or what. That's an unknown domain to me. |
If I understand correctly, "error" in zig are just one program-wide enum of numbers. Having errors as numbers instead of strings will make good error codes, and will be useful for exit codes (number a program returns after finishing, can be accessed in console and scripts) and exceptions (for filtering exceptions I'm interested in and not interested in). Especially interesting is placing close by meaning error codes close together, so that you can filter them by ranges of numbers.
Program-wide error codes are not as useful as return values that you just pass around, because it breaks abstraction a bit. Error return values just need to explain in short what happened and what to do about it, and it should be only a matter of those two functions. Error return values are just a part of an interface, it should be directly in the function's documentation. Maybe in some cases it's not even necessary to explain what exactly went wrong, just say what something went wrong and raise an exception, to let the programmer fix it.
I expect some trouble when two libraries will have different global error codes that conflict with each other. It would be nice to be able to easily switch between static and dynamic linking in a program, and those global enums may be a hindrance to that.
Another issue, is that I think that simply crashing a program on panic is unacceptable. Program should get programmer's attention on any suspicious operation and just pause the program, giving the programmer a chance to figure out what went wrong, save data, maybe change logic at runtime (which only microsoft's visual studio can, other tools just can't compare with it) to continue without restarting anything. Exceptions are more of a debugging tool than anything, and it's better to have as little of exception handling as possible in the final program, but they are a really useful debugging tool. This has to wait until the work on debugger and ide starts.
Zig uses two kinds of arithmetic operations, a + b with raises exception (actually, it just calls panic and quits) and a +% b which doesn't. It's a scary situation, when everything can potentially crash on any moment. I can take a risk if I run it in the debugger on my own machine, but I'll be afraid to release it.
Good usages of exceptions are long file parsing operations, when file can abruptly end at any time. Jpeglib actually uses exceptions there, setjmp longjump kind. They are pretty much multi-function goto here.
Ideally you should have two versions of jpeglib: one with error handling, and one without, when you are sure no error can appear.
Another good usage is when you want to make parts of your program more independant, when you don't want to let everything crumble just because one part of a system broke.
If you want to get into live coding, you will need exceptions sooner or later. Because hardware itself throws exceptions. You can't guarantee you will never make a division by zero, unless you will do an ugly extra check every time. Debugger api (at least on windows) works very simularly to exception handling, you have to work with same structures for both breakpoints and divisions by zero.
Error handling is the ugliest part of the zig at this point. It adds extra syntax in the language just to get errors. Just returning multiple values would've made things more simple, it would also free you from calling a separate function just to get exact error code (GetLastError and the likes).
One of the forth languages I've seen used an interesting method of checking for errors. It used "number THROW" everywhere. If number is zero, it did nothing.
I'm not sure if setjmp/longjmp is the best way to use exceptions, looks like you can only keep one error handler at the time.
Probably just being able to replace the panic function at runtime is good enough. It should accept error number instead of string, and it should be possible to ignore panic by just returning some value. Need to study how windows exceptions work.
Linux uses different approach to exception handling than windows, because windows method was patented at the time. They still use setjmp/longjmp since then. You probably thought there is some technical reason for not using exceptions, but no, it's just patents and money. I think windows method is more comfortable and secure, but linux is more clean and lightweight (and cumbersome). On windows you can stack exception handlers, create a chain of them, and it's integrated into the os itself. On linux, you can have only one exception handler.
Good link about exception handling http://www.godevtool.com/ExceptFrame.htm , it's 32bit. Among other things, describes how to autoexpand memory by just using exceptions, I want to try that. Since 64bit, windows have a bit different mechanism, details here https://en.wikipedia.org/wiki/Microsoft-specific_exception_handling_mechanisms .
Example of registering exceptions in structured exceptions handling. Probably useless for you, since llvm most likely has it's own ways for it.
push handler_address
push [fs:0]
mov [fs:0],esp
...
pop [fs:0]
add esp,4
tl;dr
Make panic accept a number instead of a string.
Make it possible to ignore panic (it will return to the same place it was called from), pretty much turning panic into throw.
Remove error unions, just return multiple values instead.
Use "error" numbers for exceptions, not for return values.
Once ide and debugger are done, pause on exceptions, and allow to manipulate data and modify code edit-and-continue style (which is probably patented too, don't know).
The text was updated successfully, but these errors were encountered: