[Pitch] Compile-Time Constant Values

Despite the poor phrasing, I am sure the intent is that if a function is invoked in a compile-time setting, it must be evaluated in that setting. It is unreasonable to suggest the compiler invoke functions that are compile-time runnable merely because they exist.

Similarly, functions which can run at build-time could still be called with runtime-only parameters.

2 Likes

Let me put this another way: anything evaluatable at compile-time MUST be referentially transparent. That is, replacing it with its result causes no change in behavior, and vice versa. I don’t think there is any disagreement about that.

My definition of pure, which I believe is the most common one, is effectively synonymous with referential transparency.

If you want something to happen at compile-time that doesn’t meet that criteria, what you really want is a compiler macro or directive.

1 Like

If it was "checked", it had already been executed. So, the compilation speed will remain the same.
The only reason not to store the result would be the size of the binary, but that specific case should be covered by the compiler settings.
I think the behavior of the compiler should be defined by the developer.

You’re completely correct, and I have no clue how to deal with that.

If compiler settings control whether something is evaluated at compile-time instead of runtime, compile-time evaluation isn’t guaranteed.

I agree. I can also see it becoming quite tricky in terms of security to manage compile time evaluation of functions that aren't pure. By making sure that the functions are pure the compiler can guarantee they aren't doing anything dodgy.

As mentioned earlier, this thread is not about the broader feature of evaluating arbitrary expressions at compile time. Could discussion of that wider topic please be taken to another thread on evolution/discussion? Thanks!

2 Likes

This is a good first step in the direction of general compile-time evaluation. It is also a necessary step to ensure the safety of the new SwiftPM DSL. I think we may be able to borrow the syntax from the more general case, without designing it.

The use of # prefix is a well established precedent in Swift to indicate things that are determined at compile time. I think instead of a normal keyword, we should use a #-prefixed keyword.

The way I see it, we need two:

Parameter:

func foo(input: #evaluated Int) {...}

Static constant:

struct Bar {
    static let title = #evaluate( "Bar" )
    // OR 
    static let title: #evaluated String = "Bar"
   // OR (most verbose, the actual full syntax)
    static let title: #evaluated String = #evaluate( "Bar" )
   // For now,  #evaluate() only accepts literals )
}

Constant property:

struct Baz {
    let title: #evaluated String
    init(title: #evaluated String) {
        self.title = title
    }
}

let b1 = Baz(title: "One")
// Also valid:
let b2 = Baz(title: #evaluate("Two") )
//Also valid:
let t = #evaluate("Three")
let b2 = Baz(title: t)

Constant static or property protocol requirement:

protocol NeedsConstGreeting {
    static let greeting: #evaluated String
    let instanceGreeting: #evaluated String
}

Using var for #evaluated types is prohibited:

var x1: #evaluated String // Error: a pre-evaluated String can't be modified. Use `let` instead.

But initializing a var with a compile-time constant is fine:

var x2: String = #evaluate("Hello")

We can interpret the above as a subtype relationship between Type and #evaluated Type

I believe the above design is general enough to cover the cases mentioned in the Future Directions section and constant expressions thread.

EDIT: Removed duplicated text. Somehow I managed to paste the text from my editor twice.

5 Likes

I don't particularly like the use of #evaluated as a prefix for types because most of the time # prefixed things are literal expressions (such as #file) that are replaced at compile time. I think let value: evaluated String would look a bit more swift-like.

Other than that I really like the idea of using #evaluate(\**\) to explicitly evaluate an expression at compile time because it builds off the idea that # prefixed expressions are replaced at compile time (again like #file).

3 Likes

Yes, type prefix is not ideal and needs further thought. We can also consider @evaluated even plain evaluated modifier for types.

I think I prefer evaluated more because it fits with inout. @evaluated feels more like it would be added before let and before the function argument labels (like @ViewBuilder) instead of before the type. As you said before, evaluated is almost creating a subtype and because of that I feel like evaluated should be a type prefix/modifier (because it is modifying the type).

evaluated would also be a language feature, but the @evaluated style is more often used for property wrappers defined in Swift code.

3 Likes

Agreed. evaluated works best if we stick with #evaluate() on the side of the expression (constant for now).

2 Likes

Sorry for diverting the conversation and I agree that we should focus.
I’m just curious if the core team doesn’t feel like this feature is part of a much bigger effort and thus could benefit from a wider goal, a manifesto of sorts.
Maybe is just me being afraid of discussing this specific addiction without having a clue of the big picture or maybe is because I disagree with the direction is taking ^^

The capabilities of compile-time evaluation aren't very relevant here indeed. But the syntax is though. If compile time evaluation works this way:

let x = const sin(4444)

func foo(bar: const Int) {...}
foo(bar: x)
foo(bar: sin(4444))

Then it sort of follows that compile-time constants can be expressed the same way:

let x = const 4444

func foo(bar: const Int) {...}
foo(bar: x)
foo(bar: 4444)

In other words, compile-time constants are generated by compile time evaluation. The first implementation of "compile-time evaluation" may be limited to only literals, but there will be room to grow. So I think, syntax-wise, it needs to be considered.

(I'm starting to like this syntax with const before the expression by the way.)

2 Likes

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.