Can you detail which tests are you talking about? So I can debug exactly the test and see if the Never and Error type are available.
We have tests in the test suite that use the Never
and Error
types for example. Youâll have to do a search to find them. Hereâs one which uses Never
: swift/associated_type_inference_fixed_type.swift at main ¡ apple/swift ¡ GitHub
Perhaps it's worthwhile to re-think the approach being taken? Instead of concretely storing a Never
even when it is not written in the source code, canonicalize throws Never
to "non-throwing" (if you saw a throws Never
in the source, that means Never
is likely already in scope or there's a type error). Parts of the pipeline that deal with canonical types do not have to worry about handling a Never
value in throws position (maybe if not all of it, hopefully most of it).
This means you no longer need to materialize Never
for function types synthesized when compiling tests that don't use the stdlib; they should just continue to work as if nothing changed.
The major issue is not reading a throws (Never)
statement. The issue is reading for example a non-throwing function, which is virtually treated as a throws (Never)
. So we have serialized the Type ThrowsType
. We have to set some value at any moment that a FuncType
is instantiated.
What if you model the type as an optional, instead of storing Never
if the type is missing?
None
is not really well appreciated by the compiler when checking Type
s
Hmm, another option might be to just store a null type and clients can check for that after asking for the type. Itâs not great though.
Any Type in the system is checked to be a non null pointer at some point in the process. We haven't found neither a way to represent that nor examples of similar behavior in expressions... Do you have something in mind?
Given the idea of âthrows typeâ doesnât already exist, I think it means that it would only be checked in places where you explicitly request for it (in your implementation). Maybe you can store a null type but the getThrowsType
method can return an Optional<Type>
.
That we use Never
there, is an important part of the typed throws concept. Not only does it enable () -> () === () throws (Never) -> ()
, which could probably be modelled using an optional, but also
func foo<E>(_ bar: () throws (E) -> ()) throws (E) {
try bar()
}
try foo { throw SomeError() } // E is SomeError
foo {} // E is Never, foo doesnât throw
Right, but this is just about how itâs represented in the compiler. It would still be possible to do what you wrote (weâd just assume the lack of a type is the same as the type being Never
).
I do think that setting it as Never
would be better as it would simplify a lot of implementation code. If thatâs not possible, then options such as using a bit (to mark whether a type was specified or not), an optional throws type or even a new ThrowsType
type (optionally backed by the actual type) could serve as alternatives.
It might be good to first throughly investigate the feasibility of using Never
before looking at other options. As I mentioned above, it should be possible to do so and if the standard library is not available, then just throw an error.
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 )
This is a curious approach that we haven't thought of, will think about it. Mostly because it is starting almost from scratch (the only thing it can be preserved is all the parsing methodology we have now. It'd be from serialization to beyond.
Thank you very much!
There could be a problem with this approach. How and when do we check, if a generic throws type is Never
and convert it to a Nonthrowing
function?
What I mean is this:
when we have a function like
func foo<E>(_ bar: () throws (E) -> ()) throws (E) {
try bar()
}
it would get the state ThrowInfo::Kind::Typed
. When we call it later on we could supply a non-throwing function for bar
, so that E
is Never
, which should mean, that foo
is non-throwing too, but we already gave it the kind Typed
.
The same also applies to the following case (although it is probably easier to solve):
func foo() throws (Never) {}
Again this function should be semantically the same as writing just
func foo() {}
but we somehow have to convert a function whose kind is ThrowInfo::Kind::Typed
and whose type is Swift.Never
to a function with the kind ThrowInfo::Kind::Nonthrowing
.
when we have a function like
it would get the state
ThrowInfo::Kind::Typed
. When we call it later on we could supply a non-throwing function forbar
, so thatE
isNever
, which should mean, thatfoo
is non-throwing too, but we already gave it the kindTyped
.
That's why I included "whatever implements substitutions" in the list of things that would need to be updated. When you substituted the generic argument, the throw type needs to be adjusted anyways, regardless of whether the substitution is E := Never
or E := Error
or E := MyLibraryError
or something else. The substitution logic needs to make sure that it doesn't set the configuration to Kind::Typed
+ storing a Never
, possibly by using a constructor ThrowInfo(Kind k, Type t)
which makes sure the invariant is upheld.
but we somehow have to convert a function whose kind is
ThrowInfo::Kind::Typed
and whose type isSwift.Never
to a function with the kindThrowInfo::Kind::Nonthrowing
.
So there are two places where this could be done.
- When a
FunctionType
is created. TheFunctionTypeRepr
(which represents user-written code) can still continue to store theNever
explicitly; my understanding (possibly incorrect) is that we consistently use*TypeRepr
s to present types in diagnostics, so this still avoids the problem of presenting something different than what the user wrote. This seems like the easier option to me and allows us to have a stronger invariant (noFunctionType
'sThrowInfo
hasKind::Typed
andthrownType = Never
). - When a
FunctionType
is canonicalized. This would have a weaker invariant, namely that a non-canonicalFunctionType
can haveKind::Typed
andthrownType = Never
but that's not possible for canonicalFunctionType
s (and at a later stageSILFunctionType
s because allSILFunctionType
s are canonical).
One more thing I forgot to add; if you do end up going this route, make sure to update the equality and hashing functions for *ExtInfo(Builder)?
and *ExtInfo
.