[Pitch] Literal Expressions

Hi Swift Forums. :waving_hand:

SE-0359 has previously been proposed as a foundation for compile-time programming constructs in Swift. The proposal was returned for revision with concerns which include calling for a broader vision of constant evaluation to be outlined before such constructs are added to the language.

While that work is ongoing, this pitch is for a new feature called “Literal Expressions” which introduces a simple compile-time constant-folding mechanism which targets a much narrower scope within the language in order to address a couple of significant limitations in language constructs which today require the use of integer literal values.

Any feedback is welcome, I am interested in hearing your thoughts!

Current version of the pitch is on GitHub:

17 Likes

Thanks Artem.

What happens when you have a custom operator shadowing a standard library operator and you use the same operator in a literal expression? I need to do this in front of a computer, but I understand that if you have a user-defined func +(Int, Int), it will take precedence over the standard library one. Given let a = 1 + 2 in these conditions, do I get…

  • a literal expression that uses the standard library +?
  • a non-literal expression that uses the shadowing +?
  • something else?

Essentially: do we get new lookup rules when the compiler sees the opportunity for a literal expression? If so, then I suspect that the pitch is actually source-breaking.

5 Likes

The proposal states:

Only the standard library definitions of these operators are recognized; 
user-defined operators and overloads do not participate in literal expression folding.

So my expectation is that you will get a non-literal expression that uses the shadowing +.

1 Like

While I understand the desire to keep the initial operator set manageable, the wrapping integer arithmetic operators (at least addition and subtraction) should be included, as it is quite common to use them in the contexts where compile-time evaluation is desired or necessary.

Perhaps as a future direction, it would be ideal if there was a mechanism for the body of an inlined function to ascertain if it is being evaluated in a literal expression context so that different diagnostics could be produced. E.g. << might want to produce a warning by default when its literal RHS is over-wide (such that the result is always zero), even though that’s a perfectly normal and supported situation for a non-literal RHS.

12 Likes

Just read the whole pitch, love it, :smiling_cat_with_heart_eyes: I presume let e = Int.max / 2 will produce a different number when targeting 32-bit platforms? As with Steve's concerns about bit shifts, I think some type of warning would be useful for all use of UInt/Int in literal expressions, maybe only when targeting 32-bit, because of the likelihood of introducing errors.

2 Likes

The result type must be one of the standard library integer types: Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, or UInt64.

What about Int128 and UInt128?

2 Likes

Literal expressions in generic argument position must be enclosed in parentheses to disambiguate from the type-argument parsing context, where < , > , and , tokens serve as delimiters.

I regret to inform you that parentheses are already valid in type-argument parsing, Set<(Int)> or (more usefully) Array<(any P)?>. That doesn't mean we shouldn't require parentheses for literal expressions in generic argument position, but it does mean they can't directly be used for disambiguation.

1 Like

This is fine, the way the paren disambiguation works is that we do a lookahead by skipping over (...) and then see if the subsequent token is > or , i.e we expect a "primary expression" with no postfix grammar. For (Int) we can simplify the expression back into a type in pre-checking (same as we do for types appearing in expression position).

1 Like

Ah, I see what you mean. It's not "disambiguate from the type-parsing context" in that the thing that's parsed will always be a literal expression; it's that without that there'd (potentially) be a problem with unbalanced <> (I think comma would actually be fine). And so since parentheses are always matched (except in recovery paths), expression parsing can activate on this limited region, and then it can be turned back into a type afterwards. Thanks for elaborating!

(Array<(Array<Int>)> is your slightly-more-interesting test case.)

1 Like

I don't think that's source-compatible, since () can appear as a type production today. You can write foo <(a < b, c > .d)> today, and that will get parsed as the generic type foo with the type argument a<b, c>.d. IIUC you're suggesting that it would start getting parsed as an expression instead, which would lead to it being parsed as a tuple expression ((a < b), (c > .d)). (Now, the existing parsing heuristic isn't perfect either, but to my knowledge, nobody has noticed, and it might be true too that this new rule is "good enough" in practice because nobody writes things that way intentionally.)

2 Likes

A literal expression may reference another variable by name, given that the referenced variable is a Swift let binding with a default initializer which is itself a literal expression. This includes variables declared with @section, module-scope and static let bindings, and constants imported from C-family languages. For C imports, values visible to the Swift compiler as constant-initialized are resolved to their value so static const int declarations and simple #define integer macros can participate directly.

