macro stringify<T>(_: T) -> (T, String)
Although they are not in scope for this proposal, we know that there will be other kinds of macros in the future. What marks #stringify(_:) as an expression macro, as opposed to some other kind of macro that also has arguments and a return value?
macro stringify<T>(_: T) -> (T, String) = ExampleMacros.StringifyMacro
Oooh, I like this = syntax. Nice and concise.
return MacroResult("(\(argument), #\"\(argument.description)\"#)")
I'd like to see stronger typing of the code fragments a macro works with. In particular, I'm convinced that treating interpolation of a plain String into a SwiftSyntax node as inserting raw source code is a mistake. It's the same kind of hazard we see in SQL injectionāpeople don't think about the edge cases, or don't handle them robustly enough to work correctly in all situations. For instance, this line in StringifyMacro would break if the argument itself used a raw string literal.
What we ought to do is rename the SyntaxStringInterpolation.appendInterpolation(_: some CustomStringConvertible) method to something like appendInterpolation(raw:), and add a new appendInterpolation(_:) overload that automatically chooses a robust way to escape and quote the text. For that matter, we should have overloads for inserting integer literals, boolean literals, and array/dictionary literals of these types in the same way. Then StringifyMacro can write:
return MacroResult("(\(argument), \(argument.description))")
To automatically get appropriate escaping, or:
return MacroResult("(\(argument), #\"\(raw: argument.description)\"#)")
If for some reason it really wants to control the quoting and escaping itself.
(If you hate having quote marks magically appear around interpolated strings, you could instead add a label so it's more like "(\(argument), \(literal: argument.description))". The important point is that an unlabeled appendInterpolation(_:) must not treat a random String that could have come from anywhere as though it were valid source code. You should have to do something to acknowledge that the string needs to contain valid source.)
(Aside: SwiftSyntax does not currently evolve through Language Workgroup evolution proposals, does it? Should it? I suppose it's not as vital if you can use whatever SwiftSyntax version you want, but it seems strange that the design of the types used in macros isn't really governed by evolution.)
The signature of a macro is either function-like ((T) -> (T, String) ) or value-like (: Int ), depending on the form of the macro-signature .
This sentence doesn't seem to be quite correctāa function-like signature has a parameter clause, not a function type, so (T) should be (_: T).
public mutating func createUniqueLocalName() -> String
Should this return a TokenSyntax with an identifier or something?
macro column<T: ExpressibleByIntegerLiteral>: T = BuiltinMacros.ColumnMacro
Is it a little strange that both function-style and constant-style macros start with the same keywords and the difference between them only becomes clear when we see whether there's a : or an argument list? Perhaps we should use macro var and macro func to convey this difference more quickly:
macro func stringify<T>(_: T) -> (T, String)
macro var column<T: ExpressibleByIntegerLiteral>: T = BuiltinMacros.ColumnMacro
The error question is really interesting. I'm sorely tempted to suggest that we should emit warnings through a function on the context and emit errors by throwing them from the apply(...) method. That would limit us to one error per macro, but I think it might be okay. It would mean we don't need a MacroResult, so the macro doesn't have to make up a fake expansion when there's a failure, and it would mean that nested type-checking failures could throw a special "macro aborted, but we have already diagnosed the failure so you don't need to do it again" error that would simply pass through the macro without explicit error handling beyond a try.
extension MacroEvaluationContext {
mutating func type(of expression: ExprSyntax) throws -> Type
mutating func declaration(referencedBy expression: ExprSyntax) throws -> Decl
mutating func warn(_: Diagnostic)
mutating func remark(_: Diagnostic)
}
public enum LatexifyMacro: ExpressionMacro {
static func apply(
_ macro: MacroExpansionExprSyntax, in context: inout MacroEvaluationContext
) throws -> ExprSyntax {
"\(try convert(macro.argumentList.first!.expression, in: &context))"
}
static func convert(_ syntax: Syntax, in context: inout MacroEvaluationContext) throws -> String {
switch syntax.as(SyntaxEnum.self) {
case .identifierExpr(let expr):
switch try context.type(of: expr) {
...
case let unknownType:
throw Diagnostic(location: expr, message: "can't convert variable of unknown type \(unknownType) to LaTeX")
}
...
}
}
}
EDIT:
macro-signature -> parameter-clause '->' type
Should this support omitting the return type for a Void-returning macro? Should it support throws, requiring a try before the macro? What about rethrows? Should it support async, requiring an await before the macro?