> On 23 Feb 2017, at 17:19, Joe Groff <[email protected]> wrote:
> 
>> 
>> On Feb 23, 2017, at 12:06 AM, Karl Wagner <[email protected]> wrote:
>> 
>> 
>>> On 22 Feb 2017, at 21:13, Joe Groff <[email protected]> wrote:
>>> 
>>>> 
>>>> On Feb 21, 2017, at 8:50 PM, Chris Lattner via swift-evolution 
>>>> <[email protected]> wrote:
>>>> 
>>>> On Feb 20, 2017, at 11:12 PM, John McCall <[email protected]> wrote:
>>>>>> As you know, I still think that adding typed throws is the right thing 
>>>>>> to do.  I understand your concern about “the feature could be misused” 
>>>>>> but the same thing is true about many other language features.
>>>>> 
>>>>> That's fair, but I do think there's an important difference here.  The 
>>>>> way I see it, typed-throws is really something of an expert feature, not 
>>>>> because it's at all difficult to use, but the reverse: because it's easy 
>>>>> to use without really thinking about the consequences.  (And the benefits 
>>>>> are pretty subtle, too.)  I'm not saying that we should design it to be 
>>>>> hard to use, but I think maybe it shouldn't immediately suggest itself, 
>>>>> and it especially shouldn't come across as just a more specific version 
>>>>> of throws.
>>>> 
>>>> Yeah, I agree that it will be appealing to people who don’t know better, 
>>>> but here’s the thing: the (almost certain) Swift design will prevent the 
>>>> bad thing from happening in practice.
>>>> 
>>>> Consider the barriers Swift already puts in place to prevent the bad thing 
>>>> (declaring an inappropriately narrow explicitly-specified throw signature) 
>>>> from happening:
>>>> 
>>>> 1) First of all, you need to declare a public API.  If it isn’t public, 
>>>> then there is no concern at all, you can evolve the implementation and 
>>>> clients together.
>>>> 
>>>> 2) The Second problem depends on the number of errors it can throw.  If 
>>>> there is exactly one type of error, the most common way to handle it is by 
>>>> returning optional.  If you have one obvious failure mode with a value, 
>>>> then you throw that value.  The most common case is where you can throw 
>>>> more than one sort of error, and therefore have an enum to describe it.
>>>> 
>>>> 3) Third, your enum needs to be declared fragile in order to allow clients 
>>>> to enumerate their cases specifically.
>>>> 
>>>> The third step (having to mark your enum fragile, however it is spelled) 
>>>> is the biggest sign that you’re opting into a commitment that you should 
>>>> think really hard about.  If folks don’t know that this is a big API 
>>>> commitment, then we have bigger problems.
>>>> 
>>>> 
>>>>>> One thing you didn’t mention is that boxing thrown values in an 
>>>>>> existential requires allocation in the general case.  This may be 
>>>>>> unacceptable for some classes of Swift application (in the embedded / 
>>>>>> deep systems space) or simply undesirable because of the performance 
>>>>>> implication.
>>>>> 
>>>>> So, the performance implication cuts both ways.  We can design the ABI 
>>>>> for typed-throws so that, say, the callee initializes some buffer that's 
>>>>> passed into it.  That's an ABI that will kill some potential allocations 
>>>>> in deep systems code, no question about it.
>>>> 
>>>> Agreed.
>>>> 
>>>>> But in non-deep-systems code, we generally expect that error types will 
>>>>> be resilient, which means that there are non-zero dynamic costs for 
>>>>> allocating space on the stack for the error.
>>>> 
>>>> Proposed solution:  ABI is that the callee takes in a register which is 
>>>> either a buffer address to fill in or null. On error, the callee returns 
>>>> the error pointer in a specific register.  If there was a buffer passed 
>>>> in, it uses it, otherwise it allocates.
>>>> 
>>>> In practice, this allows the compiler to only pre-allocate the buffer when 
>>>> it knows the fixed size, otherwise the caller allocates on the heap on 
>>>> demand.
>>>> 
>>>> AFAICT, the cost of this API is only a “li rN, 0” in the normal path.
>>> 
>>> If you really want zero mallocs, it seems to me like creative stack 
>>> accounting could get you there. In cases where we concretely know we're 
>>> working with an existential, we can "explode" the existential into an 
>>> opened type variable and a value of that type, and avoid allocating the 
>>> existential representation. In the case of an untyped or resilient error, 
>>> the callee could leave the error value and its type metadata on the stack 
>>> somewhere without resetting the stack pointer, stick the address of that 
>>> payload somewhere, and code would then propagate up to the catcher, who's 
>>> responsible for consuming the error value and popping the stack when it's 
>>> done with it. We could potentially do this more generally with existential 
>>> returns too, so that existentials are a viable abstraction tool even in 
>>> predictable-performance mode.
>>> 
>>> -Joe
>> 
>> Surely the compiler could already do that within a single module? If it 
>> knows it’s going to get a bunch of cases from the same enum, it can have an 
>> optimised internal call. We could possibly allow exhaustive catching within 
>> the same module this way.
>> 
>> Most libraries will need the existential wrapper due to resilience concerns.
> 
> Even across resilience boundaries, values of existential type don't 
> fundamentally *need* to use the wrapper representation when we're know we're 
> statically working with an existential, only when we're working with one as 
> an unspecialized generic T == SomeExistentialType. An existential function 
> argument can be exploded into a generic argument; existential returns and 
> errors would require out-of-line allocation if shoehorned into traditional 
> callstack discipline due to the variable size of the payload, but an 
> implementation that avoids that is still possible. You're right that, for a 
> protocol whose conformers are all known, there are further refinements 
> possible, such as discriminating the type by a enum-like tag rather than an 
> arbitrary type metadata pointer, and potentially giving the existential box a 
> specialized representation with a known maximum size. (In deep enough systems 
> code that isn't resilient at all and is compiled all at once, you could also 
> conceivably attack the untyped-Error problem by determining the max size an 
> Error buffer needs at compile time this way.)
> 
> -Joe

Yeah that sounds like a much better solution. 

I’m having a go at making the compiler tag all of the errors a function throws 
- theoretically I figure it should be similar to the CaptureWalker, just 
tagging a list of thrown Errors rather than captured variables, with some 
special annotation for masked errors or errors rethrown from another function 
or closure (if that function or closure comes from another module, it will 
always require a catch-all). That should allow later stages to figure out an 
envelope size or generate a private enum to have optimised calls, or serialise 
it with the module as a form of basic documentation. Within a single module, we 
could re-walk that list and warn about redundant catches or prove 
exhaustiveness, without the programmer having to write anything or changing our 
current model in any way.

I think we should take the softest possible course before embarking on 
something like typed-throws. Errors just _are_ version and 
implementation-specific; it’s in their nature. You shouldn’t rely on them 
happening or not. My understanding of the arguments for typed-throws is that 
the primary motivation is performance and exhaustive catching across modules 
(since we can do both for intra-modular errors regardless). The cost is that 
you will have to shuffle around your errors to essentially become per-function, 
making them harder to work with or re-throw (e.g. my function “read” now needs 
its own enum to include the errors from “open” + its own actual, unique 
errors), and the performance and exhaustive catching will only be available for 
@fixed enums, fixing your function _implementation_ in ways beyond what just 
adding @versioned does. Maybe you should just use an Either<T, Error> enum?

In practice, the benefits just aren’t there. Again, intra-modular errors can be 
made easier to live with, just with some compiler improvements. The user-facing 
side (exhaustive catching, redundant catching warnings), realistically, aren’t 
*that* hard. Optimisations to take advantage of that information can just 
happen when they happen.

- Karl
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to