Macro symbol visibility to other macros

While working on this pitch for a new .taskLocal test trait, we butted heads with a limitation of Swift macros. Because @TaskLocal and @Test are both macros, something like this cannot work:

@TaskLocal var isEnabled = false
@Test(.taskLocal($isEnabled, true))  // 🛑 error: Cannot find '$isEnabled' in scope
func test() {}

The $isEnabled macro generated symbol cannot be seen from the @Test macro, even though @TaskLocal does describe part of the symbol it generates.

This also affects the #Preview macro in Xcode:

@TaskLocal var isEnabled = false
#Preview {
  let _ = $isEnabled  // 🛑 error: Cannot find '$isEnabled' in scope
  EmptyView()
}

Again, the $isEnabled macro generated symbol cannot be seen from within the #Preview macro.

Luckily in the case of the .taskLocal trait being proposed one does not typically run into this because usually the task local will be defined in a separate module, and in that case things work just fine. But there are still use cases for having the task local defined in a test target (we have some uses of this in our libraries), and the #Preview deficiency is more annoying to work around.

This topic has been brought up before (by Stephen here), but without any definitive clarification. While at first glance it may seem reasonable that macros can't see macro generated code, it 1.) doesn't seem to be consistently enforced (see the example about observable), and 2.) doesn't seem necessary since in the end the code will be compiled and if a non-existent symbol is used it will cause a compiler error.

Is it possible to get a little clarification on whether or not this limitation could be lifted (or partially lifted) from anyone who has worked on the macro infrastructure in Swift?

5 Likes