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

(Not for checkin) Fix convenience init to protocol extension init delegation #11635

Conversation

slavapestov
Copy link
Member

@slavapestov slavapestov commented Aug 26, 2017

Consider a class initializer that delegates to a protocol extension initializer, for example

protocol P {}
extension P {
  init() { ... }
}

class C : P {
  convenience init(foo: Int) { self.init() }
}

The self.init() inside the class initializer must pass a substitution for the Self type parameter to the protocol extension method. It was passing the static type, not the dynamic type. This is wrong because convenience initializers can be inherited, so calling an inherited initializer would use the wrong 'Self' type. This was a problem if the protocol extension method either had 'Self' in contravariant position (in which case we might type check invalid code) or if it constructed a new instance of 'Self' and returned it (in which case we would do the wrong thing at runtime).

Fix the problem and add tests for both cases.

Also clean up some related code I looked at along the way.

…elfDecl()

NFC for now, but is required for correctness once a subsequent
patch gives 'self' a DynamicSelfType in convenience initializers.
…ntReturnConversionExpr

The UncheckedRefCast instruction supports optional-to-optional
casts natively, so we can avoid the unnecessary conditionals
here.
NFC for now, but becomes important once a subsequent patch gives
'self' a DynamicSelfType in convenience initializers.
…lfType

We don't want to print 'Self' here; the actual concrete type
is more useful.

NFC for now, but becomes important once a subsequent patch gives
'self' a DynamicSelfType in convenience initializers.
…te to protocol extension initializers

A protocol extension initializer creates a new instance of the
static type of Self at the call site.

However a convenience initializer in a class is expected to
initialize an instance of the dynamic type of the 'self' value,
because convenience initializers can be inherited by subclasses.

This means that when a convenience initializer delegates to a
protocol extension initializer, we have to substitute the
'Self' type in the protocol extension generic signature with
the DynamicSelfType, and not the static type.

Since the substitution is formed from the type of the 'self'
parameter in the class convenience initializer, the solution is
to change the type of 'self' in a class convenience initializer
to DynamicSelfType, just like we do for methods that return
'Self'.

This fixes cases where we allowed code to type check that
should not type check (if the protocol extension initializer
has 'Self' in contravariant position, and we pass in an
instance of the static type).

It also fixes a miscompile with valid code -- if the protocol
extension initializer was implemented by calling 'Self()',
it would again use the static type and not the dynamic type.

Note that the SILGen change is necessary because Sema now creates
CovariantReturnExprs that convert a static class type to
DynamicSelfType, but the latter lowers to the former at the
SIL level, so we have to peephole away unnecessary unchecked_ref_cast
instructions in this case.
@slavapestov
Copy link
Member Author

@swift-ci Please test source compatibility

@slavapestov
Copy link
Member Author

@swift-ci Please smoke test

@slavapestov
Copy link
Member Author

@parkera With my change swift-corelibs-foundation no longer builds:

Foundation/NSNumber.swift:266:73: error: cannot convert value of type 'NSNumber' to closure result type 'Self'
        self.init(factory: { unsafeBitCast(cfnumber, to: NSNumber.self) })
                                                                        ^

The new behavior is technically correct because this delegation is performed from a convenience initializer. Convenience initializers can be inherited, therefore it is not safe to replace 'self' with an 'NSNumber', since you might subclass 'NSNumber'.

I can think of three possible fixes:

  • Make the new behavior change conditional on -swift-version 5, avoiding the breakage for now. However this just punts the question until a later date (so even if we decide to make it conditional, we should think about the two remaining options below).

  • Declare NSNumber as final. This seems like the safest choice, because an unsafeBitcast() of a CFNumber to a subclass of NSNumber (which may add stored properties) is not going to do the right thing at runtime given how the code is written today.

  • Do a hack with generics, eg, replace

self.init(factory: { unsafeBitCast(cfnumber, to: NSNumber.self) })

with something like

// define a local function just to get the desired result type...
func castCFNumber<T>() -> T {
  return unsafeBitCast(cfnumber, to: T.self)
}

self.init(factory: castCFNumber)

This is the most general solution. It will allow NSNumber subclasses to be defined as before, but if they add extra stored properties you'll get undefined behavior at runtime.

@slavapestov
Copy link
Member Author

cc @jrose-apple

@slavapestov
Copy link
Member Author

Ok, I see you're defining a couple of NSNumber subclasses there, so making it final is a no-go.

I'll try the generics hack.

@slavapestov
Copy link
Member Author

apple/swift-corelibs-foundation#1190
@swift-ci Please smoke test Linux

@slavapestov slavapestov changed the title Fix convenience init to protocol extension init delegation (Not for checkin) Fix convenience init to protocol extension init delegation Aug 26, 2017
@itaiferber
Copy link
Contributor

cc @phausler

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.

None yet

2 participants