[Pitch] Compile-Time Constant Values

Hello Swift Evolution!

Compile-time constant values are values that can be known or computed during compilation and are guaranteed to not change after compilation. Use of such values can have many purposes, from enforcing desirable invariants and safety guarantees to enabling users to express arbitrarily complex compile-time algorithms.

We would like to start a discussion on the early steps towards building out support for compile-time constructs in Swift, beginning with basic primitives: a keyword to declare function attributes and properties to require being known at compile-time.

The topic has come up in handful of times in the past:

We would like to revisit this discussion by focusing on some initial basic building blocks towards this goal.

Please let me know your questions, thoughts, and other constructive feedback! If you have potential use-cases in mind for these kinds of language constructs, I would be very curious to hear about them as well.


Proposed Direction

Parameter const keyword

A function parameter can be marked with a const keyword to indicate that values passed to this parameter at the call-site must be compile-time-known values. This applies to initializers which may be required to initialize const properties using compile-time-known values - those passed into const initializer parameters.

func foo(input: const Int) {...}

static const property

A static const property must be declared with a compile-time-known default initialization value. For example:

struct Bar {
    static const let title = "Bar"
}

If title is not declared with a compile-time-known literal value, the compiler emits an error. Unlike a plain static let stored property, a static const let property does not need to refer to a location in memory shared by all instances of the type, and can be elided by the compiler entirely.

const property

A property on a struct or a class can be annotated with a const keyword, requiring it to be initialized with a compile-time known value, using a const initializer parameter or a default literal value (making it equivalent to a static const property). A const property guarantees that the compiler is able to know its value for every instance of the type. For example:

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

Not initializing title using a const initializer parameter results in a compilation error. In the future, this notion can be extended to allow for compile-time-known values of aggregate types.

Protocol static const property requirement

A protocol author may require conforming types to default initialize a given property with a compile-time-known value by specifying it as static const in the protocol definition. For example:

protocol NeedsConstGreeting {
    static const let greeting: String
}

If a conforming type initializes greeting with something other than a compile-time-known value, a compilation error is produced:

struct Foo: NeedsConstGreeting {
  // đź‘Ť
  static let greeting = "Hello, Foo"
}

struct Bar: NeedsConstGreeting {
  // error: 'greeting' must be initialized with a const value
  let greeting = "\(Bool.random ? "Hello" : "Goodbye"), Bar"
}

Motivating Example Use-Cases

Declarative Package Manifests that do not require execution

The Result Builder-based SwiftPM Manifest pre-pitch outlines a proposal for a manifest format that encodes package model/structure using Swift’s type system via Result Builders. Extending the idea to use the builder pattern throughout can result in a declarative specification that exposes the entire package structure to build-time tools, for example:

let package = Package {
  Modules {
    Executable("MyExecutable", public: true, include: {
        Internal("MyDataModel")
      })
    Library("MyLibrary", public: true, include: {
        Internal("MyDataModel", public: true)
      })
    Library("MyDataModel")
    Library("MyTestUtilities")
    Test("MyExecutableTests", for: "MyExecutable", include: {
        Internal("MyTestUtilities")
        External("SomeModule", from: "some-package") 
      })
    Test("MyLibraryTests", for: "MyLibrary")
  }
  Dependencies {
    SourceControl(at: "https://git-service.com/foo/some-package", upToNextMajor: "1.0.0")
  } 
}

A key property of this specification is that all the information required to know how to build this package is encoded using compile-time-known concepts: types and literal (and therefore compile-time-known) values. This means that for a category of simple packages where such expression of the package’s model is possible, the manifest does not need to be executed in a sandbox by the Package Manager - the required information can be extracted at manifest build time.

To ensure build-time extractability of the relevant manifest structure, a form of the above API can be provided that guarantees the compile-time known properties. For example, the following snippet can guarantee the ability to extract complete required knowledge at build time:

Test("MyExecutableTests", for: "MyExecutable", include: {
        Internal("MyTestUtilities")
        External("SomeModule", from: "some-package") 
      })

By providing a specialized version of the relevant types (Test, Internal, External) that rely on parameters relevant to extracting the package structure being const:

struct Test {
  init(_ title: const String, for: const String, @DependencyBuilder include: ...) {...} 
}
struct Internal {
  init(_ title: const String)
}
struct External {
  init(_ title: const String, from: const String)
}

This could, in theory, allow SwiftPM to build such packages without executing their manifest. Some packages, of course, could still require run-time (execution at package build-time) Swift constructs. More-generally, providing the possibility of declarative APIs that can express build-time-knowable abstractions can both eliminate (in some cases) the need for code execution and allow for further novel use-cases of Swift’s DSL capabilities (e.g. build-time-extractable database schema definitions, etc.).

