[Pitch] Compile-Time Constant Values

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.

3 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.

2 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.)

1 Like

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

I agree with needing to provide stronger guarantees than constexpr in C++. The new consteval feature in C++20 aims to do just that because the above lack of clarity is often confusing. We need to keep these ideas in mind for the potential future extensions that discuss compile-time evaluation.

I can imagine a few cases where this could be very useful:

  • The property wrapper example in the pitch that requires an initializer parameter to be a compile-time constant value can also have other parameters that can be runtime values - it can still be of benefit to enforce this property for only some of the parameters here.
  • When using a library with available source, in the future we could potentially allow a library function to perform compile-time computation on only the subset of its parameters that are compile-time-known values. For example, to emit custom compile-time errors based on those parameter values.

I’m very happy to see this moving forward. Compile time evaluation is a missing piece of the language that other newish languages are coming out from the get go with.

I’m bot a huge fan of ‘const’ tho. Having ‘let’ in the language already makes the const wording to confusing imo. I would much rather go with something more specific like ‘comptime’.

I’ve recently come across some newish languages (Jai, Zig…) that offer the option to run arbitrary functions at compile time with the exact same language (as opposed to a limited form of meta programming). It’s cool that we start with constants but is there a clear end goal where we can run Swift at compile time or would we rely on a separate meta system?

6 Likes

A few questions and a bike-shedding suggestion:

  • Because all the examples used in the pitch are literals, are non-literals (e.g. enum cases) within the scope?

  • It's not mentioned in the pitch, but I feel it's implied that const values can be passed to non-const parameters. If this is true, can they be be precondition'd?

    func divide(_ dividend: Double, by divisor: const Double) -> Double {
        precondition(divisor != 0)
        return dividend / divisor
    }
    
    divide(0, by: 0) // does this crash in runtime or error in compile-time?
    
  • Similarly, in the future direction of compile-time expressions and functions, can compile-time known run-time crashes automatically become compile-time errors:

    let x = Int("1", radix: 0) // can this become an error in compile-time?
    if true { fatalError() } // can this become an error in compile-time too?
    
  • I agree with the opinions upthread that const is ambiguous and "brevity over clarity". Something like compileTime is better imo.

1 Like

The term is not rigorously defined but I would describe enum cases as literals too.

Definitely they can.

This is more about a future feature where functions could be compile-time interpreted and generate compile-time failures when passed constant values. Such a feature would allow libraries to do the same thing as the standard library types do e.g. generate errors on things like (256 as Int8), so a NoneEmptyCollection could compile-time error when initialized with [].

I think this is definitely a goal, and is related to this pitch, but not really part of this pitch. Should probably be discussed elsewhere unless it specifically pertains to how this feature behaves.

These maxims must be applied with pragmatism in mind rather than as hard rules. For example, you could say func is brevity over clarity, except that the clarity of function is vanishingly small. This isn't such an extreme case, but compileTime is really very verbose for something that may become moderately common in Swift source, and I find it hard to believe it would make anything more clear except for the very earliest encounter of the feature, which is not what we should be optimizing for (and even then, it really doesn't tell you what you need to know, you still need to look it up to understand).

6 Likes

I see people coming up with different names, but nobody has proposed constexpr yet as a name

I'd prefer something more like compexpr or comptime since IIUC the "constness" of the expression is merely a side effect of the source code not being changed while it's being compiled, rather than anything inherent to the expression itself. Come to think of it, even if that's not true, it seems like "compile-time" is much more the point than "constant" (a concept for which we already have let, at least for value types).

Unrelated to @vegerot's post, should we be thinking about how this might interact with "pure" functions? Is one a subset of the other? They seem at least related, but I've been up for I think 20 hours and it's not an area I know a ton about anyway.

1 Like

Since const implies let, couldn't const replace let, and const be used as a parameter qualifier?

let s = "foo"
const s = "foo"

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

struct Bar {
    static const title = "Bar"
}

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

protocol NeedsConstGreeting {
    static const greeting: String // { get }
}
4 Likes

Semantically, const is more a guarantee about the value and not about how the variable is stored (in contrast to let, var and static). So I believe that having const as a specifier for the value or type might be another good approach:

let s = const "foo"
let i: const Int = 1

That also stylistically matches using const as a type prefix in function signatures:

func constantFunction(string: const String) // ...

My reasoning for placing it there in functions is that const is in a similar category to inout (it's about how the value is passed to the function and what values are valid, unlike a result builder where it is changing how the value is interpreted).

7 Likes

Personally, I don't like the word const for this. let is already understood to be "a constant", and const in other languages has a different meaning from what is proposed here.

What about using fix for fixed as a three-letter sibling to var and let?

var a = "foo"
let b = "foo"
fix c = "foo"

A fixed value could become synonym to compile-time constant, which needs to be differentiated from a run-time constant. const makes that differentiation more complicated.


If we're not going to add it not as a new variable type, I think the non-shortened expression fixed would be better though:

fixed let d = "foo"

Or with @stackotter's suggestion above which I like better:

let e = fixed "foo"
7 Likes

I think const is clear when used as a prefix for expressions and types because there is a clear distinction between the semantics of let and const, however I definitely see where you’re coming from. Because certain languages (looking at you js) have made very different use of const (in js, const doesn’t even mean immutable, it’s more akin to a let variable containing a reference type - the value itself still being mutable).

fixed makes sense to me in terms of expressions and variable types, however as a specifier in function arguments it almost sounds like the parameter has to be the same every time (which wouldn’t make sense, but it’s how it reads). But that might just be a bias, because I already have an understanding of what const might mean from other languages.

Full everyday words like fixed might also lead to confusion when used with types like Bug or Point. To me const Bug() sounds more clear than fixed Bug(). Even when it’s not forming a play on words I still find myself leaning towards const, but I can’t pinpoint why other than I’m used to const

1 Like