SE-0155 Status Update

Hello Swift Community,

In late August of last year, a revision of SE-0155 was approved by the core team and the responsibility for its implementation was discharged to me after I responded to a call for implementors. As many of you have noticed, we are now at Swift 4.2 and there is not an implementation of this proposal that has made it into master. I'm writing this to update you all on the current status of the implementation of SE-0155, and to issue reflections on its implementation now that we are nearing the 7-8 month mark without a proper landing.

The Proposal

SE-0155 Normalize Enum Case Representation has as its stated goal a move away from the tuple-based representation for enum case payloads. The rest of the proposal enumerates the features that would effectively be unblocked by this representation change. I list these below along with their current implementation status. For those that are interested, the final branch I intend to land is here. For those that are even more curious, the first patchset I tried to land several months ago is here.

Changing the Representation of Enum Cases

Status: Implemented (and shipping)

Compound Names For Enum Constructors

Status: Implemented

Example

enum Tree<T> {
  case empty
  indirect case branch(left: Tree<T>, data: T, right: Tree<T>)
}
// Compound function references came for free!
let tree: Tree<Int> = .branch(left:data:right:)(.empty, 42, .empty)

Overloading Enum Cases By Base Name

Status: Implemented

Example

enum SyntaxTree {
    case arrow(raw: [TypeVariable])
    case arrow(bound: [Type]) // used to be an error in Swift 3
}

Default Parameter Values For Enum Constructors

Status: Not implemented

Example

enum Animation {
    case fadeIn(duration: TimeInterval = 0.3)
}
let anim = Animation.fadeIn()

Rationale

The implementation process exposed a number of bugs caused by ancillary representations in the Swift compiler. For example, a class of bugs caused by default argument handling that allows SILGen to form substituted types that are incompatible with their corresponding abstraction patterns was discovered during implementation. Additionally, the work required to support default argument emission would itself require a substantial refactoring to SILGenApply. Attempts to work around this have been stymied by the previously linked bug.

Payload-less Case Declaration

Status: Implemented

Example

enum Tree {
     // case leaf() // Banned!
    case leaf(Void)
} 

Pattern Consistency

Status: Partially Implemented

Rationale

Questions about this part of the proposal were raised before, during, and after the proposal process. The reply from the core team in their original acceptance introduced what I felt at the time was an inconsistency in the pattern matching process. I raised the issue at the time but did not receive an official response - though the discussions I had with members of the Swift community that did respond to the original thread were valuable in their own right. I then implemented and gated both versions of the pattern matching code: one matching Swift's existing behavior but modernized for SE-0155, one matching the behavior of the Core Team's rationale.

Open Questions

The following questions are predicated on the understanding that we will need to revisit this proposal in some way. How deeply we should revisit this is entirely up to the core team. I leave these questions below as a way to begin a discussion about the direction we ultimately want this to go.

Consistency In Pattern Matching

Most pressing is the reality of the specification that exists today. Ungating the pattern matching revision reveals that a rather large amount of code is relying upon the existing Swift 3 pattern matching behavior around tuple element labels and neither the proposal nor the acceptance rationale provide a clear answer for how we wish to resolve this (see the issue thread). If the community still feels that a source breaking change of this magnitude in the name of consistency is warranted, a revision that clearly spells out the rules for pattern matching should be drafted.

Sticking The Landing

As it stands, a large amount of the refactoring required to actually implement the proposal is finished and may be merged at any time. If we can address the core source-breaking components of this proposal, the rest can simply be landed piecemeal with diagnostic gates on the parts that will be implemented later. For example, I currently have default arguments gated pending landing a SILGenApply patch, and the pattern matching consistency code simply needs to be re-gated pending a community discussion.

18 Likes

This gets my vote. Whilst it is a short term hit it is a long term gain, particularly for the hopefully arm of new Swift programmers.

3 Likes

I had understood that the core team's rationale greatly mitigates source breakage, since currently all cases must have unique base names, meaning that argument labels can be either used for all arguments or omitted entirely. What code are you observing in the wild that relies on either inconsistent use of labels or use of incorrect labels?

a revision that clearly spells out the rules for pattern matching should be drafted

I feel like the examples in the acceptance rational is a good jump off point. Quoting the full example here:

enum E {
  case often(first: Int, second: Int)
  case lots(first: Int, second: Int)
  case many(value: Int)
  case many(first: Int, second: Int)
  case many(alpha: Int, beta: Int)
  case sometimes(value: Int)
  case sometimes(Int)
}

switch e {
// Valid: the sequence of labels exactly matches a case name.
case .often(first: let a, second: let b):
  ...

// Valid: there is only one case with this base name.
case .lots(let a, let b):
  ...

// Valid: there is only one case with this base name and payload count.
case .many(let a):
  ...

// Invalid: there are multiple cases with this base name and payload count.
case .many(let a, let b):
  ...

// Valid: the sequence of labels exactly matches a case name.
case .many(first: let a, second: let b):
  ...

// Invalid: includes a label, but not on all of the labelled arguments.
case .same(alpha: let a, let b):
  ...

// Valid: the sequence of labels exactly matches a case name (that happens to not provide any labels).
case .sometimes(let x):
  ...

// Invalid: includes a label, but there is no matching case.
case .sometimes(badlabel: let x):
  ...
}

When we left our last discussion, there seems to be some different intepretation on this sentence in the rationale,

A case pattern may omit labels for the associated values of a case if there is only one case with the same base name and arity.

@codafi said:

If we’re matching by arity, the labels go out the door;

… and @anandabits replied with:

That’s not how I read it: "A pattern must omit all labels if it omits any of them; thus, a case pattern either exactly matches the full name of a case or has no labels at all."

It seems to me the disagreement whether the following should be valid:

  /// (?) This is a unique basename + arity case. We can omit label `value`
  /// but should including it be valid too?
  case .many(value: let a):

Is this the focal point of that discussion?

I had understood that the core team's rationale greatly mitigates source breakage, since currently all cases must have unique base names, meaning that argument labels can be either used for all arguments or omitted entirely.

Yes, that is my reading of their acceptance rationale as well - and it is what I implemented.

"Swift 3 behavior" extends to a number of stranger corner-cases of the existing semantic checks. As a test, I turned on errors for these and ran the compatibility suite and summarily failed it. Exaggerated examples of these are mine:

Single-Element "Tuples" Evade Labelling Mismatch Rules

switch (5) {
case let (anylabelAtAllReally: y): break
}

switch (anyLabelAtAllReally: 5) {
case let (_: y): break
}

Paren Hell

switch (5, 5) {
case let ((((((((((((((((((((x, y)))))))))))))))))))): break
} 

Tuple Matches

// Combines freely with the above
switch (5, 5) {
case (let y): break 
}

The last of these is explicitly banned, the first and second are not.

Of that tangent, but it was not the point I initially raised. At the time, I was (and still am) concerned with Future Swift under the rationale's ruleset. I'll quote the relevant part of the thread then

It is likely that cases will continue to be predominantly distinguished by their base name alone...

This makes sense given the current state of the world, but under this proposal we fully expect users to be overloading that base name and writing more and more ambiguous patterns. We should encourage disambiguating these cases with labels as a matter of both principle and QoI.

A pattern is meant to mirror the way a value was constructed with destructuring acting as a dual to creation. By maintaining the structure of the value in the pattern, labels included, users can properly convey that they intend the label to be a real part of the API of an enum case with associated values instead of just an ancillary storage area. Further, we can actually simplify pattern matching by making enum cases consistent with something function-like instead of tuple-like.

To that end, I'd like the rationale and the proposal to be amended to require labels in patterns in all cases.

I'm pretty sure these kinds of patterns that we may find distasteful are nonetheless widespread, and we will not be able to break them in Swift 4 mode. That means we have to have a good reason to cause breakage in Swift 5. Paren hell, for example, doesn't bother me—extra parenthesization is supposed to be equivalent in all cases, except that we sometimes use it as a hint to suppress diagnostics. (And when it interacts with precedence, of course.) I also don't understand why your example in "tuple matches" is disallowed; that looks like a perfectly normal match of an entire value that someone happens to have written parentheses around.

Because it extends into the pattern grammar for enum cases.

enum Foo {
  case bar(Int, Int)
}

switch Foo.bar(5, 5) {
case .bar(let y): break
}

This kills overload resolution.

enum Foo {
  case bar(Int, Int)
  case bar(Int, Int, Int)
}

switch Foo.bar(5, 5) {
case .bar(let y): break // enum case 'bar' is ambiguous without explicit pattern matching associated values
}

I'm fine with disallowing it in enum case position, but it should be fine for any single value, even if that value is a tuple. This is the same sort of change as SE-0066 distinguishing (Int, Int) -> Int and ((Int, Int)) -> Int.

Ah, I'd missed the part where you're talking about pattern matching with tuples. Internally, the compiler may share the same code here with enum pattern matching, but I don't read anything in SE-0155 as changing how tuple pattern matching works. Indeed, I understood SE-0155 explicitly to be about distinguishing enum case associated values (which are henceforth now to work like parameter lists in most situations) from tuples.

@codafi is there any chance your implementation will land in Swift 5? I'm really looking forward to this change and I also appreciate all your hard work to make it happen. :slight_smile:

5 Likes