SE-0359: Build-Time Constant Values

Regarding the "Protocol @const property" aspect of the proposal, I'm struggling to see the motivation.

If the intent is to enforce that the property's value is "static" (eg, make sure the database keys or printf-string are not runtime-able values), then that should be expressed in the type, as you'd want to propagate this quality through function calls, even if you select among values at runtime (flag ? key1 : key2). Expressing frozen/static typing is interesting and useful independent of this proposal.

If the intent is to enforce that the value doesn't change for an instance/class (eg, the property will be used as a key in a dictionary or an input to a hash that should be stable), then that is an interesting direction! But I would propose that we should implement that independent of any "build-time-ness" requirement as it's more generally useful.

For example, the proposal offers an example:

protocol NeedsConstGreeting {
    @const static let greeting: String
}

It's not clear what @const actually adds here. Wouldn't just plain static let greeting: String express the interesting part? You could even use StaticString to cover the other axis too.

I suggest the authors expand the proposal with a more concrete example that actually relies on the compiler doing something interesting at build time for the protocol property use-case.

8 Likes

I assume there's something which will be announced in two weeks which relies on this feature? If so, the timing of this review is really awkward. If not, then I really struggle to see the point. The examples of uses for it are extremely underwhelming and very much look like a solution in search of a problem. "The compiler might find a way to optimize based on this" is the sort of thing that makes me think that we'll have a @const2 in the future with slightly different semantics that the compiler actually can use for optimizations.

10 Likes

The rules around this feature seem to resemble a hypothetical ‘pure’ function annotation, which would denote functions free of side-effects.

I'd like to see the proposal authors consider the interplay with pure functions as a future direction. It seems there's an opportunity for calls to pure functions with only const parameters, to be a valid const expression itself, if we do this right.

9 Likes

For me the logical use cases of build time constant values is relevant when using perfomance sensitive data structs, for example a Vector or Matrix having a compile time known size lets the compiler un-role loops for numerical operations on these data types, the compile could even auto vectorise expressions using such data types leading to massive perf improvements.

From the "Supported Types" section:

[...] The current scope of the proposal includes:

[...]
• Array and Dictionary literals consisting of literal values of above types.
•Tuple literals consisting of the above list items.

This list excludes tuple literals inside array/dictionary literals, and nested collections. Is that simply a miswording, an oversight, or an intentional limitation? If the latter, could it be explained?

Additionally

Enum cases with no associated values

(Emphasis added.) Why is const nesting not permitted where the associated value types are on the Supported Types list?

3 Likes

As a newcomer to Swift, but with two decades of programming in other languages I was a bit blown away by the abundance of keywords, especially those that are in my opinion more stand in the way than help. “Guard” version of “if”is one example. I think that the proposed @const or its variants will have the same impact on learning and consequently popularity of Swift.
More keywords - less elegant language.

Sounds like you have lots of experience. What specifically do you suggest instead?

I only saw mentioning that this feature could be potentially useful. If the purpose is to generate a more efficient code then we should not ask the user to help with the optimization. Instead we could add a compiler directive/function that would determine if the constant is initialized at the compile time and the initialized value then we can achieve the same (even better) result without complicating the language.
Something like:
func Clamp(val, min, max) -> …
{
if CompileTimeDefined(min){
let Min = GetComplieTimeValue(min)

}
}

So only a small number of expert users would have to learn about CompileTimeDefined/GetComplieTimeValue while the rest of us mortals would not care.

Slight miscommunication @Panajev . In this example, i is the imaginary unit. Presumably all the constants from the Numerics and Complex packages would be marked as @const if this proposal is accepted

1 Like

Thanks for the clarification, I was thinking it would have to be as the rules around built time evaluation are necessarily infectious :).

WWDC didn't reveal anything which obviously depends on this, so this proposal still seems like a solution in search of a problem. Since my view on this is that I think the motivating use-cases are extremely uncompelling, I thought it might be worth writing up why they don't excite me.

Enforcement of Compile-Time Attribute Parameters

I don't understand why requiring the upper and lower bound of a clamp to be a compile-time value would be a desireable thing. It certainly isn't addressing any problem I've ever had. If the compiler was able to optimize away the runtime storage of the bounds then that'd be exciting, but implementing that isn't actually part of the proposal.

Similarly, accidentally inconsistent runtime values for a serialization key just isn't a bug I've ever seen happen, and so a feature to prevent that is not very useful.

Enforcement of Non-Failable Initializers

Compile-time validation of string arguments would be wonderful, but that isn't part of this proposal. Without that, this is just removing a single character from the call site. I'm not convinced that removing that character is even a good thing to begin with.

