I had a little time to experiment with this. Implemented a custom URL literal.
The good news is that it works - I can implement compile-time checking of URLs to find obvious and non-obvious mistakes. Since I'm writing a package rather than a system library, the result can be a non-optional URL (which I know lots of people will be pleased to hear).
The diagnostics features are very useful. I can warn about URLs which parse but are not completely well-formed as written, and provide a fix-it with the repaired URL string. By ensuring that URLs in source code are well-formed, we can guard against certain kinds of spoofing attacks (e.g. from bidirectional text), and ensure that the URLs that people write are not ambiguous, so they can be reliably audited by others.
I added a special initialiser (for the macro's use only), ensuring that these also behave more like, say, an integer or string literal, and don't need parsing at runtime.
func urlForBand(_ bandName: String) -> WebURL {
// Does not return an optional.
// Does not re-parse the string each time.
var url = #url("https://api.example.com/music/bands/")
url.pathComponents += bandName
return url
}
Unfortunately it doesn't get statically allocated, but there's now much, much less of a leap so it's finally a realistic proposition for the optimiser.
Overall, I'm very happy with that.
The less-good news is that, to be honest, building that macro was incredibly frustrating. I have plenty of other ideas I'd like to try, but it was quite draining to duck-tape things together. IMO, we should keep iterating on this implementation, and continue to fill out the rest of the macro story before we put things through review and make it official.
-
SwiftPM integration is badly needed. Currently, you need to cobble together an Xcode project with your various modules, build settings including paths with substitutions, etc. All of the various pieces need to be very carefully aligned for this to work, otherwise for anybody using a macro, their code just fails to build. I worry that macros are going to get a reputation for fragility, unreliability, and build failures, unless we really nail this part of the design.
You could say that it's at an early stage - but on the other hand, if it's going through review now, is it really that early? I don't think I'm being unfair by expressing concern that there isn't a single public pitch/sketch/prototype for the build-system side of things at this stage of the process.
IMO, no macro features should pass review until we have a concrete design for this part of the puzzle. I think it makes a lot of sense to start with the concept of packages declaring targets with compile-time code, and then to build macros on top of that support.
It's just really difficult to experiment with macros in realistic situations right now, so it's really difficult to critically evaluate.
-
I'd like to confirm - will the toolchain ship with everything needed to build and run macros? Since this is a language feature, I feel I shouldn't need to download an additional package to get started using it. What if GitHub is down, or blocked in my country?
IMO, the toolchain should include a full copy of swift-syntax, including any interface files or whatever else is needed for a complete experience (including syntax highlighting and documentation). I should be able to write a Swift project entirely offline and I should be able to define and use macros in it.
-
I very much agree with what others have said about the swift-syntax library: it is very difficult to use. For simple modifications, it feels overwhelming. I've worked with spiritually similar APIs before - HTML DOM APIs spring to mind - but using this one, from Swift, felt very awkward. Here are a couple of issues that I encountered:
i) The documentation is very bare when you find any, and because the API makes heavy use of overloads, when attempting to feel your way through the API you'll often encounter situations like this:
ii) To replace the contents of a string literal, I eventually (after a looong time) stumbled on this incantation. I think this is the right way to do it?:
stringLiteral.withSegments([.stringSegment(StringSegment(content: url.description))])
iii) Things like SimpleDiagnosticMessage
need to be defined manually. Why is it not included? Also, just emitting a relatively simple diagnostic takes a gigantic amount of code with lots of nesting.
iv) I need to keep wrapping things using Syntax(someNode)
-- why does the library not use features such as protocols and generics to express these in terms of a common supertype?
Overall, it feels like it needs a lot of work before it's ready to go in the hands of every Swift developer.
-
Foundation APIs didn't work.
<unknown>:0: warning: compiler plugin not loaded: /Users/karl/<...>/libWebURLMacros.dylib; loader error: dlopen(/Users/karl/<...>/libWebURLMacros.dylib, 0x0005): Symbol not found: (_$sSy10FoundationE37precomposedStringWithCanonicalMappingSSvg)
Referenced from: '/Users/karl/<...>/libWebURLMacros.dylib'
Expected in: '/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation'
-
Compiler performance felt really poor and the macro implementation didn't rebuild when I made changes to it. I had to make changes to the user of the macro to prompt a rebuild. I probably messed something up with the configuration, but it's hard to know (see 1).
-
If we're going with the idea that every macro should depend on its own version of swift-syntax, I worry that will also cause issues for the package ecosystem.
As more packages use macros, I could easily end up with dozens of copies of swift-syntax, all at different versions, which all need to be downloaded and built independently. How long will it take to clean-build all of those?
-
I'm not a fan of the @expression public macro
syntax:
@expression
macro url(_: String) -> WebURL = #externalMacro(module: "WebURLMacros", type: "URLMacro")
Typically, I interpret attributes as describe the thing they are applied to (@inlinable
, @frozen
, @escaping
, @propertyWrapper
). This thing is a declaration, not an expression.
I see that it has now been replaced with @freestanding(expression)
. I'm not sure that's any better, honestly.
In conclusion, I think macros are an exciting and powerful feature which have the potential to be very popular, but when I consider the lack of design for the critical package manager/build system side of things, and the complexity of the swift-syntax library, I also see a lot of potential for frustration.
I feel this feature is somewhere along the right path, but it isn't quite there yet, and we shouldn't rush it.