SE-0531: Literal Expressions

Hi Swift community,

The review of SE-0531: Literal Expressions begins now and runs through May 29th, 2026.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

https://github.com/apple/swift-evolution/blob/main/process.md

Thank you,

Ben Cohen
Review Manager

13 Likes

Variables with internal , fileprivate , or private access are all eligible.

"unless they are @usableFromInline", presumably, since that likewise makes a decision available through the module's public ABI.


complaining about the name "literal expressions"

A frequent complaint about macros is that they don't have type information, or resolved identifiers, or anything like that. When I've explained the limitation to people in the past, I've pointed out that the module you work with is called "SwiftSyntax" and the macros themselves are described as "syntactic". This is usually educational because people have not stopped to think of why the word "syntax" is there, and that it means "based on the source tokens and structure only". It's not a strict thing, but it does provide an intuition about what is and isn't possible with that library and those macros alone.

"Literal" is a similar term. A literal (or "literal expression", even!) is an expression whose structure matches the tokens in the source code. An integer literal is a sequence of digits, optionally with a radix prefix. A boolean literal is a name so special we put it in scope everywhere. An array or dictionary literal has a fixed number of elements. A string literal can take several forms in Swift, but all of them are limited in the structure they produce to match the original string—though we certainly stretch it somewhat by allowing interpolations. Crucially, all of these describe syntactic structures, because a particular boolean literal may turn out to be an NSNumber; a string literal may turn out to be a SQLStatement; an array literal may turn out to be a Set. While other languages might challenge Swift's use of the word "literal" because they do not refer to pure compile-time constants, that ship has long since sailed. A "literal" in Swift refers to syntax, not evaluability.

I know the proposed "literal expression" can be explained as "an expression made up of literals" rather than "an expression that is literal", but in this limited form they're not really that either: no floats, no strings (even without interpolation), no arrays or dictionaries. And yes identifiers, for which you cannot make a syntactic determination of validity.

As proposed, the only places the name of this feature shows up is in the experimental feature flag and in diagnostics. If we don't have a good name for it yet, let's give it a clunky feature flag, like LimitedIntegerCompileTimeExpressions, and refer to it in diagnostics as "cannot be evaluated at compile time" or similar.

Please let's not muddy the term "literal" the same way C++ muddied the term "static".

8 Likes

Great pitch, love it! Especially the fact it's zero-cost syntax wise.