Enforcement of compile-time attribute parameters

Attribute definitions can benefit from additional guarantees of compile-time constant values. Imagine a property wrapper that declares that the wrapped property is to be serialized and that it must be stored/retrieved using a specific string key. Codable requires users to provide a CodingKeys enum boilerplate, relying on the enum’s String raw values. Alternatively, such key can be specified on the property wrapper itself:

struct Foo {
  @SpecialSerializationSauce(key: "title") 
  var someSpecialTitleProperty: String
}

@PropertyWrapper
struct SpecialSerializationSauce {
  init(key: const String) {...}
}

Having the compiler enforce the compile-time constant property of key parameter eliminates the possibility of an error where a run-time value is specified which can cause serialized data to not be able to be deserialized.

Enforcing compile-time constant nature of the parameters is also the first step to allowing attribute/library authors to be able to check uses by performing compile-time sanity checking and having the capability to emit custom compile-time error messages.

Open Questions

  1. Effect on ABI Stability: Should adding or removing the const keyword constitute an ABI change?
  2. Should const keyword become a part of mangled name?

Future Directions

  • Compile-time types - allow types consisting of const properties to be treated as compile-time-known values.
  • Compile-time expressions - allow expressions that operate on compile-time-known values, from binary operators to control flow.
  • Compile-time functions - consisting of expressions operating on compile-time-known values and const parameters.
24 Likes

I think such a discussion is better suited for the “Evolution: Discussion” category, rather than “Evolution Pitches” (where this topic is currently located).

(As an aside, I think a lot of proposal topics would be better off begun in the Discussion section, rather than jumping straight to the Pitch phase.)

• • •

It would also be helpful if you could link to some of the previous threads on the topic of compile-time evaluation of code and constants.

I've added the previous threads I am aware of to the intro, thanks @Nevin.

1 Like

I like the addition of compile-time constants.

What’s the rationale behind (re-)using const from other languages where it’s meaning is highly confusing (such as C/C++ and the const char *const)?

3 Likes

I think one can separate "const as a familiar term of art" from "C pointers have two aspects that can be const and the way you indicate which positionally is deplorable".

I think there's plenty of room for better suggestions, but I would seed that discussion with two things:

  • const is to constant as func is to function
  • waffly compound names like compileTimeEvaluable are bad
11 Likes

This is also pretty much exactly how Rust uses const, so there's that.

EDIT: This isn't really how Rust uses const, I'm not sure why I said it that way. But the model feels similar in a way that makes the use of const seem pretty natural to me. Regardless, while spelling is important, the semantics are more important.

I'm excited at the idea here. I might have more to say later.

7 Likes

I would prefer a term closer to zig’s comptime that way this can be used for the use cases in the future directions.

https://zigforum.org/t/precedent-for-zigs-comptime

8 Likes

If this were implemented, could StaticString be deprecated, and replaced with const String?

10 Likes

This kind of inhabits a different space. To be honest, there is very little reason to use StaticString these days over a regular String, as the regular String is initialized with a pointer to a string in the const section and won't perform any reference counting. There may be a teeny tiny edge because at runtime StaticString doesn't even need to check if it's in this form – but the need is so small that if StaticString didn't exist today, I don't think it would be worth proposing.

But what's being pitched here is one step more constant, because while a StaticString itself is static, the value of a StaticString variable isn't:

struct Bar {
  let greeting: StaticString = Bool.random ? "Hello" : "Goodbye"
}

The goal of const here is to guarantee the value of greeting is known at compile time, unlike in this example.

11 Likes

The proposed ability to mark function parameters as compile time constants is a definite improvement over C++'s constexpr functions, but I think constexpr's ability to mean potentially a compile time constant is equally valuable and should be explored.

I would love for Swift to allow me to do something equivalent to this:

struct Point {
    float x, y;
    constexpr auto movedBy(float dx, float dy) const noexcept {
        return Point{x + dx, y + dy};
    }
};

// Compile time constant
constexpr auto origin = Point{0, 0};

int main() {
    auto x = /* ... read x from input ... */;
    auto y = /* ... read y from input ... */;

    // not a compile time constant
    auto pos = origin.movedBy(x, y);
    return origin != pos;
}

EDIT: Having said that, I would like Swift to be more aggressive with guaranteeing compile time values than C++ is. In C++ something like:

#include <cstdio>

constexpr auto f(int x) noexcept {
    auto sum = 0;
    for (auto y = 0; y < x; ++y) {
        sum += y * x / 2 + y;
    }
    return sum;
}

int main() {
    printf("f(14) = %d\n", f(14));
}