References are resolved recursively: when a literal expression references a variable, the compiler folds that variable's initializer to a literal value, then uses the result. A chain of references is followed until a root literal is reached or an initializer is encountered which cannot be constant-folded. No explicit @const annotation is required on referenced variables. The compiler automatically infers the constant-foldable property by inspecting the variable's initializer.

Maybe I missed it, but it looks like the proposal here doesn't discuss what happens with let bindings across Swift modules. If we allow a let binding module to be implicitly used as part of a literal expression in another module, that would become an implicit contract on the defining module that the let binding will always be defined with its current value, which is not part of the ABI contract today. It seems to me that this should not be allowed by default, unless the defining module adds some annotation (such as the @const annotation suggested in the text) to make that promise.

6 Likes

This is a good point, and it's twofold: not only can the value be depended on (e.g. "Foo.numItems matches up with Bar.maxCapacity") but the fact that it's a constant at all ("I can make an InlineArray of Foo.numItems values"). Both of those seem like bad news for preserving source compatibility between releases.

I think binary compatibility may actually be okay, because with library evolution on, lets are only exposed as read-only vars. Or am I misremembering?

4 Likes

Yes, cross-module references to let bindings which do not opt-in to some explicit contract (in the future with e.g. @const) should absolutely not be allowed with Literal Expressions.

2 Likes

I think this is ambiguous and I wonder if you could update the text to resolve the ambiguity. My read of "user-defined operators do not participate" is "user-defined operators are not considered during resolution", not "if resolution chooses a user-defined operator, then the expression cannot be folded".

Compounding the confusion to me is that it is presented as an extension to the Swift grammar. For instance, with this excerpt:

generic-argument → type
generic-argument → '-'? integer-literal
generic-argument → '(' literal-expression ')'

it appears to be implied that generic-argument, which was previously limited to integer literals, now accepts binary-operator as found in literal-expression → literal-expression binary-operator literal-expression, and binary-operator is a special list of operators for which the compiler always selects the standard library implementation.

In the general case, I think it's better to look at this as an additional semantic restriction on top of expression grammar. But there does need to be some change to the grammar for generic arguments to admit arbitrary expressions, since the current set of productions allowed as a generic argument is limited in order to allow us to heuristically disambiguate the left angle bracket from the less-than sign at parse time.

4 Likes

I noticed that the section of the proposal on integer generic parameters doesn't address how literal values will be represented in module interfaces, even though the sections on @section and enums do. It seems to me like we have to confront the question of how to represent literal values in .swiftinterface files for integer generics since something like this ought to be accepted:

let bufferSize = 64

public struct Buffer<T> {
  public let buffer: InlineArray<bufferSize, T>
  // ...
}

Printing the resolved literal value, instead of the initializer expression, seems best to me because it avoids the potential for reinterpreting the resolved value differently in a downstream compilation. It also side-steps the type checking rules that would needed to avoid printing an expression that is guaranteed to compile when re-compiled from the context of the .swiftinterface (in the example above, bufferSize is implicitly internal and therefore cannot be referenced in the printed interface).

Module interfaces do not emit explicit raw values for enums, so neither the original expressions nor folded constants appear in .swiftinterface files.

This is true for most enums but @objc enums (and presumably @c enums as well after SE-0495) actually do expose their raw values in .swiftinterface files.

1 Like

Ah, that’s a good catch. I commented on this later in the proposal, in the ABI compatibility section:

For integer generic arguments, the folded value appears as part of the type in module interfaces (e.g., InlineArray<5, Int>)

But it certainly needs to be mentioned in the generic values section itself, thanks for the catch.

Ah, this is definitely an oversight in the proposal. I will ensure the compiler does the right thing here and expand the relevant proposal section. My expected behaviour is going to be the same as above: textual interfaces will print folded values. This behavior is in line with the overall approach of “compilation products are identical to those where the user wrote the literal value by-hand”.

4 Likes

Yes, to be clear I don't think there is a problem with the intent, but it seems to me that the text currently doesn't match the intent.

2 Likes

Yeah, if we are committed to printing folded values that's great. I think that's worth calling out explicitly. I'd also recommend the proposal avoid making claims about the .swiftinterface representations of each of the individual constructs, then, since those might evolve over time.

1 Like

Could we not call these “literal expressions”? That is already a thing.

3 Likes