SE-0359: Build-Time Constant Values

What if the initializer is @alwaysEmitIntoClient? Would this still require the type to be @frozen?

If it doesn't just delegate to another initializer, yes. You cannot possibly do initialization if you don't know the set of stored properties that have to be initialized. All of this should be laid out in the resilience documentation from years ago.

That's for structs and classes, of course. Non-frozen enums can be semantically initialized to a known case; however, that does not mean there is an implementation path for actually emitting a constant object for them.

2 Likes

Ok, I think it’s been helpful to spell this out. Thanks.

It would be nice if we could at least get the guarantee that @const values are not subject to ARC. That should be easy - regardless of static initialization, we should be able to guarantee that once a @const value is initialized, it will never be deinitialized and thus does not need reference counting.

Neither the terms "ARC", "reference" or "lifetime" appear in the proposal. Did I miss it or is this not guaranteed?

A global constant object still needs to be usable to pass to functions that traffic in not-necessarily-constant objects, so needs to be a valid argument to swift_retain and _release even if it never gets deinitialized. We can optimize out retains and releases when we know the object is global, and the runtime can fast-path objects it recognizes as being immortal at runtime.

3 Likes

Sure, but if we have a value whose type is, say, @const Array<UInt8>, then there's no reason the compiler should ever try to retain/release that value, is there?

I've recently been implementing Unicode domain names (IDNA) in Swift, and I found that it actually is viable - I'm actually quite happy with the performance, and the initialization overheads are minimal. The compiler can already reliably emit data in to RO arrays -- well, provided that you stick to integer literals (or unicode scalar literals). This is the generated table - it is predictably an enormous goop of numbers, but it represents a database with this structure in Swift. I'm actually able to use that exact same code both in a script ahead of time to create the database, and at runtime.

I've learned a lot about what works and what doesn't right now with this kind of thing in Swift, and by far the biggest issue I had to overcome to get reasonable performance was ARC. It would be nice if these values could come with the guarantee that they won't be reference counted if you know they're @const.

4 Likes

If you pass that value to a function that does not have @const on that parameter, the implementation of that function may need to call retain/release on the value (if the calling convention for that function does not “guarantee” that parameter).

There is actually a (niche) use case for this feature. SwiftUI’s ForEach takes a range that needs to be a compile-time constant and now relies on an underscore-prefixed semantics attribute to emit a warning. IIRC, OSLog also uses compile-time strings in most/all of its methods.

Couldn’t the same be said for @inlinable? I think that perhaps const is too short a name for quite a niche feature. Also, since this term is also used differently in other languages, I understand how this could lead to @const overuse. However, I don’t see why that overuse would be significant.

Interesting. Would the next “useful” proposal relying on constantness need to also enable/propose this attribute, or would this proposal just go into effect.

1 Like

This is quite different than the proposal, which lists every motivating benefit as requiring something from the Future Directions to actually bring to fruition. Hence my complaint that this proposal does not enable anything that the language or compiler can't do today.

If the proposal was to codify an existing feature with a supported and public spelling, it would be written quite differently and the discussion would be entirely different. Especially as codifying private behavior which also happens to open up the future directions is quite different than creating a new behavior out of whole cloth that doesn't actually do anything but turn on some LEDs on the control panel.

10 Likes

I'm a bit concerned about using the word const, because of its heavy use in other languages (most notably, the C family of languages), which usually means cannot be changed once initialized (the Swift equivalent of which would be a simple let declaration).
Since the purpose of this attribute is to force the attributed declaration to be available at build time, the mismatching semantics pf the word const would be (and already is to me) very confusing. C++'s attempt to clarify was to use the word constexpr, which at least has the word expr in it, hinting at the parsing phase of the compilation process. I'd argue that something like compileTime or buildTime makes the intent much clearer. If these versions of the spelling seem a bit wordy, I'd want to see how that can be offset by upcoming attribute inference rules.

25 Likes

This is a useful building block towards a future with rich support for compile-time constructs. It is a significant problem and I am glad to see the first steps finally come to review. I have wished for this feature in certain scenarios and have some passing familiarity with analogous features in C++, and I've read and considered previous iterations of this proposal as well as this version that's come to review.

Now as to the overall fit into the language and how this feature compares to that in other languages:

I think a discussion of the "virality" of such an annotation, and some sense as to how it fits into the next steps described for Swift's compile time constructs (not necessarily a whole manifesto, but a degree of exploration beyond noting the future direction of const inference) would help to evaluate this proposal. Yes, the feature has parallels in other languages, but naturally it leads to questions whether any shortcomings also carry over which might be allayed by a larger field of view.

