Author Topic: One more big thing: Errors  (Read 13124 times)

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
One more big thing: Errors
« on: October 28, 2018, 01:06:13 PM »
With Rust, Go and Zig we have alternatives to try-catch that makes sense in a low level language.

This approach is the one I think has the most things going for it:

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2289.pdf

Truly zero overhead error handling.

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: One more big thing: Errors
« Reply #1 on: October 29, 2018, 01:35:20 AM »
It's a bit tricky getting good syntax for this. Go2 is trying a new syntax themselves. Zig has try/catch which is a bit weird and icky with try in front and catch after.

Something I was trying was this:

Code: [Select]
// init returns void + error
err != smtp.init("mail.example.com");
err !!= smtp.login(); // Conditionally execute what's on the right side if there is no error. Any error is put in err
err !!= smtp.sendMail(msg); // Same
err !!= smtp.close(); // Same

if (err) {
   printf("Failed to send mail: %s", err);
   exit(-1);
}

// With return value
file ! err = File.open_file("foo");
data ! err !!= file.read(b);

i32 value = get_may_throw() !! 0; // Returns 0 in case of error.

It might be good as a starting point for a discussion. It's a tweak of Zig.

Also have a look at the Go2 proposal which actually has echoes of the error handlers of (some versions of) BASIC as well as LISP:

Code: [Select]
type Parsed struct { ... }

func ParseJson(name string) (Parsed, error) {
    handle err {
        return fmt.Errorf("parsing json: %s %v", name, err)
    }

    // Open the file
    f := check os.Open(name)
    defer f.Close()

    // Parse json into p
    var p Parsed
    check json.NewDecoder(f).Decode(&p)

    return p
}

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: One more big thing: Errors
« Reply #2 on: November 03, 2018, 01:34:42 PM »
Since we want seamless interop with C, I'm thinking of two possible ways to do this.

First to recap the solution proposed:

1. For most architectures, we return a struct { union { T value; E error; } } with the carry flag set to discriminate the result on AArch64, ARM, x86 and x64. For other architectures, such as RISC-V, it is put in a dedicated register.

2. To the developer, this actually looks like returning struct { union { T value; E error;  }; bool failed; } where "failed" is set using this register.

3. We introduce some syntactic options that allows branching directly on the flag or on the register for rethrows.

4. For C compatibility we create a version with explicity boolean by funneling it through another function that has the union with the boolean explicit.

Syntax is another matter.

The paper suggests:
Code: [Select]
int some_function(int x) fails(float) {
  // Return failure if x is zero
  if (x == 0) return failure(2.0f);
  return 5;
}

A try macro:
Code: [Select]
fails(float) const char *some_other_function(int x) {
  // If calling some_function() fails, return its failure immediately
  // as if by return failure(some_function(x).error)
  int v = try(some_function(x));
  return (v == 5) ? "Yes" : "No";
}

And a caught macro
Code: [Select]
#define caught(T, E) struct caught_ ## T ## _ ## E { union { T value; E error; }; _Bool failed; }
int main(int argc, char *argv[])
{
  if (argc < 2) abort();
  caught(const char *, float) v = catch(some_other_function(atoi(argv[1])));
  if (!v.failed) {
    printf("v is a successful %s\n", v.value);
  } else {
    printf("v is failure %f\n", v.error);
  }
  return 0;
}

My initial proposal:
Code: [Select]
func i32 !! f32 some_function(i32 x) {
  // Return failure if x is zero
  if (x == 0) fail 2.0;
  return 5;
}

func const char * !! f32 some_other_function(i32 x) {
  int v = some_function(x) !!;
  return v == 5 ? "Yes" : "No";
}

func i32 main(i32 argc, char*argv[])
{
  if (argc < 2) abort();
  char * v = some_other_function(atoi(argv[1])) !! {
    printf("v is failure %f", err);
  } else {
    printf("v is a successful %s\n", v);
  }
  return 0;
}

Note that this syntax is a bit less flexible than that of Zig or the proposal, since "err" here is considered a special variable that will has the type of the last error. Also, we do an escape analysis to see that v is actually not used in case of the error.

As a comparison, consider the last part with a Zig syntax:

Code: [Select]
func i32 main(i32 argc, char*argv[])
{
  if (argc < 2) abort();
  if (some_other_function(atoi(argv[1]))) |char * v|
    printf("v is a successful %s\n", v);
  } else |f32 err| {
    printf("v is failure %f", err);
  }
  return 0;
}

Here escape is prevented by the syntax.

However, Swift has two lessons here, as they started out looking like:

Code: [Select]
if (let x = do_something()) {
  // main code with x here
} else {
  // error handling here
}

This code quickly leads to spaghetti:

Code: [Select]
if (let x = do_something()) {
  if (let y = foo()) {
    if (let z = foo()) {
      // main code with x, y, z here
    } else {
      // error handling (cleanup x, y?)
    }
  } else {
     // error handling (cleanup x?)
  }
} else {
  // error handling
}

In order to avoid this type of code, they introduced a "guard let". I actually made that proposal as feedback to Swift 1 beta, but it didn't arrive until I think Swift 2. There are some other improvements in Swift as well, but it shows the problem.

Anyway, the lesson here is that anything causing nesting is bad. A similar problem occurs with exceptions, but it's more manageable there, since you then can catch each error in a separate clause.

bas

  • Full Member
  • ***
  • Posts: 220
    • View Profile
Re: One more big thing: Errors
« Reply #3 on: November 08, 2018, 11:21:25 AM »
Error handling is THE most  complex thing for languages I think. The best solution I ever saw was for a (real-time) embededded Operating System.
All 'system' calls didn't return success or not, but on failure, just called a pre-registered on-error function. But that is only usable for a truly embedded
solution where some failure probably means reboot or just do nothing. It did make the code immensely better:

Without:
Code: [Select]
buffer* b = malloc(10000);
if (!b) { .. } // error handling
socket* s = open(123);
if (!s) { .. } // error handling

With:
Code: [Select]
buffer* b = malloc(10000);
socket* s = open(123);

And that is even a small example.

For C code, I've used a longjmp mechanism. Most people don't like this, but it goes make the code so much easier
to read/write and if used correctly, it just simplifies things. This also creates something like a normal-path and a separate
error path (just like exceptions and the example above). The Erlang language also has some nice mechanisms to
explicitly register error handlers for specific pieces of code. The problem with all these mechanism however, is that
none of them can be used generally for all problems.

bas

  • Full Member
  • ***
  • Posts: 220
    • View Profile
Re: One more big thing: Errors
« Reply #4 on: November 08, 2018, 11:55:40 AM »
I've read the C part of the proposal mentioned above. I think it's an attempt to introduce a universal mechanism,
for something that simply has no universal solution. Also it's a bit nasty with macros/syntax. It has to be because it has
to deal with existing C code. For C2 that restriction is not there and that is exactly why I believe we can do
better.

Although I don't have a single universal solution in my, I do have multiple non-universal ones that can maybe be
improved by some language support:

  • It would be nice to rid calling code of error handling by placed that in another 'layer'/place. This can currently be
    achieved with longjmp. The programmer must make sure the state remains solid (no mem leakage etc) when jumping.
  • C has a habit of (ab)using special return values for error values. Like value -1 for file handles. I think this is fine for some things. It does
    force a cast sometimes, because the result is signed instead of unsigned to support this
  • For some errors, a larger performance penalty is fine since it's a non-common situation anyways. But for others where
    failure is a common case, the penalty might not be acceptable.
  • returning a tuple (=overhead) for situations where failure is rare, using the 'other-layer' approach might be better.

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: One more big thing: Errors
« Reply #5 on: November 08, 2018, 03:59:04 PM »
The error handler form has advantages and disadvantages. Common LISP has error handlers, and that was also how the primitive error handling worked in more advanced versions of BASIC "ON ERROR GOTO..."

Error handlers are a lot like try/catch, but with less nesting issues. The explicit errors code returns are AWESOME for determining local code flow but propagating / handling errors creates a lot of checks.

But there are two orthogonal things really:

1. How does the syntax look
2. How is it implemented

Let's focus on the implementation first, then discuss syntax:

  • Exceptions - this means in practice that any call might cause unwinding of the stack.
  • Ad hoc (error codes, sending in a variable to hold the error etc)
  • tagged union (struct { bool, union { Result; ErrorCode } })
  • Like tagged union but with bool returned in register/flag
  • return thread local Error in Context (also used with allocator)

I don't like (1) (2). I think the memory overhead of (3) can be an issue.

So I like 4 or 5.

- 4 can use a fallback (and initial implementation) like (3)
- I don't know how straightforward (4) is to implement in LLVM
- 5 costs a memory access, so 4 is cheaper.
- 5 is straightforward to implement in LLVM

bas

  • Full Member
  • ***
  • Posts: 220
    • View Profile
Re: One more big thing: Errors
« Reply #6 on: November 12, 2018, 10:20:39 AM »
There are so many situations (performance/memory-wise) that forcing a single solution in a language lowish-level language
like C2 is a good idea. Of course offering a solution that a developer can choose is fine if this doesn't add too much complexity
to the language overall..

lerno

  • Full Member
  • ***
  • Posts: 247
    • View Profile
Re: One more big thing: Errors
« Reply #7 on: November 13, 2018, 08:47:33 PM »
My guess was that it wouldn't need to add much complexity. But the idea has to be completely fleshed out before knowing for sure. A comfortable and practical syntax is also necessary.