Considering float / string / etc are not done in this SE (but potentially in some future SE's) shouldn't this SE be called "Integer Literal Expressions"?

Couldn't @usabeFromInline just synthesize an opaque getter for use in public @inlinable functions?

I think "literal expression" is an okay name for this current proposal, but it does mean that when we get public constant values (however we end up spelling them), they're going to have to be defined in terms of being initialized from either a literal expression, another constant value, or the result of a compile time invoked function.

Not that I'm necessarily expecting compile time function evaluation to come in the same proposal as public constants.

In any case, this proposal seems reasonable to me. Programmers intuitively expect code like this to compile:

let numElements = 69_105
var array = [numElements of Int]

So much so that even C was forced to mandate support for this (for integers) in C2y, despite WG14 original stating they wanted to only support explicitly constexpr objects, because they believed the feature was just an awkward wart from C++ wanting code like this to work in C++98 before they came up with constexpr in C++11. So I don't think we should consider the alternative direction of requiring an annotation on each "literal expression".

Consider

public let foo = 42

vs

@usableFromInline let _foo = 42
@inlinable public var foo: Int { _foo }

Saying these represent different promises to clients of a library is extremely subtle at best, and disallowing the former but not the latter will result in people writing the latter with a comment saying "lol stupid compiler forced me to write this". Either there's a reason to forbid this or there isn't.

3 Likes

I'm confused, the former is already allowed, it just won't be a literal expression in a consuming module.

Unless I misread the proposal, and the former isn't allowed to be used as a literal even in internal code. That seems unnecessary.

Looks good, interesting that Int.max is now disallowed, no complaint.

I am particularly interested in this bit about future expansion:

Compile-time Bool values in turn enable control flow in compile-time expressions:
the ternary operator (condition ? a : b) and if/else used as expressions.
Together, these would significantly broaden what can be expressed
without requiring full compile-time function evaluation.

as I have toyed with Swift code like this now:

if SOME_C_CONSTANT > 3 {
// do runtime stuff
}

and this future feature would allow doing more with that, ie further compile-time elaboration. Perhaps stating the obvious, but some careful thought will need to be put into how to build that out.

A possible approach to making access to public variables work could be this to make the export of the computed value explicit:

@export(value) public let value = foo + bar
public let foo = 1
private let bar = 2

In the .swiftinterface you would just have public let value = 3 so no need to see the implementation of foo or bar. Public types could be required to depend on @export(value) variables only;

public let x: [value of Int] = ....

Of course, all types and properties would have their values available where supported internally to the module, even if not exported.

Jordan please expand on this. It's not immediately clear what foo buys you here – it's not a literal expression (as per the pitch).

Yeah, that's quite exciting future direction. Could reduce the number of #if/#endif significantly and simplify conditional variables maintenance.


BTW, is this going to be a literal expression? (+)(1, 2)

1 Like

I'd suggest calling this @inlinable(literal). It's basically the same thing as @inlinable (you share the implementation with the client module), only with a stricter contract promising the value will always be a literal expression (so that no newer version of a library will change this).

I'm actually surprised this isn't part of the proposal. Not having this means C modules have superpowers Swift module can't have.

Comment collapsed (it is not true), see next comment

My understanding is that @export is the replacement for @inlinable more or less, and that it is preferred over @inlinable because it is more explicit/has a better default with @export(implementation)

@inlinable is still its own meaningful thing. The equivalent (in library evolution mode) would have been @export(interface, implementation), but it was decided not to support that in the @export attribute.

That being said, I think we should be really cautious about adding yet another knob here for things like @inlinable or @export—we certainly don't want to complicate the story around those attributes further. How would something like @export(value) differ in practice from @export(implementation)? For a simple value property, the value is the implementation.

3 Likes

The difference is that this is usable as a constant:

@export(implementation) let constant = foo + 1
@export(implementation) let foo = 2

But this is not, despite still being valid code.

@export(implementation) let constant = foo + 1
public let foo = 2

In either case, marking constant as @export(value) would put let constant = 3 into the interface, avoiding a direct dependency on foo.

Someone is going to want to use a let as a literal expression in their own module and also a normal constant in another module. Under the proposal as written, this is disallowed.

References to variables with public , package , or open access are not permitted in a literal expression. Folding the reference would cause the referenced variable's initializer to become part of the module's ABI surface at every client of the module, which conflicts with this proposal's position that literal expressions introduce no ABI changes. Variables with internal , fileprivate , or private access are all eligible. Lifting the restriction on publicly-visible references is left to a future proposal that introduces an explicit opt-in mechanism, so that authors can choose to publish an initializer as part of their ABI.

If the intent was "references to let-constants in other modules are forbidden, except for those imported from C", the proposal should say that instead rather than appealing to visibility and ABI. I myself am not quite sure what situation is being protected against, because as written I think the proposal allows these cases:

internal let componentCount = 4
public typealias ColorComponents = InlineArray<componentCount, UInt8>
public struct Color { components: InlineArray<componentCount, UInt8> }
public var componentCountButExported: Int { componentCount }

But maybe the alias is forbidden on the usual grounds of "can't have a non-public decl referenced in a public type", and the struct just says Color has a dynamic size but not how it's computed, and the non-inlinable computed property is in fact a workaround people will use for a while until we have a syntax for exporting compile-time-available constants.

I also wonder how this rule interacts with @testable import.

But in any case, the proposal said this was about ABI, and that discussion relevant to public ABI would happen later, so I was trying to help by patching up one more case of "ABI-public". Let that not preclude challenging the restriction or its current shape!

1 Like

The proposal talks about using only literal expressions as integer generic arguments, but with our generics model it should be possible to use any integer value as an integer generic argument, even if it isn't literal or even isn't a global constant (if not now as part of this proposal, then as a future direction). What you fundamentally can't do with a non-literal-expression argument is use the underlying value for static type equality purposes with other literal expressions that might evaluate to the same value. Does allowing only literal expressions create challenges for us admitting potentially runtime generic argument values in the future? The proposal as is doesn't appear to address this as a future direction.

2 Likes

The basic premise of allowing expressions in place of literals makes sense to me, and is something widely supported in other languages. I'm curious how we would handle these expressions if they resolve to a type that isn't a compiler-recognized integer type (e.g. some custom type conforming to ExpressibleByIntegerLiteral and so forth.)

If the proposal covers that, I missed it!