[Pitch] Compile-Time Constant Values

Oh yes, you're right, there. I'm late to the party, and all credits go to @JuneBash :+1:

The idea is great, I really like it. Sometimes we need to remove something instead of adding to make things better. If two people propose the same solution independently, then I guess the idea is great.

Thanks. To me the biggest advantage in introducing const on the side of let and var is that the fact of being "compile-time" is not an attribute of the type. There's no const String type. There are String values that are known at compile time.

This comes with some neat consistency, and unlocks compile-time constant values in pattern matching and loops as well:

// Variable declaration
var a = "foo"
let a = "foo"
const a = "foo"

// Pattern matching (in future directions?):
if case let .foo(bar) = value { ... }
if case var .foo(bar) = value { ... }
if case const .foo(bar) = value { ... } // OK if `value` is itself const

// Loops (in future directions?)
for element in array { ... }
for var element in array { ... }
for const element in array { ... } // OK if `array` is itself const

There is a more subtle consistency: const as a legal parameter qualifier (just as var used to be until SE-0003):

func foo(var input: String) { }   // ❌ Legal until Swift 3
func foo(input: String) { }       // Implicit let
func foo(const input: String) { } // New
1 Like

I'm re-reading the rationale notes for SE-0003:

‘var’ in a parameter list is problematic for a number of reasons:

  • Parameter lists currently allow both “inout” and “var”, and real confusion occurs for some people learning swift that expect “var” to provide reference semantics.

This is not an argument against const.

  • Parameter lists are not “patterns” in the language, so neither “let” nor “var” really make sense there. Parameters were patterns in a much older swift design, but that was eliminated a long time ago and this aspect never got reconsidered.

This is difficult to interpret for me.

  • “var” in a parameter list is odd because it is an implementation detail being foisted on the interface to the function. To be fair, we already have this with "API names" vs "internal names”, but this is a bigger semantic implementation detail.

Not this time: const is not an implementation detail, but a real part of the function signature (a change in the const-ness of a parameter is an api change).

Wouldn't it make sense to move the const marker out of the parameter list (presumably every param would need to be marked anyway?) to indicate that the whole function or initialiser can be used in a compile-time evaluated context provided that all of the parameters were also known at compile time? Like so:

func parseInt(_ string: String, radix: Int = 10) const -> Int? { ... }

This would also align well with async and throws, I think, even if const functions might be limited to synchronous calls only, for example.

Functions defined like this would of course need to be defined in terms of compile-time evaluable parts only in its body, but OTOH the function could also be called at run time with run-time known arguments.


I agree with others that const in place of let or var might be a natural place to declare compile-time constant values.

But could compile-time const var be a thing for constructing values procedurally for instance in the body of a compile-time evaluated function? If not, why not? (Edit: See Gwendal's and my posts below, no need for const values inside a const function where everything's implicitly const unless called at run time.)

1 Like

This is a neat idea, and really inline with this post by @Ben_Cohen: [Pre-pitch] SwiftPM Manifest based on Result Builders - #28 by Ben_Cohen

Maybe you'd not declare a variable as const var, but you'd need functions that can be executed at compile time:

func makeValue() const -> String { ... }
const value = makeValue()

I'm not sure "functions that can be executed at compile time" are part of the pitch, but they are a nice extension indeed.

For this, I think we'd need reconst, or else we'd be quite crippled (not even able to perform string concatenation):

// New declaration of String concatenation in the stdlib:
func + (const lhs: String, const rhs: String) reconst -> String { ... }

// OK now
func makeValue() const -> String {
    "foo" + "bar"
}
1 Like

Oh, right! Inside a const function, naturally every value (when called in a compile-time context) would need to be compile-time known as well. So inside a const function, every let would be essentially const too (except when called in run time) and var would indicate a compile-time mutable value.

No, that would be just const. We would need reconst for higher-order functions though, just like map needs rethrows.

Oh yes, you're right.

That is what enum cases are, hence why I would describe them as literals.

By that definition, and given the equivalence of static constructors and enum cases in many parts of the language, doesn't that mean any constant value from a static would be a literal? In that case, wouldn't the meaning of literal become too nebulous to be useful? We can say Swift hasn't rigorously defined what a literal actually is, but I don't think it's useful to expand the definition beyond usefulness.

No. Just because a static variable can be const and can be used similarly to an enum case doesn't mean they're the same thing. Enum cases are literals, static variables (even const ones) aren't, though they can be initialized from literals such as string literals or enum cases.

A literal is something that, at compile-time, is replaced by a call to a StaticString initializer that accepts a pointer to the value. This is nested in a series of additional initializers that, when run, ultimately produce the desired instance.

That’s my understanding, anyway, from a practical perspective. From the perspective of the grammar, it’s quite well-documented.

In terms of implementation, enumeration cases are very similar to literals: they are compiled down to a pointer to data. In terms of the grammar, however, they are clearly distinct.

1 Like

Ultimately, what compile-time constants boil down to is running Swift code during compilation. As such, I think we should also discuss the potential security, performance, and reliability implications. How should compile-time code report errors? How would it be debugged? Could maliciously-written compile-time code somehow attack the compiler? If so, how could that be prevented?

I’d also like to know if it would be possible to automatically evaluate code at compile-time under the right conditions, in a similar manner to automatic function inlining. The halting problem might foil that, of course.

3 Likes

I'd vote for compexpr. Sounds like a more accurate version of what constexpr does

1 Like

My main problem with const as another let or var type thing is that let and const are so similar, it’s not clear that it’s a different type of thing.

And in my opinion it is more of a type thing than a type of variable, because functions can’t limit whether their arguments are let or var (I’m not counting inout because that requires an &, it’s not the same as passing a const) yet they can require that the argument is a const String.

Another reason for not adding it as another let or var kind of thing, is that javascript uses var, let and const, and they’re used completely differently.

1 Like

I dislike const anywhere in the keyword. const is an artifact of the compile time evaluation, so why not name it after what it does? Some variant of: compexpr, comptime, comp. I second @stackotter, the distinction between let and const is somewhat arbitrary.

I'm just dreaming, but it seems compile-time expressions could play well with meta-programming and the AST.

2 Likes

Even tho I don’t think talking about the specific name memas a lot right now (I think there are other questions that need answers) I can’t avoid throwing out ‘baked’ as another keyword that I prefer way over const.

1 Like

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

let name = constexpr "stackotter"
let number = constexpr 1
func nameAndAge(name: constexpr String, age: constexpr Int) -> constexpr String

I haven’t seen anyone else mention it, but we also need to decide how guarantees about the function’s return value are denoted, because if a pure function has both arguments as constexpr, then it’s return will also be constexpr and it could be used to create other constexpr values:

let name = constexpr "The queen"
let age = constexpr 254
let s = constexpr nameAndAge(name, age)

Another reason for prefixing expressions with it instead of replacing let/var is that it is a guarantee about the expression (expr) not the storage mode as such.

2 Likes

Without being a compiler dev so just speculating:

  1. I think it makes sense, the thrown error should propagate up the stack up to the ‘compile time interpreter’ and that should report it as a compile time error. Hopefully with a file/line.
  2. That makes no sense to me. In my mental mode of just running Swift at compile time, that will give you an optional. Doesn’t matter that is comptime or not.

Another reason for prefixing expressions is that perhaps you want to use a literal when calling a constexpr function:

let age = constexpr -1
let string = constexpr nameAndAge(constexpr "stackotter", age)

It is important that the literal is clearly marked as constexpr so that the semantics are clear.

1 Like