[Pitch] Compile-Time Constant Values

I made a reply a few dozen posts above about how this might help with C++ interop, but it seems to have been ignored. To those actively involved in this conversation, is my idea relevant?

2 Likes

How about dropping the text part altogether, #String for a type, #(...) for expressions?

1 Like

I don’t think #(/**/) is awfully clear. I think it’s best to go with the precedent that #file and such set of having a keyword. And using the full evaluated prefix for a types fits with the swift style more (similar to inout). evaluated is also a lot harder to miss and makes the code more readable. Prefixing types with # would be non-intuitive syntactic sugar, which swift tends to use very sparingly.

1 Like

This is awesome!

I think a powerful compile time expression would be the ability to have expressions in where clauses.

eg

protocol Matrix {
     static const let n: UInt 
     static const let m: UInt 
}

extension Matrix where Self.n == Self.m {
     // Methods for square matrices 
}

A further extension of this would be the ability to specify such values in the type signature so that the compiler could synthesise these types on demand. (maybe for this there would need to be some form of meta type equivalency test so the compile could ensure it does not create multiple types for the same pairs of n and m)

static func * <A, B, C>(left: A, right: B) -> C where A: Matrix, B: Matrix, A.n == B.m, A.m == B.n, C: Matrix, C.m == C.n, C.m == A.n  {
   var result = C<m=A.n, n=B.m>(fill: 0)
   //  do cross product
  return result
}
1 Like

That would break the expectation that extensions add functionality to specific types because the const properties aren’t part of the type. I think that would need to be a pitch of its own. If you’re interested in pursuing that concept further though, you can take a look at Rust’s const generics for some inspiration.

It is certainly an interesting concept.

I sort of see static properties on a type being the same as instance property's of the MetaType. Im aware swift does not really expose the MetaType in the same way other lanagues but I feel that the content values (and thus expressions) means swift you start to expose these. When I say MetaType im thinking about how python has meta types.

This is a short summary of what I had in mind if you think this could go somewhere I'm happy to open a seperate discussion on the forum here to see if this idea resonates with people.

Maybe swift could gain (not part of this proposal) the explicitly ability to declare meta types rathe than them currently being implicitly generated by the compiler.

eg

struct Matrix {
    const metatype<V: AdditiveArithmetic & Numeric> {
        let V: Type.self
        let n: UInt
        let m: Uint

        init(_ v: V.type, n: UInt, m: UInt) {
            self.v = v
            self.n = n
            self.m = m
        }
    }
}

with the init method of the meta type becoming the call signature of the generic specialisation.

Matrix<Float, n: 3, m: 2>

So that the compiler does not generate multiple meta types for the same signature the meta type would need to conform to equitable (or maybe washable) so the compiler could ensure it only every creates one meta type for any given combination of values. In this example the init method would be evaluated during compile time so would be an implicitly constant expression.

Doesn’t really make sense to me to be able to explicitly define it like that, I think it should probably be more like:

struct Matrix<n: Int, m: Int> /**/

The idea is definitely distinct enough for another discussion so if you do want to discuss it further you can create a new discussion. I’m definitely interested by the idea. Maybe call the discussion ‘value based generic type constraints’ or something.

I think compexpr or constexpr could be nice if used like:

let name = constexpr "stackotter"

I've been thinking about the importance of strong defaults in a language. imo, in a programming language you want the "best" option to require the least thought to use. While syntactically I like your proposal, having to remember to do let name = constexpr "stackotter" every time I make a string (well, constexpr would unnecessary for a string literal) means it may not often be used. If on the hand the syntax were

const name = "stackotter"

it makes it pretty simple for me to change my "default" variable declaration from let to const

3 Likes

Agree 100%. Imagine if we have to write

var name = immutable “name”

Instead of

let name = “name”

Having both let and const in an assignment doesn’t convey any new information, it just adds verbosity imo.

Additionally, I feel like extra annotations like this should add extra functionally (e.g. inout, @escaping [not exactly the same, but same idea]), not take it away, which const sort of does.

It would also be nice if there was a compiler warning to change let to const if possible, like from var to let, which works well with the idea of const being the default.

4 Likes

It's too bad because from a syntax and grammar perspective I like let name = constexpr "name" better. But from a DX perspective I like const name = "name" better

1 Like

These two things don't have to be mutually exclusive. const name = constexpr "name" is syntactically ridiculous but conceptually fine; const name makes name usable in subsequent constexpr evaluations, and constexpr ensures that the expression is evaluated by the front-end. Both concepts are useful. We can just have constexpr be implicit for the initializer of const name, such that const name = "name" is actually const name = constexpr "name".

