This is a follow-up to a post from earlier in the year. I've had an opportunity to refine the original idea and implement it, and the implementation is in-tree but disabled by default. I inadvertently removed the branch of swift-evolution
that the previous pitch was in, but that isn't of much concern since this is substantially different than the previous pitch.
Extending operator declarations with designated types
- Proposal: SE-NNNN
- Authors: Mark Lacey
- Review Manager: TBD
- Status: Implemented under staging options
During the review process, add the following fields as needed:
- Implementation: This is currently in-tree, disabled by default, and enabled by staging options (
-enable-operator-designated-types
,-solver-enable-operator-designated-types
) and only changes behavior in-swift-version 5
mode. - Decision Notes: Rationale, Additional Commentary
- Bugs: SR-NNNN, SR-MMMM
- Previous Revision: 1
- Previous Proposal: SE-XXXX
Introduction
Swift's expression type checker is known to have cases where it is extremely slow in type checking expressions.
Some of these slow typechecking cases involve expressions that make use of operators (whereas others involve collections or chained single-expression closures).
This proposal addresses speeding up type checking for cases that are slow primarily due to the number of combinations of overloads of operators that are examined in the course of typechecking.
Swift-evolution thread: Discussion thread topic for that proposal
Motivation
Users sometimes write reasonable looking expressions that result in either the compiler taking a very long time to typecheck the expression or results in the diagnostic the compiler is unable to type-check this expression in reasonable time
without any further hints as to how to fix the problem.
There are various reasons users hit this error:
- Expressions involving many operators and (usually) several literals, that should successfully typecheck.
- Expressions that should not successfully typecheck (but fail to emit a good diagnostic prior to the type checker giving up).
- Expressions involving collection literals sometimes fail even if they have only a handful of elements (e.g. < 20).
- Chains of single-expression closures, which in addition to exacerbating the issues with operators and literals, also create new problems due to a lack of explicit types for their parameters.
This proposal aims to address only the first of these issues.
Proposed solution
We introduce syntax to define designated types in operator declarations. These types will be used as the first place to look up operator overloads for a given operator. If the expression successfully typechecks with an overload found via those types, we stop looking at the remaining overloads for the operator.
The intent is to use this within Policy.swift
in the standard library in order to provide a hint to the type checker as to which overload should be preferred for typechecking an operator. You can see the current usage of this syntax here.
Users can also add their own designated types to operators that they declare, but they will not have a way to add their own types as designated types to operators already defined in Policy.swift
.
In effect this ties a particular operator to the semantics of one (or occassionally multiple) particular named entity(ies), usually a protocol, but sometimes a concrete type. For example, if you're implementing <<
, making BinaryInteger
the designated type for that operator says that the expectation is that you will be implementing this for a type conforming to BinaryInteger
and behaving in the way BinaryInteger
would expect it to.
The goal is to use this hint to replace the currently undocumented (and difficult to reason about) hacks in the type checker that are in place today and which are required in order to typecheck even relatively simple expressions.
Detailed design
Operator declaration syntax is extended to allow for one or more designated types to be specified. For infix operators, these types are listed after the (optional) precedence group. For prefix and postfix operators, these types come immediately after a :
following the operator name. Some examples:
infix operator * : MultiplicationPrecedence, Numeric
infix operator / : MultiplicationPrecedence, BinaryInteger, FloatingPoint
prefix operator ~ : BinaryInteger
Specifying one or more types results in the expression type checker looking at declarations defined in those types before other declarations of the operator, and stopping as soon as it finds that one of the declarations in one of the types can be used to successfully type check the expression. If it fails to typecheck successfully with one of those declarations, it continues attempting all the other overloads for that operator.
In cases where there are multiple designated types for a given operator, the specific order in which the overloads in those types are examined is left unspecified, but should produce results that are compatible with the existing type checker's notion of the "best" solution if attempting all of the overloads could result in multiple solutions.
Experimental results
The implementation for this is already in-tree, and is enabled as such:
-
-Xfrontend -enable-operator-designated-types
: enables parsing the extended operator declarations -
-Xfrontend -solver-enable-operator-designated-types
: enables having the constraint solver make use of the declarations, but only when-swift-version 5
is also specified
The current implementation has been tested across 25 test cases that were known to be slow and/or result in the type checker giving up after expending substantial effort attempting to type check the expression.
In addition to enabling this new designated type functionality, this testing disables the shrink
portion of the expression type checker (via -Xfrontend -solver-disable-shrink
). This portion of the expression type checker is known to help occassionally but often results in substantially increasing the expression type checking time since it frequently doesn't provide any benefit but does a lot of work. We also disable a number of other problematic hacks (via -Xfrontend -disable-constraint-solver-performance-hacks
).
In 14 cases, the expressions in question now typecheck in less than 30ms (often as little as 5ms). In the 11 remaining cases, the expression is either one that would fail to typecheck if we were able to typecheck it quickly, or involves other things that are known to be problematic, like closures.
Some illustrative examples:
// Was "too complex", now 13ms:
let i: Int? = 1
let j: Int?
let k: Int? = 2
let _ = [i, j, k].reduce(0 as Int?) {
$0 != nil && $1 != nil ? $0! + $1! : ($0 != nil ? $0! : ($1 != nil ? $1! : nil))
}
// Was "too complex", now 5ms:
func test(a: [String], b: String, c: String) -> [String] {
return a.map { $0 + ": " + b + "(" + c + $0 + ")" }
}
// Was "too complex", now 25ms:
func test(strings: [String]) {
for string in strings {
let _ = string.split(omittingEmptySubsequences: false) { $0 == "C" || $0 == "D" || $0 == "H" || $0 == "S"}
}
}
// Was "too complex", now 5ms:
func test(n: Int) -> Int {
return n == 0 ? 0 : (0..<n).reduce(0) {
($0 > 0 && $1 % 2 == 0) ? ((($0 + $1) - ($0 + $1)) / ($1 - $0)) + (($0 + $1) / ($1 - $0)) : $0
}
}
// Was "too complex", now 2ms:
let a = 1
_ = -a + -a - -a + -a - -a
A number (25+) of other test cases were previously compiling quickly were also tested using this functionality and with the existing hacks disabled, and those cases are as fast or faster than previously.
Source compatibility
Because of the fact that we fall back to behavior that is very similar to the current behavior of the type checker (exhaustively testing combinations of operators in the cases), this change is very compatible with the current behavior. One failure was seen in the source compatibility suite when running with this new behavior enabled by default in all language modes. The failure was in BlueSocket
, and that same failure also manifests if you build the affected code with -swift-version 4
. Specificially, for let mask = 1 << (2 as Int32)
, we now infer mask
as Int
rather than Int32
.
Effect on ABI stability
None.
Effect on API resilience
None.
Alternatives considered
There have been various attempts at fixing this problem strictly through changing implementation details of the expression type checker. To date none of those attempts has been completely successful at making relatively straightforward expressions typecheck in a reasonable period of time in all cases.
There have been various iterations of this proposal that suggested alternatives such as treating operators like member lookup, with something similar to these designated types used in places where literals appear in an expression. This current iteration of the proposal seems simpler, easier to teach, and also creates a strong tie between the operator declaration and the semantic meaning of the operator by saying, e.g. that <<
is intended to be defined by types that implement BinaryInteger
and that <
is intended to be defined by types that implement Comparable
.