[Pitch] Compile-Time Constant Values

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

Even if a func is not pure, if we can run it at compile time it’s return will always be baked at compile time no?

I think the pitch includes the usecase where only some arguments are compile time known, and that guarantee is just used during optimisation, but does not result in a compile time known return value.

1 Like

Yes fair enough. I still think that we should have some idea of what’s the end goal for Swift here. I’m sure smarter people than me already thought about this but I’m afraid we are doing things now just for variables when there may be a better solution if though holistically.

I hadn't thought of this before. I really like this idea.

I think there are almost 2 separate goals at the moment (that are related).

  1. Allow for more aggressive optimisation of certain functions by guaranteeing which of their arguments are known at compile time (a form of currying).
  2. Create a way for complex compile time values to be created in the first place

For the second, I think marking function return values as constexpr (replace with your preferred keyword) is very important. Marking the return value would likely be done by prefixing the return type with constexpr because it is semantically a guarantee about the return value.

I think using constexpr as a prefix for expressions (and types in function signatures) is the most consistent notation.

If constexpr was instead a replacement for let it would be quite strange prefixing a literal argument with constexpr (because you can't do that with let).

Here is some example code that makes use of both aspects (creating compile-time values, and 'currying' functions).

let name = constexpr "stackotter"

// It is made clear that the function will return a `constexpr` and can be used
// to create further `constexpr` values.
func greet(_ name: constexpr String) -> constexpr String {
  return "Hello, \(name)!"
}

let greeting = constexpr greet(name)

// My only issue is that this is starting to get a bit too verbose. Perhaps
// if a function is called in an expression that is marked as `constexpr`,
// it is already clear beyond doubt that the arguments must all be
// `constexpr` too?
// Alternative: `let otherGreeting = constexpr greet("Taylor")`
let otherGreeting = constexpr greet(constexpr "Taylor")

// This is an example of using `constexpr` to perform a form of
// compile-time 'currying' (partially executing the function as
// much as possible when only some of the arguments are known).
func curriedAdd(_ lhs: constexpr Int, _ rhs: Int) -> Int {
  return lhs + rhs
}

let two = 2
let threeAddTwo = curriedAdd(constexpr 3, 2)

It felt really natural writing that example use case, and I think the notation makes the semantics extremely clear at each call site. For example, if literals were not clearly marked as constexpr, the semantics of curriedAdd would not be clear at all when used to define threeAddTwo.

That would be consistent with try/await :+1:

3 Likes