Skip to content

[SE-0280] Enum cases as protocol witnesses #28916

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

Merged
merged 13 commits into from
Mar 28, 2020
Merged

[SE-0280] Enum cases as protocol witnesses #28916

merged 13 commits into from
Mar 28, 2020

Conversation

theblixguy
Copy link
Collaborator

@theblixguy theblixguy commented Dec 21, 2019

Allow enum cases to witness static protocol requirements. For example:

protocol Foo {
  static var bar: Self { get }
  static func baz(arg: Int) -> Self
}

enum SomeEnum: Foo {
  case bar // Okay
  case baz(arg: Int) // Okay
}

Swift-evolution thread: https://forums.swift.org/t/pitch-enum-cases-as-protocol-witnesses/32753

Resolves SR-3170, SR-3510, SR-9099

@theblixguy
Copy link
Collaborator Author

ping @slavapestov

Copy link
Contributor

@slavapestov slavapestov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm OK with this as long as the core team feels it doesn't need their approval.

CC @DougGregor @rjmccall @jckarter

@rjmccall
Copy link
Contributor

rjmccall commented Jan 6, 2020

Hmm. We might. I'll raise it to the Core Team.

@theblixguy
Copy link
Collaborator Author

@rjmccall Any update on this from the core team?

@rjmccall
Copy link
Contributor

No, sorry. It slipped through the cracks during the last meeting, but I made sure it went back on the agenda for tomorrow.

@theblixguy
Copy link
Collaborator Author

Thank you!

@rjmccall
Copy link
Contributor

Okay. Three points from the Core Team.

The first is that we're generally on board with this change: it makes sense, and we think it'd be good for the language.

The second is that this change does need an evolution proposal, and that proposal will feel pretty incomplete if it doesn't also cover cases with payloads.

The third is that requirement-matching is an area of the language where we really need to set out (and get some community agreement on) the basic design principles. That probably means pitching and defending a sort of mini-manifesto, out of which a number of other proposals would arise. I don't know if you're willing to do all that, but if you are, it probably makes sense to have that conversation before we raise this specifically.

@rjmccall
Copy link
Contributor

rjmccall commented Jan 17, 2020

Let me give you an idea of what we're thinking about the "mini-manifesto". The basic question here is: what kinds of differences in form and type are reasonable between a protocol requirement and the declaration that "witnesses" it? There's a sort of rough continuum here:

  • At one extremum, we could require the witness and requirement to match exactly. It's okay to e.g. write the same types using different typealiases, but otherwise, no differences at all are allowed. This is pretty much Swift's current model, and it's generally seen as excessively strict.

  • Even if we don't allow any other form/type differences, it makes some sense to allow functions with default arguments to witness protocol requirements.

  • It is probably sensible for witnesses to be allowed to have subtypes of requirements. We generally allow this with subclassing, and it makes just as much sense to do it with protocol requirements.

  • Not quite in the same continuum: your PR. Cases aren't declared as static vars and funcs, but it's certainly non unreasonable to think of them as a sort of empowered sugar for that; given that, this change could make sense even under otherwise very strict models.

  • There have been requests to allow at least certain kinds of var/func mismatch. For example, we could allow a property of function type to witness a method requirement.

  • One very aggressive way of thinking about that is to say that anything which syntactically matches the needs of the requirement should be allowed. Note that that would allow e.g. partial applications of methods to witness property requirements of function type, or unapplied instance methods to witness curried static method requirements, or some other oddities.

  • The extremum here is logically just part of the "syntactic" rule, but I think it's worth carving out: we could allow arbitrary protocol requirements to be witnessed by dynamic member lookup and method-call operations.

A mini-manifesto here would go into these points, propose a basic model, and so on. My sense is that most of the Core Team would currently favor something that allowed subtyping and defaulting but probably nothing further, and we'd accept your change specifically as a sort of desugaring. But I don't want to lead you too much.

@theblixguy
Copy link
Collaborator Author

theblixguy commented Jan 17, 2020

