Inlinable initializers vs. property initial value expressions

Hey, all. @Slava_Pestov brought up a problem when trying to use the (not-yet-approved) inlinable attribute on a struct constructor: it ends up trying to reference the tiny functions we generate to set the initial value of each property. This is a problem because those initial-value functions are not inlinable (right now), but nor should they have public symbols (ideally).

For a resilient library there is a clear answer here: you can't make a non-delegating struct constructor inlinable unless the struct is fixed-contents/fixed-layout/frozen. That's a requirement anyway because you need to know what properties the struct has, and it's easy enough to have that annotation imply "initial-value functions must be inlinable", with the semantic restrictions that entails.

However, it's less clear what to do for non-resilient libraries, i.e. the normal libraries everyone builds. Originally Slava and I planned to have the same restriction, but since everything in a non-resilient library exposes its layout the attribute suddenly only mattered for this one little thing (whether initial-value functions were inlinable or not, which in turn controls whether initializers can be inlinable). That seemed like overkill to me and Doug (after some thought), especially if inlinable gets formally added to the language this year (SE-0193) but not this other annotation on structs. (I actually have a good chunk of a proposal written for that, but don't want to bring it up until SE-0193 and SE-0192 are done.)

@John_McCall, Slava, and I talked this over and came up with several options, none of which are perfect:

  1. (original plan) Non-delegating struct initializers cannot be marked inlinable unless the struct is marked "fixed-contents" in some way (annotation to be designed). That annotation would make property initial value expressions inlinable, and enforce the usual restrictions about that. Has the advantage that it's the same restriction with or without resilience enabled, but the disadvantage that this is the only time the to-be-designed annotation is interesting in normal libraries (at least for now).

  2. Non-delegating struct initializers cannot be marked inlinable unless all of the struct's properties with explicit initial value expressions are marked as "inlinable" in some way (annotation to be designed). (I'd be a little sad if we spell this @inlinable as well because I really wanted that to mean "you can rely on this property being stored forever" in resilient libraries.)

  3. Non-delegating struct initializers cannot be marked inlinable if the struct has any properties with explicit initial value expressions. This stinks if you have a bunch of initializers and you end up repeating code, but it is simple.

  4. If there's an inlinable non-delegating struct initializer anywhere in the module, the property initial values are implicitly made inlinable. This could lead to the initial value expressions being type-checked and SILGen'd multiple times during a build, however, if such an initializer is defined in a separate file from the struct.

  5. Same as (4), but inlinable non-delegating struct initializers are disallowed outside of the file where the struct is defined. Solves the problem with (4) in exchange for a somewhat arbitrary restriction.

  6. (what's currently implemented) Property initial value expressions are given public symbols, so they can be referenced from inlinable initializers. This "works" but it means the initializer can't be completely inlined away; there's always going to be a call to some opaque code when used across library boundaries.

Note that I referred to "explicit initial value expressions" a few times; I think even if we pick one of the more restrictive options we'd still want to make an exception for the implicit = nil for Optional properties and the hidden initialization for lazy properties, as if they were written out in each initializer.

Thoughts, everyone? IIRC, where the three of us left off Slava was still leaning towards (1), I was leaning towards (5), and John was leaning towards (4).

P.S. I've been talking about structs, but classes have the same problem, really.

1 Like

I think that's an excellent summary of the issue, thank you.

Jordan is right that I think (5) is the right language direction. (4) is a minor restriction on (5); the motivation seems a bit strange to me (it's taking a somewhat improbable corner case, an inlinable non-delegating initializer defined outside of the type's defining file, and outlawing it purely to so that we can make a slightly stronger statement about what type-checking work is necessary in incremental builds), but I still see it as basically acceptable. All the other proposed rules seem to be steps in an unfortunate semantic direction, namely making property initializer expressions real independent entities in the language.

1 Like

I could be wrong here, but it seems you forgot to finish this statement?

Ah, no, it's all one condition. Rephrasing, it would be "However, if an inlinable non-delegating struct initializer is defined in a separate file from the struct, (4) could lead to initial value expressions being type-checked and SILGen’d multiple times during a build". Sorry for the confusion!

1 Like

Thank you for bringing this up. I fully agree with @John_McCall here.

For what it’s worth, I don’t think it would actually need to be SILGen’ed in multiple files (unless that’s how the use-analysis is done?); the current code-generation pattern of emitting it as a separate function is a workable implementation approach with some advantages. I just don’t think that decision should affect users.

1 Like