Having worked on the code for conditional compilation in Swift's parser (lib/Parse/ParseIfConfig.cpp
), and worked on constant-evaluation subsets of other languages, I do not think the former is appropriate as a basis for the latter: it's very specialized and underpowered code, has almost none of the machinery one needs to do this properly.
There is a much more complete basis for surfacing constant evaluation in the language (i.e. with a const
or constexpr
keyword) in the already-existing and working mandatory SIL constant folding pass (lib/SILOptimizer/ConstantFolding.cpp
), which includes implementations of the builtins and can be extended with extra functions via the @_semantics
attribute (eg. see isApplyOfStringConcat
which looks for @_semantics("string.concat")
that's on the standard library's implementation of func + (lhs:String, rhs:String)
.
The difficult thing about surfacing constant evaluation in a language (IMO) is that beyond just "knowing that this thing is evaluated at compile time" -- which is not always especially useful -- many people want it as a feature specifically so that it can interact with other static systems in the compiler earlier than the mid-level IR, i.e. the conditional compilation system (as you note) or, more commonly, the type system.
The latter happens in a few surprising yet persistent cases: computing scalar components of types like enum discriminants and pattern constants (in order to check for disjointness, say), and also when people want to statically compute fixed-size array types, which is an immediate request once you add fixed-size array types to the language (and they're on the list of things people have argued for adding to future Swift iterations). This sort of requirement -- constant eval intertwined with type checking -- breaks the phase ordering of a conventional compiler like swiftc
, so would be a very significant undertaking. It can be done, but it's a lot of work (eg. watch the very slow process of the Rust compiler shifting from an ad-hoc high-level constant evaluator to the new MIRI evaluator, while trying to carve out a decidable boundary for const
evaluation within the type system.)
Indeed, I suspect a major reason why there is a miniature constant evaluator up in the Swift parser is that the phase ordering of the compiler is too rigid to support SIL-ifying compiler-control expressions and evaluating them that early: especially given how much of the language is defined by library code, it's impractical to defer conditional compilation until the presence of a typechecked and lowered stdlib (at least in the current phase-ordering of the compiler). The same is true of the conditional compilation expressions in Rust, FWIW: there's a miniature evaluator for the custom mini-language in attribute expressions, run in early parsing phases, that remains detached from MIRI and probably always will. Not enough of the rest of the world even exists at that point.
(Similarly it's worth noting that LLVM has a gigantic amount of constant evaluation machinery of its own, through many layers of committing-to-facts, all the way down to expressions evaluated in the assembly and linking phases)
It's not ideal to have redundant constant evaluation anywhere at all, but it should be kept to as few as possible, and IMO not replicated yet again at the high level (AST) or in the type system. You really want to centralize as much logic as possible, i.e. by making it possible to synthesize and evaluate small, cheap fragments of that IR, if not in the middle of parsing (which is usually very early, i.e. it even involves masking out unrecognized syntax) then at least by the middle of typechecking.
(Sorry this reply is a bit rambling -- lots to say on this matter and not clear which order it's most useful)