I am wary of the naming, even as I know that bikeshedding has already been extensive. Still, with C++'s const and constexpr and consteval being prior art, a naked @const in Swift is glaring for its lack of clarity about the built-time aspect of its const-ness. So I want to raise a suggestion I haven't seen before, phrased as a question: Is @const to values in some ways as @frozen is to types, at least with respect to knowableness and ABI impact? If this thought has any hint of validity, any reason not to repurpose @frozen for variable bindings and parameters?

14 Likes

+1 for this. But I have a small question.
Is there any reason that the keyword “@const” is chosen instead of “const”?

This is discussed in the proposal text, under "Alternatives Considered."

4 Likes

I agree with Avi that I find it difficult to support this proposal. The only motivating example in the proposal that is not solved by a future direction is the one with a clamping property wrapper, but even in that case the problem it solves is so minor that it doesn't seem worth adding to the language to fix.

Yes, you can guarantee that the bounds you initialize with are known at compile time, but the bounds themselves are still runtime properties. I don't see the point.

Without a new motivating example which is actually solved by the proposed feature, I cannot support this proposal. -1

7 Likes

I appreciate the potential parallel, but it strikes me that @frozen is making a much stronger, and higher level, promise than the proposed @const.

The @frozen annotation makes a promise at the level of library evolution, that is, it makes a promise that is relevant across different versions of the same library. It is a promise about how the code itself will change, not just a promise about something that happens at the instant of a single compilation. Notably, @const is not relevant only for resilient libraries.

I could see an argument that @const values should also be required to be marked @frozen in resilient libraries (as per the Effect on ABI stability and API resilience section), but I think it's better if @frozen remains a concept that is only relevant in library evolution mode.

5 Likes

I’m not sure how much stronger @frozen is for public types than @const is for public properties:

The new function parameter attribute is a part of name mangling. The value of public @const properties is a part of a module's ABI. See discussion on Memory placement for details.

At the level of library evolution, this promise not to change the value of an ABI-visible @const member seems ironclad (deep-freezed?); indeed, there are a restricted but nonzero number of things one can change about a @frozen type, which is more than what I can see changing about a @const value.

Beyond guarantees regarding how types may change in the future, @frozen also enables exhaustive switching for an enum in the here-and-now. Although the attribute @frozen is only used in resilient libraries, the concept of a frozen enum exists in the language whether or not you’re dealing with a resilient library. It’s in fact nonfrozen enums that exist as a concept only in library evolution mode.

I agree with you that, in terms of how these two features have been presented in their respective proposals, there has been a difference in emphasis on the evolution versus here-and-now consequences of using these attributes. But I’m not convinced that when we peel back to study the features themselves @const is really distinguished from @frozen on this basis.

4 Likes

Agreed that for resilient libraries, @const is highly analogous to @frozen. But that doesn't, in my view, translate into an argument that we should bring @frozen into the realm of non-resilient libraries.

I'd view this in the other direction—an exhaustive enum is the underlying concept here, and in situations where source is available, every enum is exhaustive because every compilation is in a sense, independent. I don't think it's really reasonable to call enums in non-resilient libraries "frozen", since they are of course allowed to change, add cases, remove cases, etc.

Agreed, though I'd again shift the framing to "nonexhaustive enums" being the concept that only exists (and is indeed the default) in resilient libraries. Then, @frozen is the way for resilient libraries to recover the (non-resilient default) exhaustive behavior for enums.


Aside from concerns about diluting the seriousness of the promise made by @frozen by bringing it to non-resilient libraries, I think it's just the wrong word to describe what's going on. IMO it fails the same test you mentioned above for @const:

Whereas @const may difficult to distinguish in meaning from the existing "cannot change across a single execution of the program", @frozen goes too far in the other direction and (IMO) expresses "cannot change across multiple compilations of the library." We're looking for a label that expresses something in the middle, roughly, "cannot change across multiple executions of the same compilation of the program."

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:

  1. to make the variable simply resolve to an address known to be initialized prior to the access, or if not that,
  2. 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.

15 Likes

Is this essentially talking about the cost of the call to swift_once?

Because in all the benchmarking I've done, swift_once is always negligible. Like, 0.0%.

Currently, there is also a lot of setup code for globals in main. I've never understood why we have that setup code and have thread-safe lazy initialization. Why both?

1 Like

It would be the dynamic cost of swift_once plus the code-size and optimization-barrier costs of doing the setup for it, yes.

I’m not aware of any eager setup costs for normal globals that would go into main. But globals in script files are strange and have their own, not always sound rules.

3 Likes