SE-0359: Build-Time Constant Values

Constant folding and propagation are orthogonal to elimination of static constructors. Furthermore, compiler features that don’t affect surface level semantics are not typically brought through the evolution process.

I can find nothing in the proposal to intimate that static initializers are eliminated. The compiler is already able to do that if the property can be replaced with its value at build time. This proposal is not adding that ability.

4 Likes

I think we should not treat build-time evaluated functions (initializers) the same as usual ones. Library developers and users should bear in mind that using a const function implies “if the library version to build against has any bug, it could be leaked into the program”.

The key point doesn’t change: if there’s any security problem in a library, we should upgrade it as soon as possible. In the past we need to upgrade the minimum supported version, and now we should also rebuild with the new implementation to ensure we get the security fix for build-time values. In most cases users are already doing the latter as a consequence of the former.

This depends on the implementation of URL. It could either crash or throw or use some other way to handle the value. A forbidden value in the initializer doesn’t necessary mean it’s known to be incompatible with the implementation. Runtime errors or preconditions are still necessary for robustness, and library developers can fully control the behavior. A crash is the easiest implementable way.

Of course, what developers should do is to use newer SDKs as possible, and to update their applications to avoid invalid hard-coded values. This should be the ultimate and correct solution.

2 Likes

That seems to mean that the proposal's showcase itself makes no meaningful use of the feature. That's a strong sign that this proposal is not useful in isolation.

10 Likes

Totally agree👀 I guess it makes some sense to be a separate proposal, but reviewing in parallel with build-time evaluation or construction proposal would be far better for showcasing.

1 Like

It is found in the section titled “Memory Placement and Runtime Initialization”:

Effect on runtime placement of @const values is an implementation detail that this proposal does not cover beyond indicating that today this attribute has no effect on memory layout of such values at runtime. It is however a highly desirable future direction for the implementation of this feature to allow the use of read-only memory for @const values.

I'm still uncomfortable with the re-use of "const" with a different meaning than in C++, where it means immutable at runtime and is somewhat more analogous to let vs var. I know we're not necessarily trying to imitate other languages, but I think being starkly inconsistent with them isn't good either.

5 Likes

I have an aesthetic concern.

As others have pointed out, this proposal explicitly does not propose any mitigation for the virality of @const that is evidenced in other languages, particularly C++. As such, we can reasonably expect @const to start appearing very frequently. I believe this tips the scales to eliminating the @ prefix whenever possible.

The @objc analogy is a false equivalence, because most uses of @objc are inferred, and this proposal avoids the topic of const inference.

To make another analogy, Swift concurrency has both : Sendable and @Sendable. These could have been collapsed into a single @Sendable attribute, but I suspect the language designers would have found it aesthetically unpleasing to have @Sendable struct Foo { }.

1 Like

So this proposal leaves that as a future direction as well. Thus, this proposal does nothing concrete but turn some declarations into compile-time errors.

I would be more accepting if the feature was hidden behind a feature flag until it has some real-world benefit. That will mitigate against people sprinkling @const everywhere thinking it’s some kind of optimization.

4 Likes

It considers placement of constant data within the binary to be out of scope for the evolution process, but a logical and “highly desirable” functionality that is blocked by this syntax.

I don’t understand why the compiler can’t do this for any constant today, and how @const will change this.

3 Likes

Currently the language semantics require that an expression of the form Foo(bar: 1, baz: 2) invoke Foo.init(bar:, baz:) at runtime. I suppose it’s worth asking the @core-team if they would consider changing this behavior without a sentinel word when the compiler is able to constant-fold the entirety of Foo.init(bar:, baz:)’s implementation.

But rarely is an initializer so simple. Something as trivial as a precondition() could force the initializer call to be emitted. By marking Foo.init(bar:, baz:)’s arguments and return value as @const, the compiler can then evaluate the branch at compile time, even across a potential ABI boundary.

As you mention, this isn’t really a new problem. If an application writes let url = URL("http://www.apple.com"), they are likely to force-unwrap that value, and might even opt into -Ounchecked to elide the trap if it’s in a performance-critical path.

If the Foundation authors want to invalidate previously valid values, they will need to add runtime validation, most likely to all the URL struct’s getters.

Building on your thinking, though, perhaps @const needs to imply @frozen?

The proposal lists that ability as a future direction. I find that I have to repeat myself each time because you are not addressing my initial criticism.

2 Likes

Do you like the following design🤔

struct Pair {
    let key: const String
    let value: String

    /// `@const` implies if all parameters are `const` then the output is `const`.
    /// The compiler will always prefer the overload with more `const`s.
    @const
    private init(_ key: const String, _ value: String) {
        self.key = key
        self.value = value
    }

    /// The following declaration is a syntatic sugar of:
    ///     static func make(_ key: const String, _ value: String) -> Pair
    ///     static func make(_ key: const String, _ value: const String) -> const Pair
    @const
    static func make(_ key: const String, _ value: String) -> Pair {
        self.init(key: key, value: value)
    }
}

// In Swift 5, we're not going to infer `const` types by default, so
// even if a `const Pair` is returned by `Pair.make`, we'll implicitly
// convert it to `Pair`.
let pair = Pair.make("key", "value")
print(type(of: pair)) // const Pair (Swift 6) / Pair (Swift 5)

// If you need a `const Pair` in Swift 5, you can specify it manually.
let staticPair: const Pair = Pair.make("key", "value")
print(type(of: staticPair)) // const Pair

// `const` is propogated into all its properties.
print(type(of: staticPair.value)) // const String

// If a runtime value is passed, we can only construct a runtime value.
let dynamicPair = Pair.make("key", runtimeValueGenerator())
print(type(of: dynamicPair)) // Pair

// The following code cannot compile because `key` is explicitly `const String`.
let invalidPair = Pair.make(runtimeKeyGenerator(), runtimeValueGenerator())

So if that paragraph were moved into a new section entitled “Things We Plan To Do With This Functionality That Are Not Subject To Comments From The Swift Evolution Forums”, would you object?

Yes but with @const, they could theoretically write: URL(foo() + bar() + baz()), and if those functions all return @const Strings, potentially the result could be a @const String. Technically the compiler knows the result, even if the programmer can't easily see it.

That's when the lack of real compile-time evaluation shows - just because the compiler knows the value, we've decided this API doesn't need to return an optional any more. That leads to an API which is actually worse than the one we already have, which makes the failure explicit and forces you to handle it in some way (gracefully, if possible).

That's what I mean when I say I don't think this is a valuable capability to add over what exists with StaticString. Yes, developers want compile-time validation of string literals and we should try to deliver that, but I don't think this is what they want.

4 Likes

That isn't true at all. If the body of Foo.init(bar:baz:) is visible, and can be constant folded given constant arguments, the compiler is free to emit the constant result without performing a call at runtime.

7 Likes

Emitting a constant object is one possible implementation mechanism for initialization. The semantics of initialization are given by the definition of an initializer. If a library has chosen to make an initializer an ABI boundary – which is to say, it has chosen to hide the semantics of that initializer from clients outside the library — then the compiler cannot know the correct semantics of initialization and cannot emit a constant object. If a library exposes those semantics by making an initializer not an ABI boundary — which of course requires the type to be frozen — then if the compiler can fold the implementation of those semantics, it is of course allowed to.

5 Likes

I'm not sure that compile-time evaluation of functions won't be subject to SE discussion, but if that were actually planned as part of the same compiler release, I would object less, but still have reservations.