If I understand correctly, the current feature (and its implementation) do not change the "physical" ABI for variables declared @const
; such variables are accessed by calling a function that returns the value, i.e. with a getter. At most, @const
becomes a semantic guarantee that the function has no significant side effects and always returns semantically-equivalent values, which we could certainly use during optimization by e.g. coalescing redundant calls or removing unused ones. However, it would still be necessary to destroy the returned value (if it isn't trivial), because the return values wouldn't necessarily be permanently allocated, which would complicate that optimization.
This is clearly not the optimal ABI for accessing constant variables, which would be either:
- to make the variable simply resolve to an address known to be initialized prior to the access, or if not that,
- to call an "addressor" function which returns a consistent address of an immutable value.
In the first option, a @const
global variable would define a symbol that simply resolves to the address of an immutable object known to be initialized at load time, and a @const
protocol requirement would cause the protocol witness table to contain a pointer to an immutable object initialized either at load time or, at worst, during the initialization of the witness table (e.g. if the conformance were generic and the value was dependent on generic parameters). In the second option, a @const
global variable would define a global function symbol for the addressor, and a @const
protocol requirement would cause the protocol witness table to include an entry for an addressor (rather than for a getter, as it would today). The addressor would then either just return the address of an immutable object, if one can be emitted statically, or else memoize the allocation and initialization of that object.
The first of these is clearly more efficient for accesses. It would also allow the value to be fairly easily recovered by binary analysis (as opposed to either source analysis or running code — all of these options have their own trade-offs for different applications). However, it would require the memory to be eagerly initialized before access, which in the most general case would require load-time execution in order to compute type layouts and produce unique metadata. To avoid this and guarantee that the initialization could be done "statically" (which is to say, within the limitations of what common program loaders can do automatically without running any code from the loaded image), the following restrictions would be required:
-
Initialization would have to be resolved to a fully concrete initializer value. This means that all of the semantics of initialization for the initializing expression would have to be known to the variable's defining module and, furthermore, be constant-evaluable. Among other things, this would imply that all the types involved are frozen
(or defined in the module), all the initializers are inlinable
(or defined in the module), etc. We seem to want this restriction regardless, and in the initial proposal the restrictions are much stricter than this and exclude all user-defined types; I mention it only for clarity.
-
The internal layout of every component value in the initializer value would have to be known statically. This is almost implied by the restriction above, since resilient types cannot have inlinable
initializers. However, the internal layout of an enum
includes direct cases that aren't necessarily part of the value and therefore do not need to be initialized; if such a case included a non-frozen
type, this would preclude the direct-address implementation because the internal layout of the enum would not be known, even if that case were not chosen for the actual initializing value. Again, I believe this is implied by the current restrictions in the proposal, but it should be noted for future directions.
-
Any class instance or metatype value appearing in the initializing value would have to fall into one of the cases where Swift's type metadata system can guarantee complete emission at compile time. (This is an implementation detail we haven't previously needed to expose to programmers.) For example, if the initializing value includes an instance of a generic class MyClass<MyX>
, both MyClass
and the concrete generic argument MyX
would be heavily restricted. Some of the conditions for this overlap with the restrictions above, but not all of them. I believe this restriction probably wouldn't extend to indirect enum cases. Once more, I believe this is implied by the current restrictions in the proposal, unless perhaps we need unique metadata for array and dictionary buffers.
These restrictions would not be necessary with the "addressor" approach, which allows the variable to be emitted lazily at the cost of making accesses somewhat more expensive. But someone might say that achieving the direct-address implementation would be desirable enough to design these extra restrictions in. I'm not sure I would agree, but it's not completely unreasonable.
The most important thing here is that, if we want to be able to use either of these better ABIs for @const
variables, we do actually need to do that ABI work in the first release. Otherwise, @const
alone won't be enough, and we'll need to introduce a new attribute in the future which actually requests the new ABI treatment. That seems to me like it would partially undermine the story laid out in this proposal for how the proposed attribute will be gradually generalized to address more and more constant-evaluation needs. @const
would enable some semantic optimization and source-tool analysis, but we'd be fundamentally limited by these early ABI decisions about what low-level features we could build on top of it. For example, we would not be able to say that a pointer to a @const
variable has global lifetime.
If we want the direct-address ABI specifically, then as part of that ABI work, we will also need to ensure that we can actually emit String
, Array
, and Dictionary
literals statically, since those are the only complex types allowed by the current proposal. The optimizer would then presumably be free to rely on a guarantee that any allocated objects in the immutable object are in fact permanently allocated and have trivial reference counting. This work could be elided if we just use addressors, although of course the optimizer would then lose its ability to rely on permanent allocation. But permanent allocation might not actually be feasible in the long run if we hope to include general class types in the set of things that can be constant-emitted, since a class instance stored in a @const
generic variable would semantically need to be unique for a set of generic arguments.