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

Allow non-mutable properties and subscripts returning Self #23991

Closed
wants to merge 16 commits into from

Conversation

johnno1962
Copy link
Contributor

@johnno1962 johnno1962 commented Apr 12, 2019

Hi Apple,

further eases restrictions on recent PRs #22863 and #23485 to allow properties and subscripts that return Self, provided they are not settable. Will need to be cherrypicked into 5.1 cc @slavapestov.

Resolves #52726.

@johnno1962 johnno1962 changed the title Allow non-mutable properties and subscripts returning Self [DNM] Allow non-mutable properties and subscripts returning Self Apr 13, 2019
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.

Looks like a good start, thanks for digging into this.

lib/AST/Type.cpp Outdated
@@ -2297,9 +2297,15 @@ static bool matches(CanType t1, CanType t2, TypeMatchOptions matchMode,
}

// Class-to-class.
if (matchMode.contains(TypeMatchFlags::AllowOverride))
if (t2->isExactSuperclassOf(t1))
if (matchMode.contains(TypeMatchFlags::AllowOverride)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this wasn't necessary for method overrides to work, I suspect the right change is to actually look at the caller of this method in TypeCheckDeclOverride.cpp. I bet we're calling eraseDynamicSelfType() somewhere for method types but not property types. Can you take a look?

@@ -888,6 +888,13 @@ namespace {
}
}
}
else if (isa<DynamicSelfType>(member->getInterfaceType()
Copy link
Contributor

Choose a reason for hiding this comment

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

For methods, we also allow a return type of Self?, so properties should allow that too. This is not handled here. You can instead try member->getInterfaceType()->hasDynamicSelfType().

@@ -1297,6 +1297,10 @@ ConstraintSystem::getTypeOfMemberReference(
openedType = openedType->replaceCovariantResultType(baseObjTy, 2);
}
}
else if (isa<DynamicSelfType>(value->getInterfaceType()
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as in CSApply

@@ -110,7 +110,7 @@ class B: A<Int> {
let copy = super.copy() as! Self // supported
return copy
}
override var copied: Self { // expected-error {{'Self' is only available in a protocol or as the result of a method in a class; did you mean 'B'?}}
override var copied: Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

You mentioned you got your end-to-end examples to compile and run. It would be great if you would also add some SILGen and execution tests (look at test/Interpreter/dynamic_self.swift) to make sure the functionality does not regress.

@@ -1056,6 +1058,13 @@ static Type SelfAllowedBySE0068(TypeResolution resolution,
bool isTypeAliasInClass = insideClass &&
options.is(TypeResolverContext::TypeAliasDecl);

if (isMutablePropertyOrSubscriptOfClass) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, so unfortunately I still need to nitpick this method. I'll try to explain what the problem is.

Before you implemented SE-0068, we would return DynamicSelfType in a very specific case -- when options.getBaseContext() == TypeResolverContext::DynamicSelfResult was true. This got set in a somewhat round-about way: when type checking a function definition, we would look into the result TypeRepr (a TypeRepr is the representation of an unchecked type -- think of it as an AST for types written in source). If the TypeRepr was an identifier named "Self", or an optional of an identifier named "Self", we would set this flag.

For declarations other than methods, or if a method had a return type that did not exactly match this pattern (for example, (Self, Self) or Self.Type or () -> Self), options.getBaseContext() == TypeResolverContext::DynamicSelfResult was false.

Now since you're allowing Self to be written in the type of an immutable property or subscript unconditionally, you're going to be able to get properties with types like:

var foo: (Self, Self) { ... }
var foo: () -> Self { ... }
var foo: Self.Type { ... }

The code in CSApply does not handle these cases; it can only construct a member reference to something with a type of Self or Self?. We can fix that with enough effort, but you'll still have to ban code like:

var foo: (Self) -> () { ... }
var foo: Array<Self> { ... }

Where Self is not in covariant position.

I now think the right way to go about that is to not try to guess in TypeCheckType.cpp whether Self is allowed or not. Instead, when you're inside a class, always build a DynamicSelfType. Then when checking declarations in DeclChecker in TypeCheckDecl.cpp, enforce the various rules for each kind of decl:

  • TypeAliasDecl cannot have Self in its type at all, unless its in local context
  • FuncDecl can only have Self as its return type
  • VarDecl can only have Self as its type if its immutable and computed
  • Same for SubscriptDecl and its element type

For nested nominal types this check would need to ban Self from appearing in the generic signature; this is invalid for instance:

class Outer {
  class Inner<S> where S : Sequence, S.Element == Self { ... }
}

So I would implement a new function, like checkDynamicSelfResult(), that is called on class members from inside the various DeclChecker visitor methods. It would implement the rules and emit diagnostics for each invalid case.

Then you can get rid of TypeResolverContext::DynamicSelfResult altogether, and unify the two spots in TypeCheckType.cpp where you produce a DynamicSelfType.

@@ -1056,6 +1058,13 @@ static Type SelfAllowedBySE0068(TypeResolution resolution,
bool isTypeAliasInClass = insideClass &&
options.is(TypeResolverContext::TypeAliasDecl);

if (isMutablePropertyOrSubscriptOfClass) {
if (auto prop = dyn_cast_or_null<ValueDecl>
(dc->getInnermostDeclarationDeclContext()))
Copy link
Contributor

Choose a reason for hiding this comment

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

How does this work for VarDecls? A VarDecl is not a DeclContext.

@@ -77,7 +77,7 @@ class A<T> {
return copy
}

var copied: Self { // expected-error {{'Self' is only available in a protocol or as the result of a method in a class; did you mean 'A'?}}
var copied: Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

You need tests showing that mutable properties and subscripts are rejected

@johnno1962
Copy link
Contributor Author

johnno1962 commented Apr 14, 2019

Thanks for the tips. I've pushed another commit which takes things forward from where they were yesterday when I was a little over-zealous in tidying up the code. Things generally work except I can't get the covariant applied type right when properties or subscripts returning self are used from a derived class.

For example using this code:

class A0<T, S> {
  var i = 88
  required init() {}
  var foo: (Self) -> () { return { (a: Self) in } }

  func copy() -> Self {
    let copy = Self.init()
    return copy
  }

  var copied: Self {
    let copy = Self.init()
    return copy
  }
  open subscript (i: Int) -> Self {
    return Self.init()
  }
}

protocol Prot3 {
  static func +(x: Self, y: Self) -> Int
}

class B: A0<Int, Double>, Prot3 {
  var j = 77
  static func + (x: B, y: B) -> Int {
    return 99
  }
}

print(B().copied.j)

The error I get is:

TYPE MISMATCH IN ARGUMENT 0 OF APPLY AT expression at [j.swift:81:7 - line:81:18] RangeText="B().copied."
  argument value:   %17 = begin_borrow %14 : $A0<Int, Double>       // user: %18
  parameter type: $B

or for print(B()[0].j)

TYPE MISMATCH IN ARGUMENT 0 OF APPLY AT expression at [j.swift:81:7 - line:81:14] RangeText="B()[0]."
  argument value:   %21 = begin_borrow %18 : $A0<Int, Double>       // user: %22
  parameter type: $B

Whereas print(B().copy().j) works of course, so what is the difference? This is probably going to need to be fixed in two places but where????? I hear what you're saying about Self in non-covariant position in non-mutable properties getting through. That may mean we have to give up.

@slavapestov
Copy link
Contributor

The type mismatch is happening because you did not emit a CovariantReturnConversionExpr. It should be easy to add one in the right place.

Also please don’t give up on diagnosing invalid Self usages :) if you want help I can do part or all of that refactoring. It’s long overdue anyway; I’ve wanted to get rid of FuncDecl::hasDynamicSelf() and the code that computes it for a while.

@johnno1962 johnno1962 force-pushed the Self-fallback-settables branch 7 times, most recently from 900b090 to 213c068 Compare April 15, 2019 14:46
@slavapestov
Copy link
Contributor

The merge conflict is because a branch in CSApply was taken for TypeDecl and VarDecl. The TypeDecl case didn’t make a lot of sense and necessitated a complementary hack in SILGen. You can ignore the new code and resolve the conflict triviallyZ

@johnno1962
Copy link
Contributor Author

johnno1962 commented Apr 15, 2019

I've fixed the conflict and I think I've taken things about as far as I can. After juggling the pieces for longer might be reasonable I was able to get CovariantReturnConversionExpr working in the finish (Thanks for the tip - on a Sunday no less) and it solved the type mismatch I was seeing. There may be other problems & I'm building a toolchain for more detailed testing. I'll not take on the "non-Covariant position, Self" refactor if you don't mind as it seems you have a pretty clear idea about how to go about it and I really don't have a clue!

@johnno1962
Copy link
Contributor Author

johnno1962 commented Apr 15, 2019

There is one known problem, if you have a subscript override in a subclass that returns Self, the solution is seen but rejected. For:

class A0<T, S> {
    var i = "Base"
    required init() {}
    var foo: (Self) -> () { return { (a: Self) in } }

    func copy() -> Self {
        let copy = Self.init()
        return copy
    }

    var copied: Self {
        get {
            let copy = Self.init()
            return copy
        }
    }
    open subscript (i: Int) -> Self {
        get {
            return Self.init()
        }
    }
}

protocol Prot3 {
    static func +(x: Self, y: Self) -> String
}

class B: A0<Int, Double>, Prot3 {
    var j = "Derived"
    static func + (x: B, y: B) -> String {
        return x.j + x.j
    }
    override func copy() -> Self {
        let copy = Self.init()
        return copy
    }
    override var copied: Self {
        get {
            return copy() as! Self
        }
    }
//    override subscript (i: Int) -> Self {
//        return Self.init()
//    }
}

print(B()[0][0].j + B().copied.copied.j)
print(B()[0][0] + B().copied.copied)

If you uncomment the subscript in the derived class it fails with:

j.swift:92:10: error: cannot subscript a value of type 'B' with an argument of type 'Int'
print(B()[0][0] + B().copied.copied)
      ~~~^~~
j.swift:86:12: note: found this candidate
  override subscript (i: Int) -> Self {
           ^

I was seeing this on Friday and thought it was fixed but evidently not. It seems you can only define a subscript returning Self on a class that has a generic signature at the moment. I'd love to know why!

@johnno1962
Copy link
Contributor Author

Hi @slavapestov, I found the constraint which is failing and causing subscript not to be recognised on non-generic classes. It is here: https://github.com/apple/swift/blob/master/lib/Sema/CSGen.cpp#L1105. If I comment this out things are OK. No sign of why generic classes aren't affected.

@johnno1962
Copy link
Contributor Author

johnno1962 commented Apr 16, 2019

The trail leads here: https://github.com/apple/swift/blob/master/lib/Sema/CSSimplify.cpp#L2222. The code doesn't seem to be prepared for DynamicSelf so perhaps it is best to give up on the constraint.

@johnno1962
Copy link
Contributor Author

johnno1962 commented Apr 17, 2019

Hi @slavapestov, I've pushed an ultimately deeply unsatisfactory hack to stop solutions for subscripts on non-generic classes being rejected. I spent quite a bit of time digging into the constraint system as I mentioned above but the simpler hack is probably the best route with the most localised effect. In the end the following was sufficient to replicate the problem:

class Z {
  subscript (i: Int) -> Self {
    return self as! Self
  }
}

print(Z()[0])

@johnno1962 johnno1962 changed the title [DNM] Allow non-mutable properties and subscripts returning Self Allow non-mutable properties and subscripts returning Self Apr 17, 2019
@slavapestov
Copy link
Contributor

I think the problem is that the non-generic case is not stripping off the DynamicSelfType somewhere, eg in getTypeOfMemberReference(). I don't want to add a boolean parameter to addConstraint(); imagine the effect it would have on code if such parameters accumulate over time.

@@ -2285,7 +2285,7 @@ static bool matches(CanType t1, CanType t2, TypeMatchOptions matchMode,

// Class-to-class.
if (matchMode.contains(TypeMatchFlags::AllowOverride))
if (t2->isExactSuperclassOf(t1))
if (t2->eraseDynamicSelfType()->isExactSuperclassOf(t1))
Copy link
Contributor

Choose a reason for hiding this comment

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

This wasn't necessary for method overrides, so it shouldn't be necessary to add this to make variable and subscript overrides work.

Copy link
Contributor

Choose a reason for hiding this comment

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

This might be the same underlying problem as subscript overrides not working in generic classes

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, figured this one out. You need to teach TypeBase::adjustSuperclassMemberDeclType() about SubscriptDecl and VarDecl returning Self too.

I feel like the bit we want to factor out here that comes up in several places is the check for FuncDecl/ConstructorDecl/VarDecl/SubscriptDecl followed by a call to replaceCovariantResultType() with the right uncurry level. But we can clean that up later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After a few attempts I was unable to adapt TypeBase::adjustSuperclassMemberDeclType() for properties and subscripts.

@slavapestov
Copy link
Contributor

I'm going to build your branch and poke around a bit to see what's going on.

@slavapestov
Copy link
Contributor

@johnno1962 I found the problem -- ConstraintSystem::getEffectiveOverloadType() has to be updated for subscripts returning Self (and properties too; please test this case as well). Here's a diff for ConstraintSystem.cpp that seems to fix the problem, but please test it more thoroughly:

@@ -1445,6 +1449,13 @@ Type ConstraintSystem::getEffectiveOverloadType(const OverloadChoice &overload,
 
       if (doesStorageProduceLValue(subscript, overload.getBaseType(), useDC))
         elementTy = LValueType::get(elementTy);
+      else {
+        Type selfType = overload.getBaseType()->getRValueType()
+            ->getMetatypeInstanceType()
+            ->lookThroughAllOptionalTypes();
+        if (elementTy->hasDynamicSelfType())
+          elementTy = elementTy->replaceCovariantResultType(selfType, 0);
+      }
 
       // See ConstraintSystem::resolveOverload() -- optional and dynamic
       // subscripts are a special case, because the optionality is

@slavapestov
Copy link
Contributor

@johnno1962 Something else we can look at is making sure that 'self' has DynamicSelfType inside the getter of a property or subscript returning Self. This will allow you to write:

class C {
  var foo: Self {
    return self
  }
}

@johnno1962 johnno1962 force-pushed the Self-fallback-settables branch 2 times, most recently from 8749015 to f33ebb5 Compare April 18, 2019 11:32
@johnno1962
Copy link
Contributor Author

johnno1962 commented Apr 18, 2019

HUGE THANKS @slavapestov, your fix works and I've updated the last commit to remove my "hack". At this point I have to had this over to someone who actually knows what they are doing. What started as a brainwave a month ago has rather ballooned into quite a time commitment. I've gone about Self detection in the wrong way to get things working but can someone else please pick up writing checkDynamicSelfResult() as you mention. Sorry about this.

@johnno1962 johnno1962 force-pushed the Self-fallback-settables branch 2 times, most recently from 1cf6d4d to b8fc0bd Compare April 18, 2019 13:50
@johnno1962
Copy link
Contributor Author

Tests are running at the moment so this is ready to merge if you have no other changes?

@johnno1962
Copy link
Contributor Author

johnno1962 commented Apr 29, 2019

Hi @slavapestov, do you have time to wrap this PR up? I've pushed a few additional tests where you can see the current error reporting. It's getting late in the day to merge with 5.1 so perhaps you could cherrypick the narrow fix #23853 banning aliases and we can look at this more risky change for 5.2. Can you run a source compatibility test for me please?

@slavapestov
Copy link
Contributor

@johnno1962 Sure, I'll take this over. Thank you!

@slavapestov
Copy link
Contributor

I haven't forgotten about this. I'm going to wrap it up soon and see what I can cherry pick to 5.1.

@johnno1962
Copy link
Contributor Author

Thanks @slavapestov. If you don’t have time to tidy it up #23853 is the more conservative option.

Type SelfType = nominal->getSelfInterfaceType();
if (insideClass)
if (nominalDC->getSelfClassDecl() != nullptr)
Copy link
Contributor Author

@johnno1962 johnno1962 Jun 3, 2019

Choose a reason for hiding this comment

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

Just a final idle thought @slavapestov. Should this line be:

      if (nominalDC->getSelfClassDecl() && !nominalDC->getSelfClassDecl()->isFinal())

Copy link
Contributor Author

@johnno1962 johnno1962 Jun 3, 2019

Choose a reason for hiding this comment

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

It could help with this problem: #22863 (comment)

@slavapestov
Copy link
Contributor

Ok, I'm working on this now.

@johnno1962
Copy link
Contributor Author

johnno1962 commented Jun 20, 2019

Great news. There shouldn’t bee too much left to do except tidying things up. Should I close this PR? I'm trying to get extension ProtocolA: ProtocolB working at the moment. Are you available for the odd question?

@johnno1962
Copy link
Contributor Author

Superseded by #25742

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants