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.)
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.
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:
...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.
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.
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?
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).
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.
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:
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).
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
I've always thought of literals as a piece of hard-coded data written in code, but I can see it being understood differently. If enum cases are covered in this pitch, do you think things that behave like them such as static Self typed properties like Double.pi are covered in this pitch too?
I understand and agree to an extent with the argument of pragmatism. However, I think the pragmatism of clear and distinct meaning should take precedence over that of less verbosity. As many upthread have pointed out, "constant", which "const" is short for, is too close in meaning in English to "immutable". This in my opinion makes the feature less teachable if we use the const spelling, because both "constant" and "immutable" have been used in documentation to describe let properties/values/declarations.
A search for "constant" in the latest TSPL shows 340 result:
This shows a clear history of "constant" being used as the primary description for let. It will be difficult for people new to the language to learn that even though we call let "constants", we only write const for some of them in code, and they are treated differently from other "constants", because they're const. Even for people already familiar with the language, const in my opinion still messes with the mental model after they have learnt about it. Because although writing in Swift is not writing in English, your understanding of the English word still seeps in when you use the word.
I agree with this statement from you:
but I think it's somewhat off the point.
I believe what many of us are saying isn't that we want longer keywords, but keywords that disambiguate from existing usage. The difference between the pitched feature and let is not the constant-ness, but the compile-time known-ness of the constant-ness. This is why keywords like compileTime and predefined are suggested as alternatives. If they are too long, then they can be trimmed shorter, but whether they're in long or short forms, they're more clear than const.
And here is a counter-example: def is to define as const is to constant. Although def is shorter than function (and func), just as const is shorter than compileTime, it shouldn't be a more pragmatic choice than function is, because it doesn't disambiguate from other declaration keywords.