[Pitch] - Macro literal protocols

Macros are only currently allowed at the top-level. However, there are certain cases where it would be beneficial to have nested macros.

For example, we could add new literal protocols with a macro requirement:

public protocol ExpressibleByMacroIntegerLiteral {
  associatedtype IntegerLiteralType: _ExpressibleByBuiltinIntegerLiteral

  @freestanding(expression)
  macro Init(integerLiteral: IntegerLiteralType) -> Self
}

And then with compiler magic, this:

let x: Decimal = 5.3

could turn into this:

let x = #Decimal.Init(integerLiteral: 5.3)

which would then be expanded.
I know that one of the design goals for macros has been to avoid this kind of invisible macro use, but there's already a lot of compiler magic with the expressible by _ literal protocols, and this would make them a lot more versatile.

For example, currently if your type is ExpressibleByStringLiteral but only some string literals are valid, your only choice is to trap at runtime if an invalid string literal is encountered. This defeats the compile-time nature of literals which should allow for checking of the literal. In the Foundation pitch for URL to be ExpressibleByStringLiteral this was an issue that was somewhat glossed over, but is totally solved with this.

And this:

downloadFile(at: "https://apple.com")

looks a lot better than this:

downloadFile(at: #URL("https://apple.com"))
5 Likes

Also, this would make the improvements to ExpressibleByFloatLiteral for decimals a lot less necessary, since Decimal could just use ExpressibleByMacroFloatLiteral.

1 Like

I actually prefer the latter. If it's a case where a string literal is going to behave differently than usual, I'd rather it were explicitly marked.

4 Likes

anything weird happening with a string literal at compile time with a macro, can also happen today at runtime with an ordinary ExpressibleByStringLiteral conformance, just less safely and without the possibility of static diagnostics.

6 Likes

I'm not at all arguing against static diagnostics. I just think that if a string is going to be treated differently (maybe even especially at compile time) it's good to make that more obvious.

Aside: Is 5.0 really an integer literal?

Wouldn't this immediately run into problems if two of these tried to turn the same type of literal into different types? Say something turn them into a URL and something else tries to turn them into an Image? I believe macros get resolved before the compiler can use the context to figure out what type it might actually need to be, and this seems like it would need some knowledge of the type the macro ends compiles to after expansion to properly pick the right one.

No it's a float literal. The integer literal protocol was just an example, there would be a macro literal protocol for each regular literal protocol

today, string literals will only ever be inferred to a type other than String if there is surrounding type context that would indicate so. (or if DefaultStringLiteralType is overridden.)

//  these already work today!
let url:URL = "https://swift.org"

try channel.fetch(url: "https://swift.org")

i would imagine that macros-as-literal-parsers would operate on similar principles, so i wouldn’t expect them to suffer from a lack of clarity in practice either.

4 Likes

This is what I was asking about above–I thought macros got resolved where before types come into play. Don't these all work (today) via finding overloads/constructors to provide the accepted types? I thought macros got resolved before typing happened as types can change as a result of their expansion.

I should clarify - I think the compiler would have to know not only the type of the input, but also the type of the output to decide if the macro applies. With just the input type, it would have to apply all macros that worked on that input type and determine what the result types are. But in either case, it means the whole type resolution would then have to factor the macro applications into the mix on trying to decide what to do.

1 Like

presumably we would use some sort of attribute hook the way we have done many times in the past with similarly shaped features. (@dynamicMemberLookup, result builders, property wrappers, etc.)

the way i always assumed macros-as-literal-parsers would work is that the macro would only serve to provide diagnostics and the actual parsing of the literal would remain the responsibility of the ExpressibleBy initializer witness.

it sounds like the issue there is the current macro implementation loses the syntax information after the source code has been typechecked. (not a compiler developer, so this might be completely wrong!) but i imagine there is a straightforward strategy here - preserve the syntax information (which is really just a string literal) until the diagnostic macros have gotten a chance to run.

I don't think it's unprecedented to let literals be "restricted" depending on the type context. We already have the protocols ExpressibleByGraphemeClusterLiteral and ExpressibleByUnicodeScalarLiteral which are just string literals that are restricted to one character or Unicode scalar respectively. With macros we can validate various other things at compile-time, such as string literals that are valid URLs, string literals with only ASCII characters, etc.

2 Likes

I think that's a really good point. Back in Swift 1 there was some adaptation required by the C-family folks to come to terms with there not being a distinction between string literals and character literals, but of course time has demonstrated that the sky has not fallen. :slightly_smiling_face:

(I say that as one of those who's first impression was "oh no, that's a terrible design choice" and was wrong)

I was ambivalent on this proposal 'til now. Given this (and earlier) demonstrations of how we already have this "magical" behaviour, I don't think it's a significant concern - any moreso than things like operator overloading being supported; yes, it can be abused to make code hard to read, but the pragmatical solution to that is: don't do that. :laughing:

4 Likes

And it allows pre-existing behavior to be expressed more cleanly. For example, in SwiftSyntax, you can create syntax nodes from a string literal. Now, that means that if you create invalid code, it will fail at runtime instead of compile-time. And, it adds this initializer:

init(stringLiteral: String)

which a misguided developer could use to avoid optional-chaining, while actually just (basically) force-unwrapping the value.

With the macro it would be much clearer what was going on and it would allow for compile-time checking.

1 Like

I'm not sure if it would have to do extra work.

It sees that the parameter takes a URL. So it tries to type-check the string literal as a URL. First it checks if URL is ExpressibleByStringLiteral and then it would just check if URL was ExpressibleByMacroStringLiteral. At least to me it doesn't seem like a significant departure from current behavior.

1 Like