> On Dec 22, 2017, at 1:29 AM, Slava Pestov <spes...@apple.com> wrote:
> 
>> On Dec 21, 2017, at 12:42 PM, Paul Cantrell <cantr...@pobox.com 
>> <mailto:cantr...@pobox.com>> wrote:
>> 
>> 1. Presumably the portions of A inlined into B and C remain sensitive to the 
>> version-specific memory layout of A? Or will ABI stability mean that the 
>> compiler can magically rearrange memory offsets in already-compiled code 
>> when the layout changes? (Apologies if this is a too-obvious question; this 
>> part of Swift is all a mystery to me.)
> 
> There is not really a notion of memory layout at the level of an entire 
> module. For structs, classes and enums, you pretty much have the same 
> concerns with both inlinable and non-inlinable functions — if the framework 
> author can change the stored property layout of a struct or class (or adds a 
> case to an enum), code that manipulates these data types must not make any 
> compile-time assumptions that might be invalidated at runtime with a newer 
> version of the framework.
> 
> This is basically what the upcoming @fixedContents proposal for structs is 
> about — giving framework authors a way to trade future flexibility for 
> performance by allowing the compiler to make assumptions about the layout of 
> a struct as it is written at compile-time. The @exhaustive proposal for enums 
> has a similar implementation angle, but is of course more interesting because 
> it affects the source language as well, with switch statements.
> 
> We don’t plan on any kind of resilience opt-out for classes — already in 
> shipping Swift compilers, accesses to stored properties of classes use 
> accessor methods and not direct access across module boundaries.

Thanks, this is quite helpful.

My underlying concern here is that understanding even what kinds of breakage 
are _possible_ due to inlining currently requires fairly detailed knowledge of 
Swift’s guts, and even the best-intentioned among us are going to get it wrong. 
We’ll need tool help reasoning about it, not just documentation. At least I 
know _I_ will! (Actually, I’ll probably just avoid @inlinable altogether, but 
I’d certainly need tool help if I ever do use it.)

> 
>> 2. Is there some class of statically identifiable breaking changes that the 
>> compiler does (or should) detect to flag incompatible inlined code? e.g. 
>> some version of A inlined into B references A.foo, then A.foo is deleted in 
>> a later version of A, so mixing older B with newer A in a project gives a 
>> compile- or link-time error?
> 
> This is what an “ABI differ” tool would achieve, but like I said it has not 
> yet been designed.

Yes. I would certainly use such a tool if it existed, and not just for dealing 
with @inlinable.

> 
>> 3. Does this need some sort of poison pill feature for other sorts of 
>> breaking changes that are not statically detectable? e.g. invariants of a 
>> data structure in A change in release 2.0, so the author of A says “it is an 
>> error to include A ≥2.0 in any project that inlined any of my code from a 
>> version <2.0.” Is this what you were getting at with the mention of 
>> @inlinable(2.0) in the proposal? Sounded like that part was about something 
>> else, but I didn’t really grasp it tbh.
> 
> This is an interesting point and I think it is outside of the scope of these 
> proposals. If the ABI of a library changes in an incompatible manner and 
> previous binaries are no longer compatible with it, you should think of it as 
> shipping a *new* library, either by changing it’s name or bumping the major 
> version number, so that the dynamic linker prevents the client binary from 
> being run in the first place.

If the compiler/linker actively prohibits mixing of inlined code from different 
major version numbers, that eases my concern somewhat. A library author isn’t 
stuck with an ABI-sensitive mistake until the end of time.

> 
>> Yes, frameworks+app built simultaneously are clearly the more common case. 
>> Though Carthage seems to be champing at the bit to create this problem, 
>> since it added a feature to download prebuilt binaries long before ABI 
>> stability! I can easily imagining this feature spreading via word of mouth 
>> as a “secret go faster switch,” and causing no end of problems in the wild.
> 
> Perhaps, but I still think it is strictly better to formalize the feature 
> through a proposal and document the pitfalls carefully — the underscored 
> attribute is already spreading through word of mouth and in the absence of 
> official documentation the potential for abuse is greater.

Fair point. Making this feature public & documented, albeit ill understood, is 
a safety improvement over undocumented & even iller-understood!

> 
>> It might be safer — and better match the understanding of the typical user — 
>> to have @inlinable assume by default that an inlined version of any given 
>> method is only valid only for the specific version of the module it was 
>> inlined from. The compiler would by default flag any version mixing as an 
>> error, and require an explicit statement of compatibility intent for each 
>> piece of inlinable code to opt in to the danger zone of mixed versions.
> 
> How would this be implemented?

Spitballing (and still out of my depth, so thanks for bearing with me!):

Each inlinable thing comes with a minimum version number for backwards 
compatibility:

    (syntax is just a placeholder here, could be Chris’s or whatever)

    // module A version 3.0
    @inlinable(≥ 3.0.0) func foo()  // introduced in 3.0

    // module A version 3.1
    @inlinable(≥ 3.0.0) func foo()  // we maintained compatibility this time

    // module A version 3.5.1
    @inlinable(≥ 3.4.0) func foo() // now dependent on something introduced in 
3.4

(Aside: at first I thought this should be a range, but thinking it through, I’m 
not sure that an _upper_ bound serves any useful purpose.)

Any @inlinable declaration that does not explicitly state a backwards 
compatibility version in the source code defaults to the current version, i.e. 
Swift assumes breaking changes every time if the author hasn’t specified 
otherwise:

    @inlinable func foo()  // in source for version 3.5.2

    @inlinable(≥ 3.5.2) func foo()  // in ABI

Each module’s ABI specifies both the version number and compatibility version 
of each item inlined:

    // module B ABI

    requires module A
    inlined func A.foo() from 3.5.1, compatible with ≥ 3.4.0

Or perhaps it would be sufficient to emit only the maximum of the compatibility 
versions of all the items that were inlined:

    // module B ABI

    requires module A
    inlined some items from 3.5.1, compatible with ≥ 3.4.0

And then the key: at compile/link time, for every framework X, every version of 
X present — including both inlined versions and the version we’re actually 
building against — must satisfy the compatibility versions of all the items 
from X that were inlined by other frameworks.

In other words,

    min(versions of X present) ≥ max(inlined backward compatibility versions of 
X present).

• • •

This satisfies the three major scenarios I’m pondering:

(1) The stdlib and other similarly brave libraries can support long-term 
stability of inlined code.

(2) “I just build all my frameworks at the same time” users can use @inlinable 
safely, without shooting themselves in the foot when version mixing happens.

(3) A team of type #1 can safely make inline-breaking changes or correct 
mistakes.

• • •

An alternative would be a simpler, coarser-grained policy that says inline 
compatibility is either (1) same patch version, the default, or (2) same major 
version, i.e. the only choices are either “I’m not thinking about inline 
compatibility at all” or “I commit to using semantic versioning even for binary 
compatibility, not just source compatibility.” That approach smells funny to 
me, feels like it’s going to be clumsy in practice, but I could see the 
argument for it.

Cheers,

Paul

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to