Skip to content

Instantly share code, notes, and snippets.

@Gankra
Created August 13, 2019 20:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Gankra/02417f92f9273f7400cd3a1f3695f50b to your computer and use it in GitHub Desktop.
Save Gankra/02417f92f9273f7400cd3a1f3695f50b to your computer and use it in GitHub Desktop.

- We want a special “error” treatment for a certain argument/result.  A pointer-sized value is passed in an integer register; a different value may be present in that register after the call.  Much like the context treatment, the caller may use the error treatment with a function that doesn’t expect it; this should not trigger undefined behavior, and the existing value should be left in place.  Like the context treatment, this suggests that the error value be passed and returned in a register which is normally callee-save.

For the current logic, there are two interrelated issues:

- The C ABI defines how to map things to registers / stack slots.

- Other language ABI documents (including C++) are typically defined in terms of lowering to the platform’s C calling convention.  Even when the core language is not, the C FFI usually is.

We'll consider two ways to satisfy this.

The first is to pass a pointer argument that doesn't interfere with the normal argument sequence. The caller would initialize the memory to a zero value. If the callee is a throwing function, it would be expected to write the error value into this argument; otherwise, it would naturally ignore it. Of course, the caller then has to load from memory to see whether there's an error. This would also either consume yet another register not in the normal argument sequence or have to be placed at the end of the argument list, making it more likely to be passed on the stack.

The second is basically the same idea, but using a register that's otherwise callee-save. The caller would initialize the register to a zero value. A throwing function would write the error into it; a non-throwing function would consider it callee-save and naturally preserve it. It would then be extremely easy to check it for an error. Of course, this would take away a callee-save register in the caller when calling throwing functions. Also, if the caller itself isn't throwing, it would have to save and restore that register.

Both solutions would allow tail calls, and the zero store could be eliminated for direct calls to known functions that can throw. The second is the clearly superior solution, but definitely requires more work in the backend.

On Joe Groff's Grid Of ABIs: https://github.com/jckarter/jckarter.github.io/blob/master/joe/media/error-abis.pdf

-- columns --
* "normal return" is what it takes to return an "ok" case/normal return, 
* "error return" is what it takes to return an "err" case/throw
* "error propagation" is how an error gets passed up from one stack frame that received an error return to the next caller
* "error catching" is what a caller has to do to handle an error. 
(all taking for granted an ideal situation where there's no cleanup or unwinding necessary)


-- rows --
"Result" tries to show what returning a naive Result<T, U> would look like, which might be in registers or in memory depending on platform ABI rules (it looks like Rust is smart enough at least to always return Result in memory and put the tag first, so you only have to shuffle on propagation when the result type gets bigger)

"swift 4" is the shipped Swift ABI that uses a special data register that's normally caller-preserved, set to zero by the caller, and then checked for nonzero after

"CPS" is a convention i made up where the caller passes two return addresses, and the callee jumps to the nonstandard one on error

"NSError" is the traditional Cocoa convention for explicitly propagating errors, where the return value indicates an error or not, and the function takes an additional pointer argument that the callee writes to with the error if it fails

"swift 3" is the partially-implemented convention from before it had real LLVM support, where instead of using a register it adds an NSError-style out parameter (but checks the pointed-to value for zero instead of the return value)

"HRESULT" is the COM convention, where the return value is the error code, and the successful result is an out param


One thing the table doesn’t cover is that the Swift conventions also try to make it so that nonthrowing function pointers are physical subtypes of throwing ones, so you can convert up from () -> T to () -> Result<T, X> without a thunk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment