Multiple kinds of compile-time constant expressions

Continuing the discussion from Compile-Time Constant Expressions for Swift:

Since code has already been written, I hope my concerns don't end anyone's career. I'm hoping they can be incorporated with what's been done already. I came up with these questions after writing my latest fixed-size array manifesto.

In a previous constexpr thread, someone said that the MemoryLayout properties couldn't be constexpr because the implementation worked at the SIL level and types aren't laid out until after that. I reasoned that since type layout was delayed until the IR generation phase, I could move FSA bounds to that phase too. For example:

func byteCopy<T>(of t: T) -> [MemoryLayout<T>.stride ; UInt8] { /*...*/ }

This function converts a given object to its raw bytes, but statically sized and scoped. Obviously, since the MemoryLayout property can't be determined until IR-Gen, the array's shape is also delayed until IR-Gen. But what about an object like:

let sample: [delayed ; Int] = [1, 2, 3, 4, 5, 7]

where "delayed" is a (bike-shedded) keyword indicating that the given bound will be specified later. Here, "later" is pretty quick since we can see that the literal has six terms. The array's shape can be determined as soon as the AST phase. What difference does that make? Imagine we made a function like:

func makeTuple<T: [_ ; some Any]>(from a: T) -> ( #dup(T.staticCount ; T.Element) )

where "#dup" duplicates the given token a given number of times. You couldn't use this with a result from byteCopy, but using sample should be perfectly reasonable.

Even if you respond with "#dup" shouldn't be supported, the idea still applies to conditional compilation. The "#if" and related constructs should test and work as early as possible to exclude banned code from ever being compiled (by SIL and later; it still needs to be parsed).

This shows we need at least two kinds of constexpr, one that works at the AST level and another that works at the IR-Gen level. We could have ones in-between for SIL constant expressions, like others are working on now. There is a sub-typing relationship; anything that takes a IR-Gen constexpr should be able to accept AST and SIL ones, and SIL constexpr usages can accept AST ones.

What about @compilerEvaluable functions? What level should they have? They should probably work like rethrows; at IR-Gen level if at least one operand is an IR-Gen constepxr, otherwise at SIL- or AST-level depending on how early the last constexpr is determined. However, we need to mark a function requiring and/or returning a fixed level when necessary. For example, makeTuple needs to be marked that T.staticCount doesn't just need to be constexpr, but an AST-only constexpr.

With the different levels, we should had worked on either AST constexpr first, then generalized, or IR-Gen constexpr then refined. Now, we're in the middle with SIL constepxr and we need to expand both ways, and hope the SIL-to-AST refinement is compatible with the SIL-to-IRGen generalization.

Actually, what use cases are there for SIL-level constexpr? I showed examples for the AST and IR-Gen levels. Is there any reason for any constexpr that can't be done during AST to not be delayed until IR-Gen?

1 Like

Hi Daryle,

I'm still interested in this, but haven't had a chance to work on it recently. I don't think the phase ordering issues between sil and IRGen are significant - we can find a way to solve those problems. OTOH, I'm skeptical that introducing an AST-level constexpr is a good idea: this would be a significant amount of complexity and redundancy, and we'd end up having to reintroduce much of the mechanics of SIL into the AST level, which would be sadface.

-Chris