Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SR-13955] Support computed cases (aka pattern synonyms) for ergonomic binding. #56352

Open
typesanitizer opened this issue Dec 11, 2020 · 9 comments
Labels
compiler The Swift compiler in itself new feature

Comments

@typesanitizer
Copy link

Previous ID SR-13955
Radar rdar://problem/72207437
Original Reporter @typesanitizer
Type New Feature
Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels New Feature
Assignee None
Priority Medium

md5: 98206919fda80539e5d50e9c8f42f26c

Issue Description:

Swift's structs are pretty cool. You can project stored and computed properties with . syntax, and you can construct values using initializer syntax. Swift's enums are pretty cool too. You can pass in associated values for construction, and you can pattern match and create bindings for the associated values. Enums can be more struct-like where they have computed properties, which are accessed using . syntax. However, structs cannot be enum-like; they can't have computed cases![]( Oh, the tyranny)

More seriously, there is some affordance here in the form of ~=. However, it's not enough. One wants to work with structs and have the ability to destructure the input in a nice way. Consider the classic example of deserialization, where you put a bunch of different objects into the same array.

if let cat = Cat(blob) {
  // use cat
} else if let dog = Dog(blob) {
  // use dog
} else if let mouse = Mouse(blob) {
  // use mouse
} else if ... {
}

You have this cascade in a bunch of places. It would be so much nicer if you could write some "computed cases" once, and directly pattern match on blob:

switch blob {
  case .cat(let cat): // use cat like normal
  // ...
}
let blobCat = Blob.cat(tom)

// strawman syntax for computed cases
extension Blob {
  case cat(Cat) {
    get? { Cat(blob) } // try converting Blob to Cat
    set  { newValue.encode() } // convert Cat to Blob
  }
}

As an extension, we could have an attribute, say (strawman name) @uncheckedAssumeExhaustive(case1, ... caseN) which can be used to say "these patterns are exhaustive, trust me".

@typesanitizer
Copy link
Author

@swift-ci create

@typesanitizer
Copy link
Author

What I'm suggesting is the equivalent of computed properties but for cases. More specifically, you can specify a matcher (shown with get? in the example), and optionally an initializer (shown with set in the example) for a computed case. The matcher for a computed case on a type T is internally a function of type (T) -> Optional<(... tuple payload ...)>, and this tuple payload can be bound directly. The initializer is not very interesting; it is sugar for a static func, but it is there to provide visual symmetry between matching and construction.

Additionally, you can say that a group of computed cases are exhaustive together using @uncheckedAssumeExhaustive. Otherwise, you need a default case.

If you're familiar with Haskell, what I'm suggesting here is roughly equivalent to GHC's PatternSynonyms extension, and @uncheckedAssumeExhaustive would be the equivalent of GHC's COMPLETE pragma.


I don't quite understand the semantics of what you're suggesting. A matcher needs to return values that can be bound. In your example, the matcher is returning true/false; I don't think that is sufficient information. For example, you've written:

case let Contained(count: myCount, containedIn: myContainedIn):

It is not clear what values myCount and myContainedIn should be bound to here. If they are just the property values, then that's not very useful IMO, because you can already access those today.

@swift-ci
Copy link
Collaborator

Comment by Daniel Sweeney (JIRA)

I think I took an idea I had and put it in place of an idea that you had, based on the label and not the description. Sorry about that 🙂. I deleted my comment to clean up the ticket. I will look at PatternSynonyms in ghc also. Thanks!

@swift-ci
Copy link
Collaborator

Comment by Daniel Sweeney (JIRA)