Facilitate Compile-time Extraction of Values

The specific example of using this for Package.swift sounds like a hilariously overcomplicated way to turn a turing-complete config file into a declarative config file. I can see some advantages to it compared to just using a normal config file format (e.g. Xcode doesn't have to implement autocomplete and syntax highlighting for that format), but it also preserves the giant downside of Swift being very slow to compile. Skipping codegen and executing the package doesn't speed up the part of package resolution that causes performance problems.

The broader idea of having some sort of API to inspect the AST at build time to generate code or something is exciting, but @const seems like a very small piece of a very large project that wouldn't need @const to be useful.

Guaranteed Optimization Hints

The obvious question here is if the hints are actually useful, and it's not obvious to me how they would be.

Is the assumption that @const will result in more inlining? Or that the compiler will specialize the function for each value of the @const parameter? The latter is something that we've done in C++ plenty of times (shifting the parameter from a runtime argument to a template argument), but I'd have a lot of questions about how exactly that'd work and I'm not sure that something like @const is the right way to do it. A more explicit @specialize(where: value == 1 || value == 4 || value == 8) func foo(_ value: Int) might be better for that sort of thing.

6 Likes

Given that implicit conversion from @const T to T is very natural, I really think we can consider adopting @const into the type system directly. I agree that this adds some complexity, but it will enable far more power.

Consider the example given in the proposal:

typealias CI = @const Int
let x: CI?

What is the type of x? It appears to be Optional<@const Int>, which is not a meaningful or useful type, and the programmer most likely intended to have a @const Optional.

How to determine the type of x here?

We can force generics types to add explicit declaration (eg. by attribute) if they accept @const type arguments. In the Optional case, it should choose not to do so because that makes no sense.

Then the case above will emit a compiler warning saying that Optional doesn’t allow Wrapped to be explicitly @const, so the type of x is still Optional<Int>.

But wait? How to get an @const Optional?

Generic types can provide a build-time evaluated initializer, that takes an explicitly @const value, and return an explicitly @const T. Build-time evaluable overrides will always be preferred.

That is, types of the following declarations will be resolved as:

let a = 1              // a: @const Int (Swift 6)
let b: Int = 1         // b: Int
let c = Optional(x)    // c: @const Optional<Int>
let d: Int? = a        // d: Optional<Int>
let e: @const Int? = x // e: @const Optional<Int>

let x: @const _ = 1    // The exact alternative to `@const let x = 1`

Will it introduce source breakage?

Explicit type annotations are still taken as-is, and @const needs to be explicitly marked. It has the least combination priority so @const _ is valid.

Since @const values can fit everywhere non-@consts are accepted, there shouldn’t be any source breakage. Not implicitly inferring @const in Swift 5 is simply for preserving behavior of existing codes.

Implicit @const will largely push the adoption of build-time evaluation as we can have @const types in much more places without explicit annotation.

Proposed @const conversion rules

  • For every type T, @const T implicitly converts to T.
  • For value type T with stored property prop: U, prop on explicit @const T has type @const U.
  • For reference type T with let property prop: U, prop on explicit @const T has type @const U.
  • For every type T with computed property prop: U that has a build-time evaluable getter, prop on explicit @const T has type @const U, otherwise it has type U.

One more thing…

If @const is emitted into the type interface, we should consider if it should be named const as a modifier instead of an attribute.

3 Likes

I just read the proposal while writing my monthly summary issue and the Facilitate Compile-time Extraction of Values section got me thinking in its last sentence:

More-generally, providing the possibility of declarative APIs that can express build-time-knowable abstractions can (...) allow for further novel use-cases of Swift’s DSL capabilities (e.g. build-time extractable database schema, etc.).

Do I understand it right that this should also allow the community to write a new DSL for Config Files based on Result Builders and the @const attribute to replace e.g. YAML files with Swift files?

1 Like

Would it be allowed to define an enum of @const values?

@const let thirdCase: String = "case3"

enum Test: String {
  case first = "case1"
  case first = "case2"
  case third = thirdCase
}
4 Likes

I would be interested in this feature. It feels weird to require JSON or YAML as a configuration file for a Swift script.

1 Like

As WWDC has come and gone there still doesn't seem to be any sign of an actual use for this that isn't relegated to a future direction. As such I am strongly -1.

A pitch that included any of the future directions, or was at least proposed concurrently with one, would likely be different story. Having said that, I am not willing to support the use of compile time values for things like URL literals unless that also comes with compile time validation.

3 Likes