Hello Swift Evolution!
Compile-time constant values are values that can be known or computed during compilation and are guaranteed to not change after compilation. Use of such values can have many purposes, from enforcing desirable invariants and safety guarantees to enabling users to express arbitrarily complex compile-time algorithms.
We would like to start a discussion on the early steps towards building out support for compile-time constructs in Swift, beginning with basic primitives: a keyword to declare function attributes and properties to require being known at compile-time.
The topic has come up in handful of times in the past:
- @marcrasi and @Chris_Lattner3 have pitched Compile-Time Constant Expressions, which would be a natural future direction to build on top of the building blocks described below.
- @CTMacUser had an earlier pitch for a similar concept of arbitrary compile-time evaluated expressions.
- Notably, compile-time evaluation is already being employed in Swift logging as first described in this post by @ravikandhadai.
We would like to revisit this discussion by focusing on some initial basic building blocks towards this goal.
Please let me know your questions, thoughts, and other constructive feedback! If you have potential use-cases in mind for these kinds of language constructs, I would be very curious to hear about them as well.
Proposed Direction
Parameter const
keyword
A function parameter can be marked with a const
keyword to indicate that values passed to this parameter at the call-site must be compile-time-known values. This applies to initializers which may be required to initialize const
properties using compile-time-known values - those passed into const
initializer parameters.
func foo(input: const Int) {...}
static const
property
A static const
property must be declared with a compile-time-known default initialization value. For example:
struct Bar {
static const let title = "Bar"
}
If title
is not declared with a compile-time-known literal value, the compiler emits an error. Unlike a plain static let
stored property, a static const let
property does not need to refer to a location in memory shared by all instances of the type, and can be elided by the compiler entirely.
const
property
A property on a struct
or a class
can be annotated with a const
keyword, requiring it to be initialized with a compile-time known value, using a const
initializer parameter or a default literal value (making it equivalent to a static const
property). A const
property guarantees that the compiler is able to know its value for every instance of the type. For example:
struct Baz {
const let title: String
init(title: const String) {
self.title = title
}
}
Not initializing title
using a const
initializer parameter results in a compilation error. In the future, this notion can be extended to allow for compile-time-known values of aggregate types.
Protocol static const
property requirement
A protocol author may require conforming types to default initialize a given property with a compile-time-known value by specifying it as static const
in the protocol definition. For example:
protocol NeedsConstGreeting {
static const let greeting: String
}
If a conforming type initializes greeting
with something other than a compile-time-known value, a compilation error is produced:
struct Foo: NeedsConstGreeting {
// đź‘Ť
static let greeting = "Hello, Foo"
}
struct Bar: NeedsConstGreeting {
// error: 'greeting' must be initialized with a const value
let greeting = "\(Bool.random ? "Hello" : "Goodbye"), Bar"
}
Motivating Example Use-Cases
Declarative Package Manifests that do not require execution
The Result Builder-based SwiftPM Manifest pre-pitch outlines a proposal for a manifest format that encodes package model/structure using Swift’s type system via Result Builders. Extending the idea to use the builder pattern throughout can result in a declarative specification that exposes the entire package structure to build-time tools, for example:
let package = Package {
Modules {
Executable("MyExecutable", public: true, include: {
Internal("MyDataModel")
})
Library("MyLibrary", public: true, include: {
Internal("MyDataModel", public: true)
})
Library("MyDataModel")
Library("MyTestUtilities")
Test("MyExecutableTests", for: "MyExecutable", include: {
Internal("MyTestUtilities")
External("SomeModule", from: "some-package")
})
Test("MyLibraryTests", for: "MyLibrary")
}
Dependencies {
SourceControl(at: "https://git-service.com/foo/some-package", upToNextMajor: "1.0.0")
}
}
A key property of this specification is that all the information required to know how to build this package is encoded using compile-time-known concepts: types and literal (and therefore compile-time-known) values. This means that for a category of simple packages where such expression of the package’s model is possible, the manifest does not need to be executed in a sandbox by the Package Manager - the required information can be extracted at manifest build time.
To ensure build-time extractability of the relevant manifest structure, a form of the above API can be provided that guarantees the compile-time known properties. For example, the following snippet can guarantee the ability to extract complete required knowledge at build time:
Test("MyExecutableTests", for: "MyExecutable", include: {
Internal("MyTestUtilities")
External("SomeModule", from: "some-package")
})
By providing a specialized version of the relevant types (Test
, Internal
, External
) that rely on parameters relevant to extracting the package structure being const
:
struct Test {
init(_ title: const String, for: const String, @DependencyBuilder include: ...) {...}
}
struct Internal {
init(_ title: const String)
}
struct External {
init(_ title: const String, from: const String)
}
This could, in theory, allow SwiftPM to build such packages without executing their manifest. Some packages, of course, could still require run-time (execution at package build-time) Swift constructs. More-generally, providing the possibility of declarative APIs that can express build-time-knowable abstractions can both eliminate (in some cases) the need for code execution and allow for further novel use-cases of Swift’s DSL capabilities (e.g. build-time-extractable database schema definitions, etc.).
Enforcement of compile-time attribute parameters
Attribute definitions can benefit from additional guarantees of compile-time constant values. Imagine a property wrapper that declares that the wrapped property is to be serialized and that it must be stored/retrieved using a specific string key. Codable
requires users to provide a CodingKeys
enum
boilerplate, relying on the enum
’s String
raw values. Alternatively, such key can be specified on the property wrapper itself:
struct Foo {
@SpecialSerializationSauce(key: "title")
var someSpecialTitleProperty: String
}
@PropertyWrapper
struct SpecialSerializationSauce {
init(key: const String) {...}
}
Having the compiler enforce the compile-time constant property of key
parameter eliminates the possibility of an error where a run-time value is specified which can cause serialized data to not be able to be deserialized.
Enforcing compile-time constant nature of the parameters is also the first step to allowing attribute/library authors to be able to check uses by performing compile-time sanity checking and having the capability to emit custom compile-time error messages.
Open Questions
- Effect on ABI Stability: Should adding or removing the
const
keyword constitute an ABI change? - Should
const
keyword become a part of mangled name?
Future Directions
- Compile-time types - allow types consisting of
const
properties to be treated as compile-time-known values. - Compile-time expressions - allow expressions that operate on compile-time-known values, from binary operators to control flow.
- Compile-time functions - consisting of expressions operating on compile-time-known values and
const
parameters.