Hello All,
I wanted to get some advice on a path forward for improving a behavior I discovered. Because this change affects the compiler, and especially Swift playgrounds, I wanted to come to the community for advice on a solution I've devised so that we can iterate and improve on it.
The Setup
Begin by opening a playground and declare the following dummy function
func foo(_ : Int, _ : String, _ : Int) {}
Now, instead of calling it, fill in placeholders. If you're following along at home and you'd like to make your own, type any string surrounded by the delimiters <#
and #>
. For example,
foo(<#Int#>, <#String#>, <#Int#>)
The playground will emit a warning about an editor placeholder being in the file, but the playground will execute itself.
That is, until it crashes.
Fatal error: attempt to evaluate editor placeholder: file MyPlayground1.playground, line 2
Let's change the example
func foo(_ : Int, _ : String, _ : Double) {}
foo(<#Int#>, <#String#>, <#Double#>)
Now we have a static error - the playground does not execute
cannot invoke 'foo' with an argument list of type '(Int, String, Double)'
This occurs because of a quirk in our typing rules. You see, if you play this game with autocomplete instead of by hand, you'll get placeholders that look a little different:
func foo(_ : Int, _ : String, _ : Double) {}
foo(<#T##Int#>, <#T##String#>, <#T##Double#>)
These placeholders come with a contextual type. That type is accepted by the type system as gospel. When that type isn't available, the compiler synthesizes a type variable and attempts to use contextual information to bind that type variable to something. If that succeeds, then the compiler transforms the editor placeholder into a call to the compiler intrinsic _undefined
which looks like this
public // COMPILER_INTRINSIC
func _undefined<T>(
_ message: @autoclosure () -> String = String(),
file: StaticString = #file, line: UInt = #line
) -> T {
_assertionFailure("Fatal error", message(), file: file, line: line, flags: 0)
}
The Rub
When we emit this with type variables, we have very little semantic information to go off of. Most of the time, these type variables wind up picking up a damaged type from surrounding context, and if they don't then they wind up typed as Void
via a sneaky defaulting constraint. This leads to some... less than desirable diagnostics
// compile-time error!
for x in <#T#> {} // error: Type '()' does not conform to protocol 'Sequence'
In addition, emitting these as type variables is expensive. So, the decision was made in 207fffc to curtail the number of type variables we generate for placeholders to 2. The consequence is, when we call foo
with just Int
and Double
-typed placeholders, and as long as the odd-numbered placeholders have a common type that all other odd-numbered placeholders agree to (ditto for even-numbered placeholders), we get a runtime failure. Else, as demonstrated above, we get a compile-time failure. This also means that we can get a compile-time failure simply by making the even-even or odd-odd numbered placeholders disagree
// compile-time error!
func foo(_ : Int, _ : Int, _ : String) {}
foo(<#Int#>, <#Int#>, <#String#>) // error: Cannot invoke 'foo' with an argument list of type '(Int, Int, String)'
The Fix
I believe the intent all the way back in 2015 was to allow for playgrounds in an inconsistent or partial state of development - as long as that state was entirely placeholder-based - to still be executable. A feature reminiscent of runtime typed holes which offer a huge usability improvement to statically-typed developments. But the rules for when you do and don't get a runtime failure in a playground are not, and have not ever been, consistent.
So, I have a patch that takes the draconian route and collapses the runtime/compile-time distinction for playground diagnostics entirely: we will always emit an error for placeholders wherever we see them. This also means that we no longer have to keep around the weird type inference rules that lead to these diagnostics which improves QoI and incidentally simplifies CSApply quite nicely.
The problem, of course, is that this is a potential workflow-breaking change. If you were used to the typed holes behavior, that will go away entirely under this regime. So, I present some alternatives
Alternatives
Leave It The Way You Found It
Harden The Typing Rule
The current inference rule introduces an artificial low-information type variable. We could instead require the contextual type be provided - which it is in 99.9999% of cases by the IDE - then flip the inference rule into a checking rule and try to unify the placeholder with its surrounding context. This removes the expensive type variable and preserves the runtime-hole-like behavior - but not in all cases. User-defined holes without a contextual type will be treated as hostile invaders and given an error type. Holes with contextual types, whether user or IDE generated will, of course, still work just fine. Though, you won't be able to provide a mismatched contextual type here either
func foo(_ : Int, _ : Int, _ : String) {}
foo(<#T##Int#>, <#T##Int#>, <#T##Int#>) // error: Cannot convert value of type 'Int' to expected argument type 'String'
I doubt most people are typing their own placeholders. But, then again, that's what I'm here to find out.