O_o I think I have found the most...amazing? cursed? brilliant? something? approach to creating a multi-statement macro that does not require a closure. This fixes a number of small problems around captures.
I have a macro in the form #log(level: OSLogType, message, fields: LogFields)
. I have a logger that will accept a dictionary of "stuff" called fields. I would like to log both to OSLog and to this other logger. I perform various acrobatics that aren't important here to embed the fields into the OSLog message and to handle OSLogMessage-style interpolation (privacy, align, format) for my other logger.
So, given this line:
#log(logger: logger, level: .debug, "Hello, World! x+x = \(x + x)", fields: ["x+x": x + x, "y": "yval", "z": 2, "opt": nil])
My old way, similar to what I expect most folks are doing, looked something like this:
_ = { __macro_local_6loggerVMu_ in
let __macro_local_6fieldsfMu_: () -> LogFields = { ["x+x": x + x, "y": "yval", "z": 2, "opt": nil] }
__macro_local_6loggerVMu_.log(level:.debug,"Hello, World! x+x = \(x + x) -- \(MyLog.expand(fields: __macro_local_6fieldsfMu_))")
MyLog.log(level:.debug,"Hello, World! x+x = \(x + x)",fields:__macro_local_6fieldsfMu_)
}(logger)
Create a closure and immediately run it. I use _ =
at the start to avoid the {
being interpreted as a trailing closure start in some contexts. And I pass the logger
in rather than capture it because of a Swift bug (though honestly, it's probably better to pass it than capture it).
This approach works ok, but it has a problem. If either fields
or message
rely on $0
within their own context, this fails since it creates its own closure.
So, can we do better? How about this?
_ = if let __macro_local_6fieldsfMu_: LogFields = Optional(["x+x": x + x, "y": "yval", "z": 2, "opt": nil]),
logger.log(level:.debug,"Hello, World! x+x = \(x + x) -- \(MyLog.expand(fields: __macro_local_6fieldsfMu_))") == Void(),
MyLog.log(level:.debug,"Hello, World! x+x = \(x + x)",fields:__macro_local_6fieldsfMu_) == ()
{ () } else {() }
Use if let
to create a local binding for the expression, and then test that each statement returns ()
to make a boolean. When there are no fields, I get rid of the local binding.
I don't think this helps @schwa's defer
problem, but it does seem to handle cases that closures struggle with (such as issues of self
capture).
Obviously a real CodeItemMacro would be better, but as I've thought about it, it does open some really weird situations. Imagine emitting }\n func f() {
, closing the current context and starting a new one. I can see places that would probably be exactly what you wanted, but I could see it spiraling out of control, too.