Function body macros

The Introducing names in body macros section would be necessary imo. If I see a function body that uses an unknown type or value that I don't know/didn't write and cannot either cmd+click or cmd+f to find quickly it might as well not exist and fail the type checker, but it doesn't because it's hidden in some string in a macro in some module of a package I added.

It also only enhances the safety of the macro to me as the user and is consistent with all other macro-introduced names.

1 Like

Yes, exactly. I can add an example of this.

It'll show up in the macro expansion. Remember, this is only relevant when the body macro is replacing the existing function body and adding new names that are referenced from the code that the user wrote. In such cases, I suspect that you're likely to have to look into the macro expansion anyway to understand what's going on if there's any sort of non-trivial manipulation of the body.

My concern with the "Introducing names in body macros" feature is that we only really have the one @Tracing example that motivates it. There might be other (better) ways to do this tracing, and introducing new names in this manner might be an anti-pattern. Without more use cases, I think it's premature to extend the proposal in this manner.

Doug

2 Likes

I don't think "it's in the macro expansion" is safe or communicative enough that an entirely new type(s) and value(s) was added to my code - I don't see that section as just an expansion of the proposal but a foundational necessity.

To me this is more like observing the app in Instruments using one of its tools like "Sampler". I can see quite a lot in there including statements, stacks and use counts. I don't have to change the source code in any way to do that, at worst I need to use "debug" vs "release" or vice versa and/or enable some diagnostic option. Internally compiler might do something to help viewing those stacks reliably by including some extra metadata that it would otherwise won't have to include. I believe the same could be done for tracing, including tracing of function results.

Proposal looking great! Thank you, Doug.

Both the preamble and body replacement are very important and I'm happy with their current shape. I agree that preamble is important enough to get it's own kind, and specially because we'd want to compose them.

I suspect the answer is yes but it wasn't called out in the text, would this be allowed:

@TracedBodyReplacement
@RequirePermissionsPreambleMacro("permission.name")
func hello() {}

say, if we have some "add precondition checks" preambles, we could combine it with a SINGLE body replacement macro?


Random naming thought unrelated to proposal itself:

As I was reading this I thought that probably @Span func hello() { span.attributes } will be a nicer spelling... since we put in the name of the macro "it adds a span", so it'll be a little bit less confusing where the span came from maybe :thinking: This may be a good pattern to follow, rather the "something-ed" naming.

But that's unrelated to the feature of these macros, just thinking how we'll be using them.


From prior discussions and general direction of Swift tooling that I remember: since tools like sourcekit-lsp "just expand the macro" it should be possible to have them understand the types properly even without the type hint.

I agree with the assesment that it may be better to wait out adding this explicit naming feature until we see more reasons to do so. The best outcome would be that tools actually do understand the macro well enough and we don't need to do anything here.

Even if we added this feature, then still tools would have to learn to inspect this hint which again is "work" that such tool has to do... So yeah, let's not jump the gun with adding this immediately perhaps.

1 Like