2 Likes

such that const name = "name" is actually const name = constexpr "name" .

That's a great idea. In that case, we can get rid of const name as a special initializer and just have const name = "name" be syntactic sugar for let name = constexpr "name". This makes forms like

func nameAndAge(name: constexpr String, age: constexpr Int) -> constexpr String

simpler.

Otherwise we'd have to ask in :point_up_2: if name is a let or const. (I don't know if this is even a consideration in the compiler anymore since variable parameters like func nameAndAge(var name: String) was removed in Swift 3)

About compile time expressions. Maybe it's not related, but ... It would be nice if bitwise operation was (pre)compile time operation (if possible).

For example, there is a warning if you initialize OptionSet with rawValue as 0. But there is no warning if bitwise operation's result is zero.

struct ShippingOptions: OptionSet {
    let rawValue: Int8
    
    static let nextDay    = ShippingOptions(rawValue: 0) // warning
    static let secondDay  = ShippingOptions(rawValue: 1 << 1)
    static let priority   = ShippingOptions(rawValue: 1 << 2)
    static let standard   = ShippingOptions(rawValue: 1 << 333) // actual 0, no warning
}
1 Like

Returning to the current pitch, one thing I can't really make sense of is how it grows into the feature it wants to be. Constant evaluation of functions and instantiation of objects are on the roadmap, but it seems like they would deprecate large parts of the current first step. Once we have constant evaluation of functions, it stops making sense to have const-annotated parameters (the function allowing constant evaluation transitively requires its arguments to be const-evaluated). Once we have constant-evaluated instantiation of objects, const on instance properties also stops being very useful because evaluating the initializer at compile-time implies all fields have a value computed at compile time. (What's worse, if you want compile-time evaluation of mutating methods, you can't use const on any fields that you want to have mutable!)

This leaves us with static const properties and requirements, which I think will continue to have value going forward.

2 Likes

Arguably it's better if 'const' is orthogonal to 'let' because we might want it to apply to other kinds of declarations, such as 'func'. Also given that this is an advanced, rarely-used feature, making the declaration site concise shouldn't really need to be a goal.

4 Likes

I have a very superficial suggestion: we should consider spelling this as @const rather than const. My arguments are as follows:

  • this behaves more like an attribute than a keyword since it doesn't fundamentally change the behavior of the declaration, rather it restricts it in some way, similar to @objc (there is also an analogy with property wrappers here).

  • this is an advanced, rarely-used feature so burning part of the lexical grammar on a new contextual keyword might be undesirable.

  • the @ jumps out at the reader, immediately making it clear something special is going on.

Unfortunately I don't have any more substantiative feedback on the pitch at this time, but I'd really like us to consider @const rather than const. For some reason const rubs me the wrong way.

26 Likes

Is it, though? We'd likely want either the same or a very similar spelling for functions (not just values) that must be evaluable at compile-time. That then extends to every function called by those functions, and suddenly we're in C++ "constexpr all the things!" land.

For those unfamiliar with C++, this means that every function that possibly can be constexpr should be constexpr, otherwise you won't be call it from compile-time contexts. So it spreads everywhere; even library authors who aren't interested in compile-time meta programming need to tag their functions as constexpr.

3 Likes

Like @usableFromInline? :wink:

Actually it isn't like @usableFromInline:

If your function is @inlinable, everything it calls must be @inlinable (recursively), so that is like constexpr -- but @usableFromInline serves as a cap which breaks that recursive rule and allows you to include non-inlinable functions and declarations.

That kind of approach works for inlining, because theoretically inlining shouldn't affect semantics, only performance/code-size. So it's okay to have some leaf functions opt-out of inlining. Compile-time evaluation isn't like that; if a leaf function opted-out and was evaluated at run-time instead, it could very easily affect program semantics and mean the top-level @const/constexpr assertion would no longer be accurate.

6 Likes

I think @Slava_Pestov and us have fundamentally different ideas of what this features' place is in the landscape of Swift.

Because of this, I think we need to explicitly ask:

Do we expect compile-time constant values to be a core part of Swift that should be used frequently, or a hidden advanced feature to be used only in certain situations?

When I made my proposal I was assuming it was the former. That when I'm programming I would essentially use const name = "name" (which is syntactic sugar for let name = constexpr "name") as a default, and use let when that doesn't work. Similar to how I work in Rust.

I think it all comes down to this question. If we decide this is an advanced feature to only be used sparingly, then I :100:% agree with @Slava_Pestov and think the @const syntax makes more sense

3 Likes