[Pitch] Compile-Time Constant Values

I have quite a bit of sympathy for this line of thinking, too. I wonder if alternative keywords could take some inspiration from the way it is described or taught:

Perhaps keywords like known, verified, proven or revealed might (or might not!) be some alternatives?

static known let title = "Bar"
static verified let title = "Bar"
static proven let title = "Bar"
static revealed let title = "Bar"

I don't want to distract much from the deeper and more important discussion about the building blocks etc. But some initial ideas about a precise keyword or naming might help frame discussion and thinking about the problems compile time constants are attempting to solve.


Unrelated question. Does this pitch finally mean I can build programs with source code like this will fail to compile if the URL cannot be initialised?

static const let websiteUrl: URL = URL(string: "https://swift.org")
4 Likes

From this point pint of view, static let fulfills the requirement "guaranteed to not change".
So new keyword adds some additional meanings: "known or computed during compilation" and "not change after compilation".
As for "not change after compilation" – it can be omitted, because you know, changing value during compilation is something strange and useless. So "not change after compilation" and "guaranteed to not change" are equal meanings in this context.

As for aleternatives, I like static known let very much.

So, at least, we have 3 excellent alternatives to const let:
static known let
static predefined let
static compiletime let

and several good ones:
static verified let
static compilevalue let
static compilesythesized let
static proven let
static revealed let

What do others think?

2 Likes

I would imagine anything conforming to the ExpressibleBy*Literal class of protocols could be compile-time enforced

3 Likes

I think there was some talk of moving away from those protocols entirely, but certainly the existing ones should be allowed to be implemented as a compile time function.

Are there any constraints that prevent any type from being usable for compile-time values?

One of the challenges here is heap allocation. But in theory is should be possible to emit all the objects that were heap-allocated and not deallocated during compile-time interpretation as a static variable in .data segment with immortal ref count.

class Foo {
    init() {
        print("Init") // Printed as a note in compiler output
    }

   deinit() {
        print("Deinit") // Never executed
   }
}

static known let x = Foo()
1 Like

Came to post this, that would be neat !

3 Likes

As I understood, the first step in the proposal is just to store compile-time constants (that are assigned to literals) in the special type of variables, not to execute anything at compile-time (including any initializers or functions).

2 Likes

Is there any reason we couldn't drop the let entirely?

enum Thing {
  static const thing: String = "hello"
}
22 Likes

I like that. The triple var, let, and const would clearly reveal the intention in my book.

1 Like

This is a great option in my opinion. The only thing is that let has already a meaning of something constant, immutable.
But using of some new const_keyword instead of let & var make syntax clearer.

1 Like

Just curious: in case of a library func with a const parameter, what does compile time even mean here?

It would depend if the source for the library is available or not. If available, no problem, simply compile it with your own const parameters. If a binary-only library, I think you'd have to make sure to provide a public API without any const parameters.

literals are completely unrelated to the concept of being known at compile-time or not. for example, String and Character can only be instantiated at run time, since they depend on the ICU library. more generally, types imported from other modules can implement their own literal-initializers, which cannot run until the module has been loaded (possibly dynamically). this has important implications for types like Decimals and BigInts, which are often imported as shared dependencies, and may or may not be appropriately inlinable.

5 Likes

Any chance we can keep the # as a compile-time signifier, ala #if and #available? Does #let read well to people?