The above statement could apply to macros in general, though. Most macro kinds can add new types and values to your code, which can change how code around them is type-checked. We became comfortable with this because macros are explicitly marked in the source code (whether by # or @), and because the tools make it easy to see into the macro expansion to understand exactly what your code is doing.

To make the case that we must have this expansion in the core proposal, I think you'll need to argue that the problem is different and more dangerous in body macros than for other macros.

I do not see this working in the case of code completion: if you're in the body of a function with @Traced on it, I don't think it makes sense to expand @Traced with the half-written body and then try to figure out where the "cursor" is in the expanded code. But maybe there's some heuristic solution we could try out.

Yes. You get at most one body macro, and any number of preamble macros. Here, both @TracedBodyReplacement and @RequirePermissionsPreambleMacro("permission.name") would see the same body {} that the user wrote, but the final body would be whatever body @TracedBodyReplacement produces with the preamble content of @RequirePermissionsPreambleMacro at the beginning.

Doug

1 Like

Ah I see the problem with the body replacement and completion... that is tricky.

Thanks for explaining the composition as well, that's great.


Here's a hot take we just discussed off-thread I wanted to post here as well:

What if... instead of embracing the "wrapping" style, we make official the APIs to push / pop a task-local value, such that wrapping isn't necessary (at least for task-local setting macros).

The actual reason for "body wrapping" is that the only safe way to ensure task-locals are properly "scoped" is by wrapping them with the usual ServiceContext.withValue(...) { body }... However, internally a task-local is actually managed by a pair of "push" and "pop()" calls. They must be paired and trying to pop a task-local value from the bindings stack when there isn't anything results in a crash; and forgetting to pop "leaks" the value - which is why the only official API is the withValue to bind task-locals today.

However, we could make official these APIs:

enum ServiceContext {
  @TaskLocal
  static var current: ServiceContext?
}

// safe, existing API:
// ServiceContext.$current.withValue(...) { }

// new, unsafe, API we could offer:
ServiceContext.$current.unsafePushValue(...)
defer { ServiceContext.$current.unsafePopValue() }

Mis-managing the pop will result in crashes or incorrect runtime behavior. But since we can say these are unsafe APIs, that's acceptable, and especially if they're intended to be used by macros really, because then tracing can become a preamble macro:

@SpanPreamble
func hello() {
  // let span = Tracing.startSpan(...)
  // ServiceContext.$current.unsafePushValue(span.context)
  // defer { ServiceContext.$current.unsafePopValue() }
}

The only question then remains about introducing names from a preamble macro -- if we could either use the Introducing names in body macros mechanism to have a preamble macro to say it'll introduce span: Span then we're golden.

Could this be an interesting direction?
I'd be happy to handle the proposal and implementation of the missing task-local APIs.

Body replacement macros probably remain useful for various reasons, but the less we can use them the better.

Correct, because all currently existing macros that add names must declare explicitly so, even with names: arbitrary. The syntax and macro author-client interfaces are consistent across all currently existing macros.

I'm arguing the opposite: the problem is the exact same and poses the same dangers as other macros. Its omission here is inconsistent and a gap in the language.

The Swift book states the following and I not only point it out here to help people learning Swift, but also as a contract between the compiler and developers.

... a macro’s declaration provides information about the names of the symbols that the macro generates. When a macro declaration provides a list of names, it’s guaranteed to produce only declarations that use those names, which helps you understand and debug the generated code.

If names: _ wasn't required here, we would most likely have to add: "except for function body macros, where names: arbitrary is (practically) implicitly provided. You cannot explicitly declare your names on function body macros."

The requirements for writing any macro and introducing any names is already defined, not just in some teaching guide, but in the expectations of all macros in the language.


The question I'll now ask is: Why is this macro so different such that the problems and dangers are no longer relevant? Is there some minimum bar (in scope/safety) where macros above that bar require a names definition and below it don't?

I apologize if I'm coming off as pushy to this somewhat small point, but this inconsistency is glaring to me.

Yes, I think it's a good direction. We could lift the blanket ban on preamble macros declaring names, which doesn't have a strong technical basis, and use the existing mechanisms for that, e.g.,

@attached(preamble, names: named(span))
macro SpanPreamble() = ...

The nice thing about preamble macros is that you can expand them as part of code completing in the function body, so you get the full type information from the span declaration that the expanded macro produces. It's very much like any other macro that can introduce names.

Ah, I misunderstood the points you were making. Now it makes more sense...

No, it's not that there's a judgment call here regarding scope/safety, it's that body macros are not just augmenting code by adding new things: they are wholesale replacing the body that the user wrote with one the macro has written. It could be as simple as embedding the code in a closure (the tracing example we've been discussing), or be some major translation akin to result builders. Given that, I don't know how we would describe (or implement) a restriction that detects cases where the body is mostly untouched but an undocumented name was introduced without also rejecting the more interesting use cases.

Doug

3 Likes

Any chance this could be reconsidered? I would hope that applying a body macro directly to a subscript or property would work as expected when defined as a getter shorthand, given how common these constructs are.

For example, I can imagine folks wanting to add body macros to SwiftUI views, and wanting to simply annotate the var body:

+@PrintChanges
 var body: some View {
   VStack {
     …
   }
 }

Rather than having to refactor everything and introduce a layer of indentation that body macros would otherwise remove:

 var body: some View {
-  VStack {
-    …
-  }
+  @PrintChanges get {
+    VStack {
+      …
+    }
+  }
 }

