Freestanding Macro that doesn't return an expression

I was trying to implement Assign if different using freestanding macro and I did succeed but kind of cheating using a closure because the freestanding macro is expected to return and expression.

#assign(b, to: a, if: !=) for example is expanded to:

{
    if a != b {
        a = b
    }
}()

Is this the only way to currently do this? Any plans to lift this restriction with another kind of freestanding macro?

@Douglas_Gregor

This is the only way to do this today. The early pitches for declaration macros had "code item" macros that could do this, and there's a partial implementation of the feature in the compiler behind an experimental feature flag (CodeItemMacros), but nobody has followed through with pitch/proposal/complete implementation. Personally, I haven't seen that many use cases for code-item macros vs. the other kinds, so I haven't looked into them further.

Doug

2 Likes

Hello,

I have a use case for this kind of feature that might be valuable.

It's not possible to possible to wrap os log calls because OSLogMessage cannot be initiliazed from a String variable, it forces us to duplicate log lines to both log into Unified logging system and to Crashlytics for example.

I would like to use a macro to expand the boilerplate code that perform those 2 calls. It seems that only CodeItemMacro can achieve this but I might be wrong...

2 Likes

I created a macro for this exact same use case, and ended up using the same immediately-executed closure strategy described in the original post here, in order to consolidate the two expressions down to a single one.

1 Like

(resurrecting an old thread)

I have a use case along the lines of:

#LogEnterExit()

Getting expanded to:

log("\(#function) Enter")
defer {
    log("\(#function) Exit")
}

Obviously it's more complex than that but that's the basic gist.

1 Like

Would completing the implementation of CodeItemMacros solve the need to add the {/* Generated code here */}() wrapper? Depending on the code before a macro containing the wrapper, it will occasionally get treated as a trailing closure and then fail with:
.../swift-generated-sources/@__REDEACTED_MANGLED_NAME_.swift:1:1 Function type mismatch, declared as '@convention(thin) @substituted <τ_0_0> (@in_guaranteed MySwiftUIView) -> (@out τ_0_0, @error any Error) for <()>' but used as '@convention(thin) (@guaranteed { var String }) -> ()'

My use case is basically the same as schwa's but the wrapper option is causing a few issues, even a compiler crash on Xcode 16.3 beta 1.

One work around is to change the wrapper to ;{/* Generated code here */}(). Note the leading semicolon.

But it would be really nice to not have to use a work around in a work around :)

I tried finding your macro, but wasn't successful, so I wrote my own to do just that.

As it is my first experience working with Swift Syntax I am not sure if my implementation is elegant enough. Most likely not :slight_smile:

I would appreciate any feedback you might have.

@Jeremy_Gold you might find it helpful to.

Here is the macro: SmartLogMacro

1 Like

I've also built this OSLog wrapper macro. The solution I'm using to avoid occasional trailing closure ambiguity is _ = {...}() rather than leading semicolon. I'm not certain which is a better approach.

It does create some small headaches because the closure creates slightly different capture rules than the surrounding code. For example, it breaks cases where people were previously logging $0. We would definitely replace this with a CodeItemMacro if possible.

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.

Since we're already talking about cursed constructs, you could probably use (if) case patterns to avoid the optional wrapping and comparisons:

_ = if case let __macro_local_6fieldsfMu_ = [...] as LogFields,
  case _ = logger.log(...),
  case _ = MyLog.log(...)
{ () } else { () }

I don't think that would actually be a concern because each CodeBlockItemSyntax still has to be a single, complete expression, statement, or declaration. The macro wouldn't be able to return something that could close an existing scope and start a new one, because each element in the returned [CodeBlockItemSyntax] must parse independently.

4 Likes