The f(14) isn't guaranteed to be evaluated at compile time even though it's only input is a literal. Clang and GCC will constant propagate it, because they have a good optimizer, but MSVC won't.

An ability to pass some parameters as const and others as non-const is quite hard to understand. If the function receives at least 1 non-const parameter, it can’t be executed at the compile-time. So why should it bother if the parameter is known at the compile-time?
applying a const keyword to declarations (let and func) seems more reasonable to me.

3 Likes

It's not really relevant to this discussion, but I think this remark is perhaps a bit too flippant. For low-level performance, StaticString can be invaluable.

As for this proposal? +1, although I'm uncomfortable with the name const in a language which emphasises value semantics. The word "constant" can be easily confused to mean "immutable"; all let values are in some sense "constant", but they are not all known at compile-time (consider what developers might understand by a const Array<Int>). Thesaurus.com even lists them as synonyms:

I really like the lack of ambiguity in @compilerEvaluable... but it is incredibly verbose. Perhaps we could use something inspired by Zig and go with @compileTime Int?)

func foo(input: @compileTime Int) {...}

I think that strikes a nice balance between clarity and brevity.

14 Likes

Looking forward to this and especially the future compile-time metaprogramming directions listed.

An ability to pass some parameters as const and others as non-const is quite hard to understand. If the function receives at least 1 non-const parameter, it can’t be executed at the compile-time. So why should it bother if the parameter is known at the compile-time?

@mfilonen2, there are two benefits I can think of:

  1. In reasoning about the variable inside the function, you know it can never change at runtime.
  2. The compiler can do constant folding/propagation with the constant parameter even if there are non-const parameters, sometimes reducing the function itself to a constant. I don't know how much the compiler team plans to implement that, but it becomes a possibility with const parameters.

Me too. But I agree: let’s nail down semantics first.

1 Like

Is it useful for the compiler know whether the result of invoking a non-@transparent initializer with const parameters is itself constant? If so, how does it know this?

1 Like

I've got some questions that aren't really answered in this pitch:

What types can these compile-time constants have? In the pitch we see Ints and Strings, I would imagine that probably the other integer and floating point types, and also Bools will be supported as well. What about Array and Dictionary?
And how are these types being 'selected' for being able to be the type of a compile-time constant? Maybe we need one of the parts of the future directions as well, namely this one:

Otherwise, Int, String and so on would have to be special-cased in the compiler, which would probably also work, but the pitch says nothing about it.

Apart from these questions, I'm really glad that this topic is moving forward again. I think that it's definitely about time, that Swift gets some support for compile-time constants.

4 Likes

I'm curious about the inconsistency of the placement of the const keyword, as shown in the example above.

Naively, I would have expected either:

let title: const String
init(title: const String) { ... }

or

const let title: String
init(const title: String) { ... }

Or is there a reason for why function parameters need to be treated differently?

FWIW, the latter alternative seems more accurate to me, since const is a behavior of the property/parameter rather than the type. This reminds me of previous discussions justifying why result builder attributes are positioned before the parameter name: init(@ViewBuilder content: () -> Content) { ... }, etc.

4 Likes

I think const looks like an argument label in init(const title: String). Putting const before the type (i.e. const String) is similar to putting inout and etc. Unless we decide to treat const not as an argument label in the previous example, in which case it might be source breaking but probably very minor.

6 Likes

I think that this is too close to an argument label. But I actually think, that const (or however it will be called in the end) could actually be a property of a type (instead of being a property of a variable) and could join the growing list of keywords in front of type names (inout, some, soon probably any).
That would mean that const T would have to be implicitly convertible to T.
Also, this would be possible:

let foo: const String = "String"
let bar = foo // type of bar is 'const String'
let baz: String = foo // type of baz is 'String'
4 Likes

static const let looks very strange in Swift, because let already has meaning of constant.
One more thing, that static also mean something fixed, unchanging. static var currently is a bit strange, but it is ok. At least it is not misleading at all.
But static const let is frustrating. Looks like echo from Objc.

My opinion is that const is not a good word for the purpose. Here are some variants:
static predefined let
static compiletime let
static compilevalue let
static compilesythesized let
...

PS: For my own static compiletime let is the best variant for now. It is interesting to see variants from others.

I have also some questions about example:

struct Baz {
    const let title: String
    init(title: const String) {
        self.title = title
    }
}
  1. Is const let title: String have better perfomance in camparison to let title: String and static let title: String?
  2. Is const let title: String has differences in how CoW working with such values? How copy of such value will be made? What if it is mutated?
  3. How const let title: String differs from current StaticString? Do we need StaticString when const will appear?
4 Likes