I've been putting off responding to the "frozen types layout" question because it's complicated. :-) @Slava_Pestov and I thought about a very similar problem quite a bit, so I'm going to explain that problem first and then try to connect the two.
Here's the similar problem. Pretend that CoreGraphics, an Apple framework, forgets to mark CGPoint, a simple struct containing two CGFloats, as frozen. This would be terrible cause we do math on points all the time, as well as perhaps storing big collections of them for things like "drawing bƩzier curves" or "tracking mouse movement". So in dishwasherOS 2, CGPoint gets this new "frozen-after-the-fact" attribute.
// Fake syntax not being proposed, do not criticize
@frozen(dishwasherOS 2)
public struct CGPoint {
public var x, y: CGFloat
// ā¦
}
Okay, great. This has a few effects:
-
Because CGPoint wasn't frozen from the start, any existing functions with CGPoint parameters or return values still have to pass them indirectly. This isn't terribleāit's incredibly common in C++ with templates that always take referencesābut it's a litte unfortunate.
-
Any new API introduced in CoreGraphics in dishwasherOS 2 can avoid this indirection in theory. After all, it's never existed in a world where CGPoint wasn't frozen.
-
App targets and packages built from source can avoid this indirection in their own functions if they have a new enough deployment target.
-
There's still lots of other benefits here that don't have to do with parameter passing: any manipulation of local variables can be done in registers if the struct is small enough, Array iteration doesn't need to ask what the stride is at run time, etc.
Now consider a library that uses CGPoint that's not shipped with the OS, but still has library evolution support turned on, i.e. it's important to maintain binary compatibility.
// MoreGraphics.swift
extension CGPoint {
public var distanceFromOriginSquared: CGFloat { x*x + y*y }
}
Here's the weird thing. If MoreGraphics has a minimum deployment target of dishwasherOS 1, distanceFromOriginSquared
has to operate on the CGPoint indirectly. But changing your minimum deployment target should also not break binary compatibility, if you're vending a binary framework, meaning that even with a minimum deployment target of dishwasherOS 2, distanceFromOriginSquared
still has to operate on the CGPoint indirectly. What you really need is "what was my original minimum deployment target", which we don't currently have an option to provide.
It actually gets weirder. If I had written this instead
// MoreGraphics.swift
extension CGPoint {
@available(dishwasherOS 2, *)
public var distanceFromOriginSquared: CGFloat { x*x + y*y }
}
then distanceFromOriginSquared
could potentially manipulate self
directly. Why? Because now there's a guarantee that there aren't any clients older than dishwasherOS 2, and there never wereābecause that would have been a breaking change. But that means making a distinction between "module-wide minimum deployment target" and "availability annotations on individual declarations", when normally we don't bother to do that.
(This also applies to the far-future potential feature of versions that are independent of the OS, discussed in docs/LibraryEvolution.rst.)
Everything that applies to MoreGraphics applies to inlinable code in CoreGraphics itself, by the way, since inlinable code can't assume it's going to be run against the same version of the library. Essentially, the minimum deployment target of a library with evolution support doesn't mean anything to its clients.
So, to close out, everything gets the benefits of the frozen-after-the-fact except for API that already exists in libraries with evolution supportāboth the library that defines a type and other libraries that use it.
Changing frozen layout has similar issues. Any struct that's explicitly frozen now can't take advantage of the new frozen layout without breaking its binary interface, but neither can we bump the default based on a particular compiler version or minimum deployment target, because people change those for other reasons and that shouldn't break binary compatibility. So we are boxing ourselves in a little.
That said, I don't know if this is ultimately important enough to matter. Yes, the C layout algorithm isn't very good, but at the time when you freeze your type, you can also reorder your stored properties to minimize padding. That's not a great answer because (1) it means you can't use order for presentation unless you hide your storage behind computed properties, and (2) it's a manual process that we don't make transparent. But it's something. It's only if we expected the default frozen layout to do things like packing Bools into another field's spare bits that this would really become important, and I feel like it's okay to leave tradeoffs like that on the table when not explicitly requested.