I think there are two slightly different things here. One is the choice of representation in FunctionTypeRepr. The other is the choice of representation in *FunctionType. I was (incorrectly) assuming the question was about *FunctionType; I realized this after looking at your PR. In any case, that is something that you'll probably need to handle right after you finish working on FunctionTypeRepr. I'm not super familiar with the *TypeRepr types, so I'm not going to comment on that. I've written my thoughts below on changes to *FunctionType.
(Disclaimer: I'm very much biased towards using (exhaustive) switch statements instead of if-else chains.)
Today, we don't store an extra reference to Error per throwing instance of *FunctionType, because it's understood that throwing functions today throw Error. We could follow the same pattern for Never.
00 -> non-throwing == throws Never, no extra storage
01 -> untyped throw == throws Error, no extra storage
10 -> typed throw == throws T, T != Never,
where T is stored in *FunctionType's trailing storage
11 -> unused/illegal
Call-sites which use this can get a "view struct" out of a *FunctionType, like ThrowInfo or something, which reads off the bits and the trailing storage (if necessary) and creating a C++ equivalent of the following Swift enum:
// Swift
enum ThrowInfo {
case nonThrowing
case untypedThrow
case typedThrow(SwiftType)
}
// C++
class ThrowInfo {
enum class Kind : uint8_t {
Nonthrowing,
Untyped,
Typed,
};
Kind kind;
// invariant: thrownType is non-null iff kind == Kind::Typed
Type thrownType;
}
Type ThrowInfo::getThrownType(); // assert if kind != Typed
struct ASTExtInfoBuilder {
// throwing -> extended to 2 bits instead of 1
unsigned bits;
// clangTypeInfo elided...
// new field, may potentially be null
Type thrownType;
}
// Update methods
ASTExtInfoBuilder ASTExtInfoBuilder::withThrowInfo(ThrowInfo info);
ASTExtInfo ASTExtInfo::withThrowInfo(ThrowInfo info);
// materialize ThrowInfo on-demand from various types
ThrowInfo ASTExtInfoBuilder::getThrowInfo();
ThrowInfo ASTExtInfo::getThrowInfo();
ThrowInfo AnyFunctionType::getThrowInfo() { return getExtInfo().getThrowInfo(); }
Now clients can exhaustively switch on a ThrowInfo::Kind (because we have to roll our own ADTs in C++
) and possibly call getThrownType under case ThrowInfo::Kind::Typed.
If instead you just add an extra field of type swift::Type to AnyFunctionType's trailing storage, now clients need to use if-else chains to (a) check Never, then (b) check Error then (c) check the last case, I think that's a less ergonomic API. Also, you need to manually audit places call-sites of ASTExtInfo::isThrowing(); either removing that bit altogether to avoid two sources of truth going out of sync, or make sure that the bit on ASTExtInfo stays in sync with what you stored in the trailing storage.
Lastly, one problem I suspect you're going to encounter is that the thrown type will get dropped in places when new function types are creating by slightly tweaking existing types. (I'm saying this from painful experience working with this recently
) By storing it always, I suspect that you'll probably have to do more plumbing to propagate the thrown type correctly. If you materialize it on-demand (and maybe even bundle it with ExtInfo as above), it might require more LoC overall but it the complexity would be largely contained within the *FunctionType/*ExtInfo/ThrowInfo types, with small updates to:
- type-checking (to make sure you get the right subtyping relation)
- call sites for
ASTExtInfo::withThrows
- whatever implements substitutions
(EDIT: Sorry, replied to the wrong comment. This was meant for @minuscorp )