Still thinking about this. Is this where you're going with this?:

  1. Structs can now contain one or more case entries.

  2. the name of a struct case has similar syntax to an associated value in an enum so it allows just a tag, or a tag with some arguments.

  3. The body of a struct case has get? and set branches.

  4. When switch ing on a value of the struct type, the get? branch is called. If it returns .none the case does not match, otherwise the value can be bound in a case let.

  5. The get? has type Self -> (args)? where (args) is the tuple of arguments to the case label and Self is the type of the struct.

  6. The set branch is called when the case is constructed directly. Direct construction looks like a static method call using the type name of the struct, the name of the case and the arguments of the case. The type of the set branch is (args) -> Self where args is the tuple type of the case arguments and Self is the type of the struct.

  7. If you switch on a value of struct type, the switch cases can be any of:

    1. match on equality, if the struct is Equatable. The case value would be an instance of the struct itself, constructed through init.

    2. match using ~{{}}=, using an expression pattern (TSPL calls it an expression pattern in the language reference.) This looks for an overload of ~= with the pattern argument matching the type of the value in the case and the value argument matching the type of the value switched on. If the overload exists and returns true the case succeeds. (I might have swapped pattern and value there, I did not look it up.)

    3. match using get? if the case value is of the same type as the value switched on and is labelled using one of the struct labels defined for the struct. This can bind values in the switch case using the tuple returned from the struct case.

    4. match using equality if the case value uses a struct case value and the struct is Equatable . (So in case .cat(let cat): above, if the result of the struct case .cat was == to the switch value blob it would succeed.

  8. If you switch on a struct value with case s, you can either match all defined struct cases as well as some of the other case types, or include a default.

Am I getting closer there? I am not sure that the @uncheckedAssumeExhaustive is necessary, if you can include a default. But my reasoning might be wrong there. I'm trying to validate my understanding of the rest of it, assuming it is valid.

@typesanitizer
Copy link
Author

That's mostly it. These could also be used for enums/classes/protocols, but I anticipate the primary use to be for structs.

I hadn't thought about 7.4, that's a good point. It could be allowed in case the computed property has a set (or say we could call it init). The exact rules around which of get?/set would need to be settled. (FWIW, right now, I'm learning more towards calling them match/init instead of get?/set.)

For @uncheckedAssumeExhaustive, I think it might be useful in certain situations when importing closed discriminated unions via C or C++ . Discriminated unions are implemented as a pair of tag and union in C and C++ and their layout can be arbitrary. Since Swift enums have a specific layout (technically only on platforms with ABI stability, but kinda' true everywhere right now given the implementation), in general we can't import them as Swift enums at zero-cost, but we can import them as structs and hand-write (or maybe even have the compiler synthesize) computed cases so that you can pattern-match exhaustively on them.

@swift-ci
Copy link
Collaborator

Comment by Daniel Sweeney (JIRA)

Thinking more, it seems like the implementation for this would be:

1. Make the parser accept cases in struct declarations and extensions to structs.

2. Add the struct cases to the AST for struct declarations.

3. Type check the struct cases in the declarations, inferring, validating, adding the types to the type environment, like for everything else.

4. Change the type checking of a switch statement so that it accepts valid struct cases and rejects invalid struct cases. This might only be type checking, but there might be some parsing/AST changes for switch statements.

5. Glue together the switch cases and the struct cases. On the one hand, you could do a syntax transformation that put the get?/match function in place in the switch case and tested for optionals and bound values. On the other hand, you probably want to take advantage of the ABI layout of structs somehow, the way you would with enums. For some group of the struct cases you should be able to figure out that you're looking for bit patterns in the struct and simplify the whole switch-case, like you would with an enum. For other struct cases you probably need to call the match function and so on. I'm not sure exactly how switch compiles. I guess this is in SILGenPattern.cpp right now for enum cases.

I'm assuming in the above that after type checking that the SIL is generated and that that is where most of the heavy lifting takes place. I'm not sure I'm correct about that, I have not looked that closely at that part of the compiler, but switch statements have so much control flow that you'd (I'd) expect a change to them to be in the SIL section of the compiler.

Thinking about it more I'm not sure in implementation terms whether this works like enums at all though, it might have to be almost all new code. SIL has internal support for enum cases with like select_enum and so on. To do this might require something like select_struct_case in SIL that then gets more handling in IRGen . I'm getting ahead of what I understand here.

@typesanitizer
Copy link
Author

It's probably clear from context, but just pointing out for clarification: this is a wishlist item/feature request, not a 'someone needs to implement this and submit a PR and we can land it'. It would certainly need to go through Swift Evolution.

Implementation-wise, what you've described should mostly be fine except I think for an initial implementation, one probably doesn't need special SIL instructions and you can SILGen the matcher as a function; if the inliner deems the matcher implementation to be sufficiently small, it can inline it and do further optimizations. One would also need to implement a mangling scheme.

@swift-ci
Copy link
Collaborator

Comment by Daniel Sweeney (JIRA)

Right, it was pretty clear that it would need to go through evolution. I feel like there is a gap around structs and switch-case matching and was interested in thinking it through. I will keep thinking about it.

@typesanitizer
Copy link
Author

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler The Swift compiler in itself new feature
Projects
None yet
Development

No branches or pull requests

2 participants