[Pitch #3] Swift Compile-Time Values

Hello, Swift Community!

This is a new version of the pitch for bringing the concepts of compile-time values and compile-time computation to Swift previously discussed in: Pitch #1
and Pitch #2.

This pitch is currently also referenced in another pitch for a feature that does not exist in the language yet: Section Placement Control (Pitch, Discussion Thread), and it's worth considering it together with this one for further context on what kinds of applications this can have in the language.

Any feedback is welcome, interested in hearing your thoughts!

Current version of the pitch is on GitHub:

19 Likes

I think that compile-time evaluation is really important and the proposal has many features I think are essential. I have a few questions:

The Global variable or Property @const attribute section looks like it's globally undecided as to whether @const is OK on instance let variables or not. The intro says "a stored property can have @const", and then the example says @const static let, then the next paragraph says instance @const variables don't participate in the layout of the type, then the last paragraph says it's an error to use @const let without static. This should be clarified.

What is the overlap between parameter @const attributes and macros? I would imagine that macros is how people would usually implement this kind of static verification.

Actually, speaking of macros, can macros get the resolved constant value of a @const expression?

Can a @const value be used as an integer generic argument (like Vector<Int, @const multiply(4, 4)>? I assume that yes, but it means @const values and @const functions have ABI consequences, and I don't see this addressed. (I understand that generic parameters cannot be used in @const expressions because they may not be compile-time constants, and that's probably useful to note somewhere as well.)

One of the motivating examples is compile-time checking, for instance with a @Clamping property wrapper. What is the mechanism by which the compiler reports an error? Is assert/precondition recognized in @const functions to emit a diagnostic if it failed, like static_assert?

In the case of URL, given that it is not @frozen, how would constant evaluation/verification work in practice? I guess you could have a StaticURL type that's ExpressibleByStringLiteral and take that as an URL initializer argument seamlessly?

What are the rules for inferring that a non-@const function can be used in constant evaluation?

5 Likes
struct DatabaseParams {
  @const static let encoding: String = "utf-8"
  @const static let capacity: Int = 256
}

Why is @const useful on properties / local vars?

If it's to pass to a function requiring const, then the compiler should be able to infer this, same with satisfying a protocol const requirement.

1 Like

I'm still taking in many of the details, but the syntax for functions struck me as odd:

public func celsiusToFahrenheit(_ degrees: Int) @const -> Int {
  return degrees * (9 / 5) + 32
}

It looks like @const is being used as an effect here (Ă  la throws and async), but attributes modify the subsequent declaration or type and in this case it appears to be modifying the function arrow. I would expect one of the following instead:

// as an effect
public func celsiusToFahrenheit(_ degrees: Int) const -> Int

// as a decl attribute
@const public func celsiusToFahrenheit(_ degrees: Int) -> Int

The second one seems like it would be consistent with how vars/properties are declared, so I'm curious why it wasn't chosen.

Speaking of throws, can a compile-time constant function throw? (I'm assuming async is out of the question for obvious reasons.) The most natural answer at first would seem to be "no", but if a @const function could throw a frozen Error subtype that has a @const-initializable description, those could be turned into compiler error diagnostics, which has a certain elegance—a version of C++'s static_assert that feels more like writing regular Swift instead of introducing new special constructs.

13 Likes

§ Proposed Solution:

The attribute participates in name mangling.

§ Effect on ABI stability and API resilience:

The new @const attribute does not affect name mangling.

:confused:


§ Basic Supported Types:

Integer and Floating-Point types (Int, Float, Double, Half)

  • Does this exclude the {U}Int{8,16,32,64,128} types?
  • Should Half be amended to Float16?

InlineArrays initialized with literal values consisting of @const elements of above types.

  • The InlineArray type hasn't been pitched yet.
  • Why not support the Vector (aka Slab) type?
6 Likes

Will this be possible?

protocol SomeProtocol
{
    @const static let constant = 1
}

func useProtocol(x : SomeProtocol) -> Int
{
    return x.constant
}

If so, I don't know the underlying details but i assume it would access the relevant getter from the class? Or the class really would need to store the constant somewhere, which might impact this part of the proposal:

@const properties do not participate in the type's memory layout

Will const functions participate in overload resolution?

Eg:

func getValue() -> Int
{
    return 1
}

func getValue() @const -> Int
{
    return 2
}

let a = getValue()
@const let b = getValue()

Is this possible, and if so is the line for a ambiguous as either function is an option?

1 Like

This looks really cool, especially the SPM example and the synergy with the section placement control pitch! I have two questions:

Firstly, are there any concerns about security? In the context of the SPM manifest, for example, I assume that part of the motivation is to limit packages from running arbitrary Swift code on the user's machine. And while moving the attack surface into the compiler seems to limit what the author of a malicious package can do, it could also create a new attack vector for all Swift programs. I'm guessing that the scope for causing damage is limited, but I am curious if there's been any consideration for this. I guess C++ has this in constexpr and is fine?

Second, are there any concerns about the effect on build times for overly aggressive, or extremely prolific, uses of this feature? For example, let's say that I decide to go further than your URL example and make a URL.init(preValidating string: @const String) function, which does some checking to make sure that the URL is valid. The checks could be quite expensive. I'm also curious about how this is implemented; it seems like C++ constexpr uses some sort of special interpreter, and if that's out of process, this could add up in a similar way to how each macro invocation impacts build times.

2 Likes

I think this would be pretty neat to have! Some thoughts reading through the pitch:

Propagation

The propagation rules seem backwards to me. I realize after re-reading the earlier pitches that they were initially the other way around, but I'd like to re-propose that the compile-time guarantees flow forwards rather than backwards, giving the role of @const to be a constraint where it is needed instead:

@const let a = 42
let b = a // implicitly @const

This can be further simplified if we enable any literal value (or collection of literal values?) to be implicitly known at compile time:

let a = 42 // implicitly @const
let b = a // implicitly @const 
@const let c = b // allowed because b is implicitly @const

Seen another way, this makes future compile-time computations implicitly supported, and marking something with the constraint either reserves the possibility for this in the future, or allows propagation into specialized function scopes.

Collections

For myself, the biggest benefit to annotate my code with something like @const would be for data pages and lookup tables that could be baked in at compile time. I know there is a section in future directions for this, but it honestly feels like we need a pre-pitch of that to go along and inform this one to make sure all the details are hashed out.

Expressions

How will something like this compile? Will it warn that the conditional is superfluous? Can we make it explicit?

@const let condition = true
...
@const let a = 1
@const let b = 2
@const let c = condition ? a : b

Name

I'd much rather a more explicit name such as compiletime that indicates to anyone coming across this for the first time what it does. const is already used in many languages to mean what let means in swift, and doesn't carry with it the same weight as a compile-time constant constraint. Especially for those that don't follow the evolution process, I imagine the first time they encounter const in optimized code won't actually be met with clarity, but with confusion that something like completive can avoid completely.

Placement

I'm not a fan of this being a (yet another) attribute. To me, the following would be much easier to read and remember:

compiletime let a = 42
func clamp(_ value: Int, min: compiletime Int, max: compiletime Int) {}
3 Likes

There is one! [Pitch 3] Section Placement Control is what you're looking for.

1 Like

On top of specifying a custom section name with the @section attribute, marking a variable as @used is needed [...]. When using section placement [...], such values are typically going to have no usage at compile time [...], therefore we the need the @used attribute.

Does the pitched design present the right default, then, by separating a very common use case into two separable attributes?

If, as I understand it, "typically" one will want to mark anything with both @section and @used, shouldn't @section imply @used by default and dead-strippability could be opt-in via another parameter (i.e., @section("...", strippable: true)?

Attribute @constInitialized

Static initialization of a global can be useful on its own, without placing data into a custom section

...such as for ____? While I don't argue against the point, given that @section implies static initializability, a dedicated spelling for just this doesn't seem to be necessary for the overarching feature (section placement control).

Since the @const pitch isn't yet to a point where the distinction between @const and @constInitialized can be exercised, and since there aren't any motivating use cases named in this pitch either, it seems to me better to defer this explicit feature to be holistically designed alongside that potential future direction for @const rather than right now.

Not that I have a strong opinion on whether @constInitialized is the best way to get that, but there is definitely value in ensuring that a global variable doesn't need runtime initialization. If you have a large global variable (like a large array) that the compiler must produce a runtime initializer for, the code to initialize that global will be bigger than the global itself, often 1.5x or 2x bigger. If binary size matters for you (for instance, because you have a firmware target), this is a problem, and an attribute that ensures it either has no runtime initializer or diagnoses is one way to solve it.

5 Likes

Whoops, looks like my feedback went into the wrong tab--will repost over on the other thread.

I've clarified in the current draft that we are currently discussing static properties only. Though it is open to discussion whether we should also allow instance properties be @const for the sake of allowing the client to use these properties as instance properties, even though they are implemented as not participating in storage layout and technically must have the same value across all instances. See the "Non-static @const properties" sub-section of the Future Directions and Roadmap section.

Because of fundamental layering constraints of the compilation stage where we can feasibly perform compile-time interpretation for Swift, the current proposal's semantics are such that compile-time values do not participate in the type system and cannot currently be used as inputs to Macros such that Macros get the resolved value (See the Participation in the Type System section which mentions Macro use-cases). That said, I do see this as potentially a very intriguing and important use-case that we may want to design for in the future, but it did not fall into the scope of where we see this proposal today.

As per the Integer generic parameters implicitly @const section, yes, we would like this to be possible. That said, because compile-time values do not participate in the type system (in this proposal), we can only really do this because/if the integer generic parameters are treated as opaque values for the purposes of type-checking. Which means, I believe, that we can avoid this having any ABI impact (@Joe_Groff to confirm).

This is a great question. As a future direction, it absolutely makes sense to teach the compile-time evaluation subsystem of the compiler to transform regular assert/fatalError etc. calls into compile-time failure diagnostics. I've added a note to that effect to the future directions roadmap. In the meantime, this proposal would probably benefit from including a basic static_assert functionality.

For now it is simply that the return expression contains only operations on/references to values which fall under the rules specified under @const Propagation rules and supported operations section.

2 Likes

For properties, the purpose is to allow opting into a more-explicit contract for guaranteeing certain values to be @const-usable, including across module boundaries. And to allow other potential use-cases which may rely on an API surface which has build-time knowable/extractable arguments, which is where protocol property requirement part of this proposal comes in.
Otherwise, as you correctly point out, the compiler can/should infer references to properties and variables which are usable in compile-time context without the need for explicit annotation by the user.

While you're correct about the pitch's answer currently, I think this would be a very natural future direction. I will write this up in the roadmap section of the pitch.

@cooperp, in the current draft of the pitch we do not specify whether a default @const value can be specified in the protocol definition, which we should clarify. The two obvious options I see is:

  1. If a default is provided in the protocol definition, then conforming types cannot provide their own.
  2. @const Protocol property requirement does not allow a default value in the protocol definition.

Both of which would preserve the non-participation in the type's memory layout semantic, with the protocol's witness table including an entry for the property's getter either way.

In the current draft, @const does not contribute to mangling and @const functions do not participate in overload resolution. i.e. for the sake of code-generation,@const on a function does not carry any meaning, which means your example should not compile.

1 Like

To me this syntax:

var x: Double = @const radToDeg(Double.pi)

Suggest we could also do things like:

MyType().rotate(by: @const radToDeg(Double.pi))

Here it would force the radToDeg result to be resolved at compile time?

If that is the case how would this work for lines that have multiple statements.

var alpha: Double = @const  radToDeg(Double.pi) * someRuntimeValue

You have noted this would fail however I feel if we add braces it shoudl succeeded.

var alpha: Double = (@const  radToDeg(Double.pi)) * someRuntimeValue

I am not sure I like the use of @const in the trailing position on a function.

func radToDeg(_ radians: Double) @const -> Double {
    return radians * 180 / Double.pi
}

Since swift currently places throws and async and rethrows in this location maybe just const without the @ would be cleaner. Since the behaves in a smiler way to rethrows.

func radToDeg(_ radians: Double) const -> Double {
    return radians * 180 / Double.pi
}

Or just put the @const before the method and allow constant methods to also be called at runtime.

@const func radToDeg(_ radians: Double) -> Double {
    return radians * 180 / Double.pi
}

Since the compile time value is always constant, this sounds that this type of value does not need real symbol visibility to other compiler unit, nor it has the linkage issue ?

Just like the compiler pass ConstFolding for some const basic type for Int, etc

Since the compile time value is always constant, this sounds that this type of value does not need real symbol visibility to other compiler unit, nor it has the linkage issue ?

Although it may be technically feasible to always propagate the representation of @const value to its uses, I don't think it's a foregone conclusion that that is always the optimal way for generated code to interact with a constant value. Constant values may be aggregates that have a large representation in memory, and referencing the value through a shared memory location may make more sense than materializing the value everywhere it is used.

2 Likes

One thing I'd like to see discussed in more detail in the pitch are the consequences of @const values being exposed in the API interface. If clients of resilient module are allowed to exploit their knowledge of the value of some @constdeclaration from a dependency, then I think that implies that changing the values after they have been exposed to dependents should be considered to be ABI breaking in the general case. That also has a more subtle implication that @const functions cannot evolve the outputs they produce for given inputs without risking breaking ABI. It may be safe to refactor an implementation so long as it does not produce different values, though.

5 Likes

It looks like there's still some language referring to @const "stored" properties:

  • the reference to @const properties not impacting the layout of the type is unnecessary, since they're all static
  • the protocol requirement section refers to instance properties throughout
1 Like