Having the macro at the member level would also make typical use align nicely:

// '@MyMacro' applied consistently on a method vs. computed property
@MyMacro func foo() -> Int { … }
@MyMacro var foo: Int { … }

// vs.
@MyMacro func foo() -> Int { … }
var foo: Int { @MyMacro get { … } }
14 Likes

Just to follow up here... I prototyped allowing preamble macros to introduce declarations (whose names must be covered by the names in the attached macro definition, of course), and it really is quite nice. I'll update the proposal based on this feedback.

Yes, of course we can reconsider! Thanks for providing more examples and rationale; I may have been hasty in banning this.

I can certainly see wanting something like that.

In my initial response to @allevato, I was primary thinking that I wanted to avoid ambiguities between function body macros and accessor macros. However, if we keep this narrowly focused on when you are using the shorthand syntax, it's clear that it is applying to exactly the body of that getter and nothing else.

Doug

7 Likes

Yet another revision based on feedback here. The changes are:

  • Allow preamble macros to introduce names.

  • Introduce @AssumeMainActor example macro for body macros that perform replacement.

  • Switch @Traced example over to be a preamble macro with push/pop operations, so it can nicely introduce span.

  • Allow function body macros to be applied to properties that use the shorthand getter syntax.

    Doug

8 Likes

Sweet, this is shaping up very nicely.

I went ahead and prepared a pitch and implementation of what would be necessary for task locals to benefit from this preamble style macro for tracing (and similar use cases): Unsafe task-local binding by ktoso Β· Pull Request #2222 Β· apple/swift-evolution Β· GitHub

We can pitch this properly later on in a separate thread.

1 Like

Looking forward to implementing python's @lru_cache with BodyMacro

2 Likes

I'm very late to this, but would this proposal also enable the addition of the currently missing "assume some global actor"? Just yesterday I tried to write up a snippet idea but hit the wall that I cannot assume being on my own global actor. MyGlobalActor.shared.assumeIsolated resulted into an error.

While @AssumeMainActor is great, it would be better if we could generalize something like over all global actors.

Writing a generic assumeIsolated method as an extension on GlobalActor requires the ability to abstract over a global actor attribute, e.g. something like

extension GlobalActor {
  public static func assumeIsolated<T>(
    _ operation: @Self () throws -> T
  ) rethrows -> T
}

where the @Self is inferred to be whatever global actor type assumeIsolated is called on. I think this would be useful, but it's not related to function body macros.

5 Likes

Thanks for the clarification. Two more off-topic questions though. What's the reason the assumeIsolated is not yet available for global actors and why did they only made into the newer OSs without being potentially back deployed? I looked up the implementation and besides the built-in check there's just a simple unsafe bit cast from an isolated function type to a non isolated to be able to perform the synchronous call. It really feels odd not being able to use these functions and falling back to wrapping everything into a Task.

These few posts could probably be split out into a discussion of future directions of assumeIsolated -- they're not really macro related.


There's two mixed up questions here:

  • One is about back deploying assumeIsolated,
  • and another about expressing assumeIsolated for any global actor.

The prior is blocked on a bug/limitation in @backDeploy. If it'd get fixed, we'd be able to back deploy assumeIsolated. The right people know about the bug and hope to get to it.

The latter is missing what @hborla just showed in her reply which is a missing typesystem feature. Really the way I'd like to express these in API is to have equivalance between GlobalActor.shared == @GlobalActor and then be able to

static func assumeIsolated(
  instance: some GlobalActor, 
  operation: @isolation(parameter: instance) () -> T) ... 

or something like this, so not specifically about the @Self -- but that would work too, though specifically for just the global actor like that. (caveat: this is just wild thinking out loud, not some specific idea).

This is not really related to macros though, they don't help here until there's the missing typesystem feature for (2).

// added caveat

3 Likes
Off-topic: a status update on assumeIsolated back-deployment

The blocker didn't end up having to do with @backDeployed but I got to the bottom of it so #66473 will be resolved in 5.10.

5 Likes

Thank you again @tshortli for the investigation and fix there :pray: :blush:

1 Like