static #let foo = “foo”
func foo(#let param: String)

The func parameter looks a little weird to me in light of the deprecated var func parameters, but its meaning as an immutable-at-compile-time constant is otherwise clear IMO.

4 Likes

And why this dependency on ICU prevent instantiation at compile time ? The swift compiler can also depend on ICU and create a const representation.

This is what clang does with CFString literal. The C strings are converted to UTF-16 at compile time and stored as const data.

3 Likes

It gets problematic if you allow compile-time operations at an (extended) grapheme cluster level (including things like getting the length of a string), because the compile-time result may be different from the runtime result. This is especially true if, like C++, there are user-defined functions that can be executed both at compile time and at runtime; the same code might produce different results depending on whether the argument passed to it is a constant.

(Note that similar differences can happen between runs of the same compiled code on different versions of ICU.)

2 Likes

Unicode is only a problem if the data tables are part of a system dependency (ICU or the standard library on Apple platforms). If you statically link those things, your view of Unicode characters is also fixed at compile-time and could in theory also be evaluated by the compiler. I believe Unicode also makes forward compatibility guarantees, although I haven't examined them in much detail and they may/may not be sufficient for everything we'd want.

One issue that I've found with Swift is that it won't statically allocate objects (i.e. serialise them in the binary); StaticString is basically the only exception, and even simple arrays of RawOptionSets get initialised at start-time in the main function.

For example, take the percent-encoding table used by WebURL (Godbolt). If you check out the main function, you'll see that the compiler evaluated the array enough to reduce it to a series of magic numbers, but still initialises it at runtime. For us, part of the goal of the project is to be 100% Swift and not use any C shims, and it's only 256 bytes so the overhead is small, but it is a significant reason why the standard library's own Unicode support has to be written in C rather than Swift.

So what I wonder is: will we be able to support compile-time constant values that allocate memory? It doesn't seem like we can today. I'm not just talking about Arrays; also user-defined types built with ManagedBuffer etc.

7 Likes

Moved to a separate thread per request.

Original comment

There definitely are situations where the compiler can outline complex values directly into the data segment of the binary, but I don't know what the limitations are. For example, this example (godbolt) with a simple Int and String struct gets converted to a data blob:

output.staticArray : [output.Foo]:
        .zero   8

mainTv_:
        .zero   8
        .zero   16
        .quad   6
        .quad   12
        .quad   10
        .quad   7234932
        .quad   -2089670227099910144
        .quad   20
        .quad   133540975310708
        .quad   -1873497444986126336
        .quad   30
        .quad   133541042677876
        .quad   -1873497444986126336
        .quad   100
        .quad   7236850741311532655
        .quad   -1513209474789907086
        .quad   1000
        .quad   8462097072821464687
        .quad   -1441151879073603213
        .quad   100000
        .quad   -3458764513820540908
        .quad   .L__unnamed_1+9223372036854775776

.L__unnamed_1:
        .asciz  "one hundred thousand"

...where the code to initialize it consists only of a type metadata lookup and then a call to swift_initStaticObject, which appears to be fast—it just populates the runtime metadata pointer at the beginning of the inlined data.

One cool thing about this is that I was able to use Strings instead of StaticStrings and it still worked—and in fact, the strings that could fit into the small string representation used that instead of using a pointer to separate character data.

I'm not sure what it is about OptionSets that prevents this from working, though. I think this happens as part of the ObjectOutliner SILOptimizer pass in the compiler, so that would be the place to improve.

The other drawback about this is that since it's an optimization pass, unoptimized builds will still get the slow code path that initializes everything at runtime. I wonder if this transformation could happen as a mandatory pass, so that debug builds could still rely on static data.

3 Likes

Moved to a separate thread per request.

Original comment

I poked at this a little bit more and the outcome was interesting (and beyond my understanding of how the optimizer works).

It turns out OptionSets by themselves aren't a problem; some of them are able to outline into static objects fine. Your WebURL case does if we stop the array after element 0x67. But once we add element 0x68, or anything after that, it stops outlining.

I looked more closely at the generated post-opt SIL and it looks like in the 0x00-0x67 case, the ObjectOutliner pass outlines the whole array into a single SIL value mainTv_, which is what we want.

In the 0x00-0x68 case, it looks like the individual arrays inside the larger array (the option set unions) are outlined separately (mainTv_...mainTv7_), and the overall array is never outlined.

Maybe the inner array outlining is an intermediate step to outlining the whole thing, and the 0x68th element pushes the number of basic blocks or instructions past some limit that's coded into an optimizer pass that makes it claim it's too complex to analyze, causing it to break down after that?

Hopefully someone like @Erik_Eckstein might have some more insight here. I've been looking into taking advantage of swift_allocStaticObject-based values for other projects but if they're sensitive to small perturbations in the input like this, then that gets a lot trickier.

3 Likes

This is great info to dig into but let's move this sub-thread into a topic under Compiler Dev rather than this pitch.

3 Likes