Thoughts on fixing a SIL verification error and/or bad codegen involving default arguments & implicitly-opened existentials

going to digress a bit from the original topic with some meandering thoughts, primarily to try and clarify my thinking and document various issues here...


as Holly covered in SE-411, and with the additional points Slava noted earlier, the current evaluation order is (each category evaluates left-to-right):

no isolated default parameters:

  1. rvalue opened existential args
  2. explicit rvalue args
  3. default args & formal access args

isolated default parameters:

  1. rvalue opened existential args
  2. explicit rvalue args
  3. formal access args
  4. default args

when existential opening comes into play, the issues it can cause of which i'm currently aware are:

  1. opening an rvalue existential can change the 'normal' evaluation order of rvalue arguments (Slava pointed this out above)
  2. opening an lvalue existential crashes the compiler if default arguments precede it (the motivation for this thread)

there seem to be a number of competing concerns involved in ordering the evaluation of call arguments:

  1. argument evaluation should generally occur left-to-right
    • this aligns with most programmers' model of how call argument evaluation order works, and has been stated in various places as how Swift behaves
  2. formal accesses should generally occur 'late'
    • minimizing the extent of formal access reduces 'spurious' access violations
    • allows patterns like self.foo(self.x) where foo() is mutating to work
    • various desired behaviors involving opened existentials have put pressure on the language to work this way (see here, here)
  3. default argument evaluations should occur after rvalue evaluations
    • for reasons John cited here
  4. isolated default argument evaluations may require an executor hop
    • i think this incentivizes evaluating them later than usual to keep the hop 'close' to where the ultimate method invocation will happen
  5. opening existentials should 'just work' and be transparent to users to the extent possible
    • existential opening must occur before processing default arguments as the opened type is provided to the default arguments' generic context (someone fact check me on this?). this is why the compiler currently crashes in the motivating issue for this thread.

some issues with the current behavior:

formal accesses

it seems... confusing and somewhat irritating that you can get dynamic exclusivity violations when evaluating default arguments that go away if you 'manually inline' the argument generator implementation:

var i = 0
func orderMatters(_: inout Int, d: Int = i) {}

orderMatters(&i) // 💥 runtime exclusivity violation
orderMatters(&i, d: i) // ✅

this sort of thing makes me feel that the formal access phase should maybe just be its own special thing that happens as late as possible, but it's not clear if that would break some desired language semantics, or even makes sense as a proposition. this quote from the 'ownership manifesto' makes me wonder if the existing interaction with default arguments is correct/desirable:

The evaluation of a storage reference expression is divided into two phases: it is first formally evaluated to a storage reference, and then a formal access to that storage reference occurs for some duration. The two phases are often evaluated in immediate succession, but they can be separated in complex cases, such as when an inout argument is not the last argument to a call. The purpose of this phase division is to minimize the duration of the formal access while still preserving, to the greatest extent possible, Swift's left-to-right evaluation rules.

is this justification that we should be postponing formal accesses until after default argument evaluation entirely?

isolated defaults

while i'm not sure whether it comes up much in practice, it seems kind of confusing to have different evaluation orders of inouts & default arguments depending on whether or not there's a default argument present.

opened existentials

breaking left-to-right evaluation order when opening an rvalue existential parameter is confusing.


i'm not sure all of this actually makes sense, but my current intuition about how the ordering perhaps should work is (all sections left-to-right):

  1. evaluate non-default arguments
    • open existentials as they occur (waves hands)
    • perform first evaluation phase of lvalues requiring formal access as they occur (only resolve the 'storage reference', do not begin formal access)
  2. evaluate 'caller side' default arguments
    • are these just expression macros currently?
  3. evaluate 'callee side' default arguments
    • perform an executor hop first if needed for isolated default args
  4. begin formal accesses

i think there's an argument that default args should be processed left-to-right in declaration order as well but historically they haven't been for reasons alluded to earlier in the thread, and the isolated use case sort of pushes toward handling them separately anyway. given the constraints, overall it seems it may be simpler to treat them distinctly as a group, and processed separately from non-default arguments.


returning to the motivating issue... i'm a bit less sanguine about moving default arguments unconditionally, since it seems that could introduce exclusivity violations that didn't previously occur (and they may not show up until runtime). if that still seems like a plausible path forward, i can try to clean up the draft, but maybe we'd need to be somewhat more targeted about when such logic could be applied (cases in which we'd previously just fail to compile), or aim for a more holistic solution that ensures the opened existential types are somehow made available after the rvalue evaluation phase.

1 Like