Is there a mechanism to output the fully-macro-expanded source from a compiler invocation?

i was recently looking into some confusing behavior involving macro expansions, and was wondering what the current capabilities are for viewing the 'fully macro expanded source code', ideally from some direct compiler emission rather than something that requires an IDE or other tools. so far i've found that (at least for the 'native' macros), if you are building locally, you can enable debug output that dumps the compiler plugin messages (here). while helpful for seeing what's going on, it's a bit cumbersome to translate all the messages into what the logical 'expanded source code' looks like. there is also the -print-ast option, which does include macro output, but loses some info (e.g. trivia).

does something like this already exist? if not, does such a feature seem like a potentially useful thing to support? if so, also curious if anyone has thoughts on how it might be implemented...


edit: seems there is a -dump-macro-expansions option, which dumps out the macro expansions (as they occur?), so perhaps that's good enough.

1 Like

Since each macro expansion goes in its own source file this kinda "doesn't actually exist" (and plenty of semantic analysis checks fall into that void between source files, leading to assertions at codegen time, so even if you had a mode which output a single source file with all the macro expansions, it wouldn't necessarily behave the same).

There's also special cases like initializers remaining on computed properties to provide type annotations, but otherwise being ignored, which aren't "valid swift".

interesting, do you happen to have an example of such a check that you can think of?

also curious to better understand this case specifically โ€“ what do you mean by 'initializers remaining on computed properties'? i guess i was under the impression that macro expansion always resulted in 'valid swift', but it sounds like you are aware of some counter examples?

This one's easy โ€” make a macro with an accessor role, and apply it to a property with an initializer:

@ComputedPropertyReturning3 var myProperty = UInt8(27)

"expands to" something like

var myProperty = UInt8(27) {
    3
}

Which is then treated as

var myProperty: UInt8 {
    3
}

That is, the initializer is in some sense "still present", but only used to infer the type of the property.

2 Likes

An early example of this (since fixed) was that it was impossible to witness a static protocol requirement from a macro โ€” the same code would've worked fine if written out inline. There were a lot of issues about it, here's mine: Type generated by peer macro can't conform to Equatable ยท Issue #68683 ยท swiftlang/swift ยท GitHub

This is hardly the only example though โ€” I come across it semi-regularly that invalid code generated by a macro passes sema only to crash the compiler at codegen time. Normally I just fix the macro and move on, so I don't have a newer example to hand sorry :/

I also ran into many such issues in the past. See this and this, for example. I was left with the impression that macro code was handled differently, but never really understood why.

2 Likes