Martin Uecker, 2026-10-24
Previously, I discussed a [span type](./2025-07-02.html),
[a vector type](./2025-07-20.html), an optional type
called [maybe](./2025-08-10.html), and a variant type
[variadic](./2025-09-21.html) (now renamed to
`variant` which makes more sense). I experiment with these types in my
[noplate library](https://codeberg.org/uecker/noplate/). A couple
of months ago, I explored how GCC's optimizer in combination with sanitizer can be (mis-)used to prove
[safety properties at compile-time](https://uecker.codeberg.page/2026-05-22.html).
Interestingly, the idea of using sanitizers for proving safety properties is
also used by Anthropic to [verify](https://www.anthropic.com/research/formal-verification)
C code, although there it is applied to the generated RISC-V code.
Today, I want to discuss the design of a generic `result` type.
We want a type that behaves similar to `maybe`, but which can
not be ignored without a warning and which also encodes an error message.
static result(int) divide(int a, int b)
{
if (b == -1 && a == INT_MIN)
return result_fail(int, "overflow in division");
if (b == 0)
return result_fail(int, "division by zero");
return result_success(int, a / b);
}
We start by defining the result type as a generic macro that simply wraps
an algebraic `sum` type that contains the type of the result
and an error type that simply wraps a string.
typedef struct { const char *error; } error_t;
#define result(T) struct [[nodiscard]] result_##T { sum(T, error_t) sum; }
Here, we used the `[[nodiscard]]` attribute that tells the
compiler to emit a warning when the result is ignored when returned
from a function.
Side note: A known limitation of our macro-based generics is that the
type arguments must be typedef names, but this is only a minor
inconvenience which hopefully can be addressed in the future. I still
like to make the following observation at this point. An alternative design that
uses tagless structures can use arbitrary type names, because it does not need
to construct a new tag using token concatenation (`##`). Such types
then always require a single unique declaration before use. But this has a major problem:
Where do you place the declaration for `result(T)` and
`sum(T, error_T)` for various types `T`? These generic
types should be vocabulary types that can freely be used anywhere.
Let's continue constructing `result` by adding two constructors.
#define result_fail(T, e) (result(T)){ sum_init(T, error_t, (error_t){ (e) }) }
#define result_success(T, v) (result(T)){ sum_init(T, error_t, (v)) }
And finally also some accessor functions.
#define result_ok(T, r) (!(r).sum.sel)
#define result_value(T, r) sum_car(r.sum)
#define result_error(T, r) sum_cdr(r.sum)
The `sum` type is simply a tagged union which works similar as
`maybe` as it traps when the inactive member is accessed.
I will write about it another time, as the design of the algebraic
types is not yet completed.
Now let's look how it works in an example ([godbolt](https://godbolt.org/z/7Y8q8szhP)).
int main()
{
int a = 2;
int b = 1;
divide(a, b);
}
: In function 'main':
:34:5: warning: ignoring return value of type 'struct result_int', declared with attribute 'nodiscard' [-Wunused-result]
34 | divide(a, b);
| ^~~~~~~~~~~~~~~~~
:17:20: note: in call to 'divide', declared here
17 | static result(int) divide(int a, int b)
If we ignore the return value, we get the expected warning. Of course,
`divide` is a poor example for this, because ignoring the result
is not a problem. For certain other functions this is very useful.
Now let's consider a complete example ([godbolt](https://godbolt.org/z/491ec3n4T)).
int main()
{
int a = 2;
int b = 1;
result(int) r = divide(a, b);
if (result_ok(int, r)) {
printf("The result is %d.\n", result_value(int, r));
} else {
puts(result_error(int, r).str);
}
}
But is this now safe? Let's try to use the result value in the false branch.
} else {
printf("The result is %d.\n", result_value(int, r));
puts(result_error(int, r).str);
}
If you also change `b` to zero, this will trigger a run-time error.
But what if I want to avoid run-time errors?
Let's try our [previously described trick](https://uecker.codeberg.page/2026-05-22.html)
of re-declaring `__builtin_trap` with the `[[warning("UNSAFE")]]`
attribute. It works! Try it yourself on [godbolt](https://godbolt.org/z/rzYGWMoWK)
by changing the `#if 0` to `#if 1`. This works because because noplate
internally also uses `__builtin_trap>`.
There is still a minor potential issue with the design of the type.
If we add `[[gnu::noinline,gnu::noipa]]`
to the `divide` function, so that the compiler will not look into
it, it will then complain that the argument to `puts` maybe null.
This is actually nice, as it tells us that there might be a run-time issue!
Note, that in this example, without the attributes, it can see that the pointers
to the error strings are not null, but when the `divide` function were
moved into a library, the information about the pointers would be inaccessible to the optimizer.
To avoid this, we could modify the `error_t` type to ensure that the error
string is always initialized in a way the optimizer can see this. For example, a version which uses
the `strv` (string view) type from the noplate library instead of a traditional
C string does not have this problem (the noplate's README contains a link to such an example).
For now, we can just add a check for this. The complete example, that the compiler now
has proven statically safe with regard to signed integer operations, null pointers,
and error handling can be found [here (godbolt)](https://godbolt.org/z/YKfnYdqb7).
Of course, the compile-time check using GCC's optimizer is hack, but it shows what can be
done relatively easily with existing technology already shipped with compilers.
We only need a better interface to it. There is a proposal to add a `compiler_assert`
feature to C ([PDF](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3950.pdf)),
but WG14 was not yet convinced.