@rjmccall Okay, sounds good! I did manage to get it to work with cases with payloads (I'll update this PR) while I was waiting for a response so I think the proposal + implementation part is covered and I can pitch it on the forums.

In terms of the "mini-manifesto", I am happy to write one down based on the points you've provided and pitch on the forums. Does this "mini-manifesto" need to be "approved" by the core team before the proposal can be considered for review?

@rjmccall
Copy link
Contributor

@rjmccall Okay, sounds good! I did manage to get it to work with cases with payloads (I'll update this PR) while I was waiting for a response so I think the proposal + implementation part is covered and I can pitch it on the forums.

That's awesome and great to hear.

In terms of the "mini-manifesto", I am happy to write one down based on the points you've provided and pitch on the forums. Does this "mini-manifesto" need to be "approved" by the core team before the proposal can be considered for review?

We'll need to talk about what process we want to use for this, but you can go ahead and start the conversation in the meantime, I think.

Thanks for taking this on!

@theblixguy theblixguy changed the title [Typechecker] Allow enum cases to satisfy static var { get } protocol requirements [Pending Evolution] Enum cases as protocol witnesses Jan 17, 2020
@theblixguy theblixguy added the swift evolution pending discussion Flag → feature: A feature that has a Swift evolution proposal currently in review label Jan 17, 2020
@theblixguy
Copy link
Collaborator Author

Hmm, I wanted to build a toolchain, but it seems like there's some sort of linking issue because when the witness is a payload case and (1) I build with -Onone and/or (2) I use public visibility then I get:

Undefined symbols for architecture x86_64:
  "_$s4test1EO3baryACSi_tcACmF", referenced from: // enum case constructor
      _$s4test1EOAA1PA2aDP3bar5valuexSi_tFZTW in test-2582e0.o // witness thunk
ld: symbol(s) not found for architecture x86_64

I don't get this with -O unless I use public. It doesn't affect payload-less case witness, only ones with payload. It's a little strange.... there's probably a few places where we're still assuming the witness could only be a func/var/constructor I think.

@jckarter
Copy link
Contributor

@theblixguy I believe we only generate the case constructor symbols by need as shared-linkage symbols so that they don't have to be ABI. You might need to trigger the case constructor's generation when it's used as a witness.

@theblixguy
Copy link
Collaborator Author

theblixguy commented Jan 24, 2020

@jckarter Thank you for the tip! I inserted a call to emitEnumConstructor in emitProtocolWitness, however in this case, SILDeclRef::getLinkage never actually returns shared linkage. It falls down and returns hidden_external linkage. I tweaked it to handle an enum element decl and set the limit to OnDemand which fixes the problem I was having with linking. I am not sure if that's the right/correct way though. Without this, I kept running into issues where emitEnumConstructor will emit it with shared linkage, but emitProtocolWitness would try to fetch the witness func ref (via a call to getWitnessFunctionRef) and I'd hit an assert in getOrCreateFunction due to this mismatch.

@slavapestov
Copy link
Contributor

Enum case constructors should be emitted as needed already by getGlobalFunctionRef(). You can reference a case constructor across modules already, via currying syntax. Can you see why it's not working when referenced from the witness thunk code path?

@slavapestov
Copy link
Contributor

Eg, if I write

_ = Optional<Int>.some

It emits two thunks:

sil shared [transparent] [serializable] [ossa] @$sSq4someyxSgxcABmlF : $@convention(method) <Wrapped> (@in Wrapped, @thin Optional<Wrapped>.Type) -> @out Optional<Wrapped> {

This is the thunk emitted by emitEnumConstructor(). It has shared linkage and a body.

Then there's the curry thunk which gives it the AST type (Optional<Int>.Type) -> (Int) -> (Optional<Int>.some):

sil shared [transparent] [serializable] [thunk] [ossa] @$sSq4someyxSgxcABmlFTc : $@convention(thin) <Wrapped> (@thin Optional<Wrapped>.Type) -> @owned @callee_guaranteed (@in_guaranteed Wrapped) -> @out Optional<Wrapped> {

@theblixguy
Copy link
Collaborator Author

@slavapestov Yeah, I think I have now found the actual problem! So, it's my fault, I made a mistake in SILGenWitnessTable::addMethod. It seems like the code I added to call addMethodImplementation wasn't being invoked in some cases, so we ended up with a SILDeclRef to an enum case witness, but a Func kind. This also explains why I had to manually return a function type in getFunctionInterfaceTypeWithCaptures!

@theblixguy
Copy link
Collaborator Author

@swift-ci please build toolchain

1 similar comment
@theblixguy
Copy link
Collaborator Author

@swift-ci please build toolchain

Copy link
Contributor

@slavapestov slavapestov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad you sorted out the problem with emitting enum constructors. The change looks good to me now.

addSymbolIfNecessary(reqtAccessor, witnessAccessor);
});
} else if (isa<EnumElementDecl>(witnessDecl)) {
addSymbolIfNecessary(valueReq, witnessDecl);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice, enum constructors never have public linkage so they don't need to be visited by TBDGen. However in case this ever changes, there's one less spot to update...

@theblixguy theblixguy added swift evolution approved Flag → feature: A feature that was approved through the Swift evolution process and removed swift evolution pending discussion Flag → feature: A feature that has a Swift evolution proposal currently in review labels Mar 27, 2020
@theblixguy theblixguy changed the title [Pending Evolution] Enum cases as protocol witnesses [SE-0280] Enum cases as protocol witnesses Mar 27, 2020
…e is used as a witness

Also, tweak SILDeclRef::getLinkage to update the 'limit' to 'OnDemand' if we have an enum declaration
Also remove a FIXME and code added to workaround the original bug
@theblixguy
Copy link
Collaborator Author

@swift-ci please test

@swift-ci

This comment has been minimized.

@swift-ci

This comment has been minimized.

@theblixguy
Copy link
Collaborator Author

@swift-ci please test

@swift-ci

This comment has been minimized.

@swift-ci

This comment has been minimized.

@theblixguy
Copy link
Collaborator Author

@swift-ci please test

@swift-ci

This comment has been minimized.

@swift-ci

This comment has been minimized.

@theblixguy
Copy link
Collaborator Author

@swift-ci please test

@swift-ci

This comment has been minimized.

@swift-ci

This comment has been minimized.

@theblixguy
Copy link
Collaborator Author

@swift-ci please test source compatibility

@theblixguy theblixguy merged commit f724d1f into swiftlang:master Mar 28, 2020
@theblixguy theblixguy deleted the fix/SR-3170 branch March 28, 2020 10:44
@damienissa
Copy link

damienissa commented Jul 28, 2020

Hi all. I have found some issues!

import Foundation

public protocol Foo {
    
    static var foo: Self { get }
}

enum Bar: Foo {
    
    case foo
}

func load<F: Foo>(_ foo: F) {
    
    print(foo) // Printed: "foo"
}

load(Bar.foo)  // Printed: "foo"

enum Bar2 {
    case foo
    case bar(Int)
}

func load2(_ bar2: Bar2) {
    
    switch bar2 {
    case .foo: break
    case let .bar(val): print(val)  // Printed: "5"
    }
}

load2(.bar(5)) // Printed: "5"

protocol Foo3: Equatable {
    
    static var foo: Self { get }
    static func bar(_ value: Int) -> Self
}

enum Bar3: Foo3 {
    
    case foo
    case bar(Int)
}

func load3<F3: Foo3>(_ foo3: F3) {
    
    switch foo3 {
    case .foo: break
    case let .bar(val): print(val); #error("Pattern variable binding cannot appear in an expression")
    default: break
    }
}

load3(Bar3.bar(5))  // Compiler failure

We have the only comparable ability like this:

func load3<F3: Foo3>(_ foo3: F3) {
    
    switch foo3 {
    case .foo: break
    case .bar(5): print("Compared");  // Printed: "Compared"
    default: break
    }
}

load3(Bar3.bar(5))  // Printed: "Compared"

@xwu
Copy link
Collaborator

xwu commented Jul 28, 2020

Hi @damienissa,

That behavior is correct and also unrelated to this proposal feature. (Incidentally, even if it were both incorrect and related to this proposal, the forums and/or bug tracker would be the place to discuss it.)

It is unrelated to the proposal feature because the proposal feature is about enum cases serving as witnesses for protocol requirements; this does not mean that static protocol requirements are equivalent to enum cases. It is correct behavior because the protocol does not guarantee any way of binding the argument passed to bar: the only thing we know about types that conform to the protocol is that we can invoke bar with an argument and get back a result of type Self. We can test to see if a given instance is equivalent to .bar(5) but the only way to know what integer was used to create that instance would be to test every value from Int.min to Int.max—and that’s if bar is a pure function.

@damienissa
Copy link

@xwu Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
swift evolution approved Flag → feature: A feature that was approved through the Swift evolution process
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants