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:
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:
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
#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:
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:
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:
if (let x = do_something()) {
// main code with x here
} else {
// error handling here
}
This code quickly leads to spaghetti:
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.