Exposing concurrency info via sourcekit

I've recently been poking around sourcekit and the LSP implementation a bit. My motivation was to see if we could surface some additional concurrency info through the language server & VS Code plugin. Specifically the things I have in mind at the moment are:

  1. "Isolation crossings". These are apply sites where the caller/callee have different isolations.
  2. Inferred isolation for closures. If a closure doesn't have isolation explicitly written in its signature, show the isolation that's inferred.

This information already exists in the typechecked AST, so exposing it via sourcekit seems fairly straightforward. The approach I've taken so far is basically to copy what the collectVariableTypes request does: walk the AST to collect the stuff of interest, set things up for transport, then turn the info into "inlay hints" in the language server. The prototype I have locally ends up rendering something like this:

If it's not clear, the ':shuffle_tracks_button:' icons represent the isolation crossings, and the greyed-out text within the closures are the inferred closure isolations.

I'm wondering if anyone has feedback on the general strategy (copy what collectVariableTypes does), and if there are any "gotchas" to be aware of as I attempt to flesh this out a bit more.

18 Likes

Another useful thing to surface would be the isolation regions that the compiler tracks. In particular, knowing which value is in what region at any given point.

7 Likes

Like inferred types (though I don't know if Swift's LSP exposes those, Kotlin's does), inferred isolation is generally useful, so if you're viewing the methods of a @MainActor class, seeing the implicit isolation would be nice.

3 Likes

This is an interesting idea, but I assume not as straightforward as surfacing isolation info that's already present in the AST. I feel I also have less of a clear idea what a helpful UI for that sort of thing might look like... What comes to mind is the little DSL that was used in the RBI proposal and is similar to what's rendered in the debug logs:

let x = NonSendable()
// Regions: [(x)]
let y = NonSendable()
// Regions: [(x), (y)]
let closure = { useValues(x, y) }
// Regions: [(x, y, closure)]
await transferToMain(closure) // Ok to transfer!
// Regions: [{(x, y, closure), @MainActor}]

But not really sure about the UX there.

It probably wouldn't be too hard to extend the closure logic I hacked together to collect isolation info for other entities. I do wonder how noisy that might look (probably end up with @MainActor @Sendable all over the place), but perhaps that issue could be solved with configuration options in the plugin integration.

2 Likes

I think the implicit info is usually shown while holding a key combo rather than being always on. Though, as an Xcode user, I don't get to use features like that very often, so I'm not sure. :frowning:

I believe this would be neat if we put this behind a flag.

I think this would be a great addition. Especially the closure annotations are something I’ve thought about several times before. And a request like collectVariableTypes seems like the correct implementation strategy to me as well. I can’t think of an gotchas that you need to be aware of from the get-go.

In the end it would also be nice to have a SourceKit-LSP configuration option that allows users to configure whether they want to see the typical inlay type hints, the actor isolations or both.

The actor isolation crossings are an interesting idea as well. A couple random thoughts:

  • I would separate this from the idea above to show the variable actor isolations because it’s a separate feature
  • Instead of using inlay type hints, you could consider using code lenses for the annotation because they provide more of a line-based separation. Just an idea to explore
  • How are you planning to compute where the isolation crossing happens?
2 Likes

Thanks for your feedback Alex (and for your work on the relevant tools here!).

I agree that configuration options here seem like a good idea. To clarify though – by "configuration options" within the context of the LSP implementation, do you mean like parameters to pass through to a request, something more "global" that controls whether the request is issued at all, or something else? I'm still a bit fuzzy on what logic is governed by the LSP vs the IDE plugins that integrate with it.

Agreed.

Yes, this is also something I've been thinking about. Some sort of "line-based" UI seems like it might be nice because it would visually emphasize the "boundary/crossing" concept.

IIUC this info is already computed for (all?) apply expressions[1]. I'm not sure of the exact details regarding how it's derived, but my general impression is that the info is recorded for applys where the caller and callee isolation is either statically known to be or may dynamically be found to be different. I believe these are the places where "dynamic suspension points" may occur, and also where values "crossing" the boundaries must be Sendable/sending.


  1. It's also computed for certain closure configurations and attached to the closure expression, though I think the apply site info is what would make most sense to expose in the IDE. ↩︎

1 Like

I meant adding it to SourceKitLSPOptions, which can then be set by .sourcekit-lsp/config.json, initialization options etc.

Oh, interesting. I’m not familiar with that part of the code base but great to hear if it’s already readily available in the compiler.

1 Like

I've assembled draft PRs for surfacing inferred closure isolation as inlay hints:

  1. Sourcekit PR
  2. LSP PR

It's still quite rough and not as tested as it should be (couldn't figure out how to get the new LSP-side tests passing at all yet), but empirically it does seem to mostly do what I had hoped, and I wanted to get some directional feedback before investing too much more time. I left a number of TODO's within the PRs for which I'd appreciate feedback if/when someone has a chance to look it over.

One thought regarding the sourcekit side changes: To reduce scope, the current implementation reports info only for closures, but as noted above, it may be useful to support reporting isolation for more kinds of entities[1]. To this end, in the current draft, there is the concept of reporting a "kind" that could be used by clients if we were to extend the collection logic in the future, though it's not clear to me how best to structure things here for forward evolution.

Anyway, not sure what's best to discuss in this thread vs the PRs, but happy to respond to feedback wherever.


  1. And it would perhaps also be useful to more generally support reporting "inferred type info" for closures that includes things like implicit @Sendable or other portions of a closure's signature that aren't explicitly written in source. ↩︎

3 Likes