Compile-Time Constant Expressions for Swift

Definitely +1 to #assert.

I agree that @compilerEvaluable is pretty unwieldy. There is probably a better name.

@compiletime sounds a bit like the function will only run at compile time, which isn't true -- when you call one of these functions with an argument whose value is only known at runtime, the function will run completely normally at runtime.

@const (or @constant) sound like they more accurately reflect what happens. Would this confuse people familiar with C/C++, where const means something quite different?

4 Likes

That is true re: @compileTime. I’d be perfectly happy with @const, @constant or even @constEvaluable if you wanted to eliminate the chances of C++ folks being confused. However I don’t think that’s a huge concern since immutable self is the default in Swift. If a C++ person happens to see someone else’s code with @const and tries to use it like they’d use const member functions in C++, the worst that could happen is that they get a compiler error telling them some expression isn’t evaluable at compile-time, and then realize that “oh, @const doesn’t mean what I thought it did”.

Rust also uses the const keyword for its analogous functionality (still in nightly), so we have that going for us.

1 Like

From the point of view of language features that require compile-time evaluation, only @compilerEvaluable functions run at compile time. For example, #staticAssert(foo()) only works if foo is @compilerEvaluable, even if foo just returns a constant.

I imagine that the implementation work might enable some new constant-folding optimizations that operate on non-@compilerEvaluable functions, but we won't specify any of this in the proposal, because it doesn't affect Swift semantics.

I see @inlinable and @compilerEvaluable as quite different things. @inlinable enables some optimizations, but does not make the function behave any differently from the language's point of view. @compilerEvaluable adds a major new ability from the language's point of view: static features like #staticAssert can call the function. I do not think that a parameter to @inlinable should change its behavior so much.

Looks like it's a first step towards removing StaticString! Our proposal doesn't add support for strings (to keep it simple). Future work could add support for strings. Then we'd have to do something to remove StaticString in a sufficiently backwards-compatible way.

But why do we have to have #staticAssert and assert, #if and if. Why can't the compiler just evaluate things if it can? What is the advantage of #<anything>?

1 Like

Determinism. If the developer wants to be sure something is evaluated at compile time, he wants the compiler to let him know if it can't do so. The only way the compiler can be sure is if the directive is explicit.

11 Likes

Again, this is well covered in the proposal. If you want to counter this pitch with a no attribute, no compiler directive solution then it would be great if you responded to the points made in the proposal about resilience, etc. Though I'm not sure what the counter-proposal would be (do nothing?) because the compiler is already free to evaluate and simplify things where it provably can, that being the basis of many optimisations. This is a feature that I see as building a sound base for constructing other features on (e.g. hopefully fixed length arrays some day) that won't break catastrophically and non-locally, or silently turn compile-time errors into runtime errors, when you update a dependency or make seemingly innocuous changes to your code.

8 Likes

Also because #if can be used where if cannot, e.g. at file scope and type scope.

4 Likes

I'm not sure #if vs if is the best analogy, since #if also happens in a completely different phase of compilation, after parsing but before any semantic analysis has happened, which severely limits the kinds of predicates it can evaluate.

1 Like

Move it to a later phase? :slight_smile:

But seriously, I feel like that’s more of an implementation detail (a non-trivial one, sure) than some fundamental difference between if and #if.

Moving it to a later phase is somewhat at tension with what it's trying to accomplish, since #if decides what parts of the code get to move on to the other phases of the compiler.

5 Likes

I get that, but that wouldn’t change if you moved it later in the compiler. You’d typecheck and interpret only the condition at first, then depending on what it is, you move one of the branches through the rest of the compiler. Recurse until finished. Perhaps this level of complexity would be showstopping; I don’t know enough about the architecture of the compiler to make that decision.

As @anon31136981 said, I don’t see why the compiler can’t evaluate if early in the pipeline if it can and leave it to latter if it can’t. Having #if is just burdening the programmer with an implementation detail.

Actually just to be clear, I am in favour of #if constexprs being a separate thing from if statements. :slight_smile:

I do apologize for my part in derailing this thread with #if discussion though since that’s not in the pitch!

Yep, that's a good point, I agree.

The current proposal is "strength reduced" to supporting the minimum set of features necessary to get Int to work. This is why generics are included in this first proposal but existentials are not part of that MVP feature set. This is also why I'm fine dropping MemoryLayout from the proposal - it's an interesting application of the tech, but not a core part of the fundamental proposal.

-Chris

I don’t think the proposal is really about resilience, the first sentence of the proposal says “The ability to evaluate user code at compile time is an important ability that would allow Swift to support many inherently static features.” Note “would”; the proposal is about new capability. I am proposing that that capability is generalised and not reserved for annotated functions.

But to directly address the resilience issue:

  1. I don’t see the value, for the programmmer, knowing at what phase in the compilation process a function could be evaluated.

  2. I would not object to an annotation on an expression that marked that expression as should be compiler evaluated. The compiler could then give you a warning that it couldn’t evaluate the expression. This is quite different in nature to the proposal:

  • It is an annotation on use site.

  • It doesn’t enable a behaviour.

  • It just warns the programmer that their assumption is incorrect.

Edit: Fixed typo and formatting.

This is a pretty impressive proposal. It covers a lot of points and improves compiler controls, which have been relatively lacking so far in Swift. Maybe I missed this in the proposal, but we should be able to use #if with @compilerEvaluable (or whatever the name ends up being). But overall +1 for an incredibly impressive proposal.

1 Like

I meant that there are at least two sections[1][2] in the proposal that cover this, and the primary reasons given are, to paraphrase:

  • To provide a stable and understandable user model that supports good compile-time error reporting. Any implicit model here would, for example, shift errors from compile-time to run-time as you edited your code and unintentionally added or removed something from a function that meant the compiler could no longer evaluate it at compile-time (e.g. you add some logging to a function).
  • Resilience. If being evaluable at compile-time is not part of the contract of things exported from a module, then you need to either presume nothing from a separate module is compile-time evaluable or you accept that compile-time functionality will randomly break when you update your dependencies.

Do you disagree with this reasoning? What do you see as the use case for, or purpose of, this feature? I'm getting the impression from your replies that you might see it quite differently to the proposal authors.

3 Likes

I think there are a lot of potential downsides to having to mark functions as compiler evaluable, e.g.:

  1. Many functions will end up been marked as such. What's the point in a language that infers type to reduce clutter if it is cluttered with annotations everywhere?

  2. The annotation will spread through everyones code base, because if I want a function to be compiler evaluable then every function it calls needs to be compiler evaluable. This is what happened in C++ with const, seemed like a great idea to explicitly mark intent! In practice it was a nightmare were people had to go through whole existing codebases and annotate with const. Same thing with compiler evaluable, there will be whole discussions on Swift Evolution about which functions in the standard library need to gain the annotation.

To address points raised directly:

I don't think it achieves that goal. If I write:

val example = f()

How do I know that f will be called at compile time? An annotation that would achieve that is:

@compilerEvaluated val example = f()

The compiler could flag this with a warning/error to say that it couldn't evaluate f().

If you wanted to ensure that the function you wrote was compiler evaluable then write a test using @compilerEvaluated, as above. This does not make compiler evaluated part of a methods ABI, which I think is a good thing.

Compiler evaluable should be an optimization, not part of the definition.

PS For consistency I also object to @Inlineable, #if, etc for the same reasons, these shouldn't be placed as burdens on the programmer. It's not 1970, we are not writing C. Compilers have moved on and many languages now shy away from overly annotated code.

I’m in favour of the proposal generally.

But I think #staticAssert should be renamed as #assert; the behaviour of it is already implied.

I’m not familiar with how the compiler works, so I don’t know how feasible this is, but could we use @compilerEvaluable only on public functions? Within a module the compiler could figure out what is required to be compiler evaluable. For the resilience story you only need to define the public contract.

This will make @compilerEvaluable something that is only used by library developers (progressive disclosure) and it prevents spreading through everyone’s code base.

2 Likes