Why are default arguments evaluated after 'regular' arguments in call expressions?

i've recently learned that default arguments in a call expression are evaluated after 'rvalue' parameters. the following example illustrates this behavior:

func sideEffect<V>(_ v: V) -> V {
  print("side effect for: \(v)")
  return v
}

func testArgEvalOrder(
  a: String = sideEffect("a"),
  b: String,
  c: String = sideEffect("c")
) {}

testArgEvalOrder(b: sideEffect("b")) // b, a, c
testArgEvalOrder(b: sideEffect("b"), c: sideEffect("c")) // b, c, a

i was rather surprised to learn this, and i'm wondering why exactly things work in this way. the best documentation i've found regarding the current behavior is from this section of SE-411. however, that exposition doesn't really offer motivation for the existing behavior (since that's not really its purpose), and i'm curious what it is.

from doing a bit of 'code archeology' my best theories thus far are:

  1. the behavior was inherited from the fact that Swift used to allow default arguments to be 'shuffled' at callsites.
  2. emitting default argument function calls later may allow for better optimization opportunities or improved codegen.
  3. there's no specific reason – the language doesn't 'officially' commit to a particular behavior, and implementing it this way was easier/beneficial (or something).

grateful for any historical insights anyone may be willing to share! cc @John_McCall – i believe you may have written some of the original code, so wondering if you know any of the motivations here.

investigation notes
  1. possible first implementation of 'default args emitted after other args': Blaming swift/lib/SILGen/SILGenApply.cpp at bf75beeb7a320e6a77ff8e49cfd03c402392ee3e · swiftlang/swift · GitHub
  2. some stuff on calling conventions & default arg generators hinting at some optimization ideas: swift/docs/ABI/CallingConvention.rst at e4d57c3bd8bd3f461b1428fe899967aeac8754bd · swiftlang/swift · GitHub
2 Likes

It's an intentional choice. The idea is that default arguments are more like part of the implementation of the method. Evaluating them after the formal evaluation of the explicit arguments helps to create a firmer split between things that are evaluated on behalf of the caller and things that are evaluated on behalf of the callee. For example, it makes it easier to refactor a default argument into an overload and vice-versa.

It also very intentionally prevents APIs from doing shortsighted things like putting a likely-to-stay-defaulted parameter at the start of the parameter list with the idea that it will have side-effects that affect the evaluation of the rest of the arguments.

10 Likes