diff --git a/include/swift/Sema/IDETypeChecking.h b/include/swift/Sema/IDETypeChecking.h index 5a08ff3da7b5c..db9e4af7938bb 100644 --- a/include/swift/Sema/IDETypeChecking.h +++ b/include/swift/Sema/IDETypeChecking.h @@ -211,6 +211,37 @@ namespace swift { std::vector &scratch, bool FullyQualified, bool CanonicalType, llvm::raw_ostream &OS); + /// An inferred actor isolation suitable for surfacing in an IDE inlay hint. + /// Reported as an offset+length anchor (currently the closure's `{`), with + /// the pretty-printed isolation and a kind string written into a shared + /// string buffer. + /// Inferred actor isolation info for + struct InferredIsolationInfo { + /// Offset of the entity to which the isolation corresponds. + uint32_t Offset; + + /// Length of the entity to which the isolation corresponds. + uint32_t Length; + + /// Offsets into the shared string buffer for the source-style isolation + /// (e.g. "@MainActor", "nonisolated", "@SomeGlobalActor"). + uint32_t IsolationOffset; + uint32_t IsolationLength; + + /// Offsets into the shared string buffer for the kind of entity this + /// isolation is attached to. Currently always "closure". + uint32_t KindOffset; + uint32_t KindLength; + }; + + /// Collect *inferred* actor isolation for every explicit \c ClosureExpr in + /// \c SF. Closures with their isolation written explicitly in the signature + /// are skipped. Pretty-printed isolation strings are written to \c OS. + void + collectInferredIsolations(SourceFile &SF, SourceRange Range, + std::vector &IsolationInfos, + llvm::raw_ostream &OS); + /// Resolve a list of mangled names to accessible protocol decls from /// the decl context. ProtocolDecl *resolveProtocolName(DeclContext *dc, StringRef Name); diff --git a/lib/IDE/IDETypeChecking.cpp b/lib/IDE/IDETypeChecking.cpp index e6c184ee99274..30326627fd271 100644 --- a/lib/IDE/IDETypeChecking.cpp +++ b/lib/IDE/IDETypeChecking.cpp @@ -13,6 +13,8 @@ #include "swift/Sema/IDETypeChecking.h" #include "ReadyForTypeCheckingCallback.h" #include "swift/AST/ASTContext.h" +#include "swift/AST/ASTWalker.h" +#include "swift/AST/ActorIsolation.h" #include "swift/AST/ASTDemangler.h" #include "swift/AST/ASTPrinter.h" #include "swift/AST/Attr.h" @@ -1032,6 +1034,209 @@ swift::getShorthandShadows(LabeledConditionalStmt *CondStmt, DeclContext *DC) { return Result; } +//===--------------------------------------------------------------------------===// +// Isolation collection +//===--------------------------------------------------------------------------===// + +/// Walks a SourceFile and records actor isolation info. Currently only supports +/// explicit ClosureExprs. Closures whose isolation is written in the signature +/// are skipped. +class InferredIsolationCollector : public SourceEntityWalker { + SourceManager &SM; + unsigned BufferId; + + /// The range in which isolation info is to be collected. An invalid range + /// means the whole file. + SourceRange TotalRange; + + /// The output vector for InferredIsolationInfo emitted during traversal. + std::vector &Results; + + /// Buffer in which all printed isolation strings are stored, null-terminated + /// and deduplicated. + llvm::raw_ostream &OS; + + /// Map from a printed string to its offset in \c OS. Used for deduplicating + /// both isolation strings and kind labels. + llvm::StringMap StringOffsets; + + /// Returns the (offset, length) of \p S within \c OS, printing it first if + /// it isn't already present. + std::pair getBufferRangeForString(StringRef S) { + auto It = StringOffsets.find(S); + if (It == StringOffsets.end()) { + StringOffsets[S] = OS.tell(); + OS << S << '\0'; + } + return {StringOffsets[S], S.size()}; + } + + /// Whether the given range overlaps the total range in which we collect + /// isolation info. + bool overlapsTotalRange(SourceRange Range) { + return TotalRange.isInvalid() || Range.overlaps(TotalRange); + } + + /// Render an isolation in the spelling you'd write in source, suitable for + /// an inline IDE badge. Examples: "@MainActor", "@SomeGlobalActor", + /// "nonisolated", "nonisolated(unsafe)", "nonisolated(nonsending)", + /// "actor-isolated", "@isolated(any)". + // TODO: what should be responsible for this formatting? + static void printSourceStyle(const ActorIsolation &isolation, + llvm::raw_ostream &Out) { + switch (isolation.getKind()) { + case ActorIsolation::Unspecified: + // TODO: how to handle 'unspecified'? + // We currently skip it during the walk so should not hit it here, but + // what should we return? Empty string? Pass it through and let clients + // decide? + Out << ""; + return; + case ActorIsolation::ActorInstance: + Out << "actor-isolated"; + return; + case ActorIsolation::Nonisolated: + Out << "nonisolated"; + return; + case ActorIsolation::NonisolatedConcurrent: + Out << "@concurrent"; + return; + case ActorIsolation::NonisolatedNonsending: + Out << "nonisolated(nonsending)"; + return; + case ActorIsolation::NonisolatedUnsafe: + Out << "nonisolated(unsafe)"; + return; + case ActorIsolation::GlobalActor: { + if (isolation.isMainActor()) { + Out << "@MainActor"; + } else { + Out << "@" << isolation.getGlobalActor().getString(); + } + return; + case ActorIsolation::Erased: + Out << "@isolated(any)"; + return; + } + } + llvm_unreachable("covered switch"); + } + + std::pair printIsolation(const ActorIsolation &iso) { + SmallString<64> Buf; + { + llvm::raw_svector_ostream Out(Buf); + printSourceStyle(iso, Out); + } + return getBufferRangeForString(Buf.str()); + } + + /// Record one inferred-isolation entry. \p Anchor must be valid and in this + /// source file's buffer. + void record(SourceLoc Anchor, unsigned Length, + const ActorIsolation &Isolation, StringRef Kind) { + if (Anchor.isInvalid()) + return; + if (SM.findBufferContainingLoc(Anchor) != BufferId) + return; + + unsigned offset = SM.getLocOffsetInBuffer(Anchor, BufferId); + auto isolationBufferRange = printIsolation(Isolation); + auto kindBufferRange = getBufferRangeForString(Kind); + + InferredIsolationInfo Info; + Info.Offset = offset; + Info.Length = Length; + Info.IsolationOffset = isolationBufferRange.first; + Info.IsolationLength = isolationBufferRange.second; + Info.KindOffset = kindBufferRange.first; + Info.KindLength = kindBufferRange.second; + // TODO: the var type collector analog constructs this inline... + // is one way better than another? + Results.push_back(Info); + } + +public: + InferredIsolationCollector(SourceFile &SF, SourceRange Range, + std::vector &Results, + llvm::raw_ostream &OS) + : SM(SF.getASTContext().SourceMgr), BufferId(SF.getBufferID()), + TotalRange(Range), Results(Results), OS(OS) {} + + bool walkToExprPre(Expr *E) override { + // Skip the subtree if it's outside the requested range. + if (!overlapsTotalRange(E->getSourceRange())) + return false; + + auto *Closure = dyn_cast(E); + if (!Closure) + return true; + + if (Closure->isImplicit()) + return true; + + auto Isolation = Closure->getActorIsolation(); + if (Isolation.isUnspecified()) + return true; + + // If the closure signature has an explicit isolation attribute in + // its signature, exclude it. + // TODO: what is the best way to robustly detect this case? + // Should we pass it through regardless of whether it's "implicit"? + if (Closure->getInLoc().isValid()) { + const auto &Attrs = Closure->getAttrs(); + + // Found an explicit @concurrent attribute, so skip this closure. + if (Attrs.hasAttribute()) + return true; + + // Check for an explicit global actor attribute. + if (auto result = evaluateOrDefault(Closure->getASTContext().evaluator, + GlobalActorAttributeRequest{Closure}, + std::nullopt)) { + // If we found a nominal, then this closure has an explicit global actor + // annotation, so we can skip it. + if (result->second) + return true; + } + } + + // TODO: should we report the full source range here or infer an "anchor"? + // Clients would need to figure out where the right placement is for + // closures. + const auto closureCharSourceRange = + Lexer::getCharSourceRangeFromSourceRange(SM, Closure->getSourceRange()); + + record(closureCharSourceRange.getStart(), + closureCharSourceRange.getByteLength(), Isolation, "closure"); + return true; + } + + bool walkToDeclPre(Decl *D, CharSourceRange DeclNameRange) override { + // Skip this node and subtree if outside the range. + return overlapsTotalRange(D->getSourceRange()); + } + + bool walkToStmtPre(Stmt *S) override { + // Skip this node and subtree if outside the range. + return overlapsTotalRange(S->getSourceRange()); + } + + bool walkToPatternPre(Pattern *P) override { + // Skip this node and subtree if outside the range. + return overlapsTotalRange(P->getSourceRange()); + } +}; + +void swift::collectInferredIsolations( + SourceFile &SF, SourceRange Range, + std::vector &IsolationInfos, llvm::raw_ostream &OS) { + InferredIsolationCollector Walker(SF, Range, IsolationInfos, OS); + Walker.walk(SF); +} + +//===--------------------------------------------------------------------------===// + void ReadyForTypeCheckingCallback::doneParsing(SourceFile *SrcFile) { // Import resolution will have already been done by IDEInspectionInstance, // we need to bind extensions here though since IDEInspectionSecondPassRequest diff --git a/test/IDE/inferred_isolation.swift b/test/IDE/inferred_isolation.swift new file mode 100644 index 0000000000000..b77cfb6e642fd --- /dev/null +++ b/test/IDE/inferred_isolation.swift @@ -0,0 +1,56 @@ +// RUN: %target-swift-ide-test -print-inferred-isolation -source-filename %s | %FileCheck %s + +@globalActor +actor MyActor { + static let shared = MyActor() +} + +@MainActor +final class C { + var n = 0 + + func work() async { + // Closure that inherits @MainActor from the enclosing context. + // CHECK: let inheritedMain = { + let inheritedMain = { + self.n += 1 + } + inheritedMain() + + // Detached closure: nonisolated regardless of enclosing context. + // CHECK: Task.detached { + Task.detached { + print("detached") + } + + // Task closure inherits the actor context. + // CHECK: Task { + Task { + self.n += 1 + } + } +} + +func freeFunc() { + // Closure outside any isolated context -- nonisolated. + // CHECK: let f = { 1 } + let f = { 1 } + _ = f() +} + +// Closures whose isolation is *written* in the signature must not be tagged, +// since the information is already on screen. + +func explicitMain() { + // CHECK: let g = { @MainActor in 2 } + // CHECK-NOT: ]*}}>{ @MainActor + let g = { @MainActor in 2 } + _ = g +} + +func explicitCustom() { + // CHECK: let h = { @MyActor in 3 } + // CHECK-NOT: ]*}}>{ @MyActor + let h = { @MyActor in 3 } + _ = h +} diff --git a/test/SourceKit/InferredIsolation/basic.swift b/test/SourceKit/InferredIsolation/basic.swift new file mode 100644 index 0000000000000..fe19b41921a2b --- /dev/null +++ b/test/SourceKit/InferredIsolation/basic.swift @@ -0,0 +1,32 @@ +@MainActor +final class C { + var n = 0 + + func work() async { + let inheritedMain = { + self.n += 1 + } + inheritedMain() + + Task.detached { + print("detached") + } + } +} + +// RUN: %sourcekitd-test -req=collect-inferred-isolation %s -- %s | %FileCheck %s + +// CHECK: key.results: [ +// CHECK-NEXT: { +// CHECK-NEXT: key.kind: "closure", +// CHECK-NEXT: key.offset: {{[0-9]+}}, +// CHECK-NEXT: key.length: {{[0-9]+}}, +// CHECK-NEXT: key.actor_isolation: "@MainActor" +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: key.kind: "closure", +// CHECK-NEXT: key.offset: {{[0-9]+}}, +// CHECK-NEXT: key.length: {{[0-9]+}}, +// CHECK-NEXT: key.actor_isolation: "@concurrent" +// CHECK-NEXT: } +// CHECK-NEXT: ] diff --git a/test/SourceKit/InferredIsolation/range.swift b/test/SourceKit/InferredIsolation/range.swift new file mode 100644 index 0000000000000..f88efa3103d19 --- /dev/null +++ b/test/SourceKit/InferredIsolation/range.swift @@ -0,0 +1,43 @@ +@MainActor +final class C { + var n = 0 + + func work() async { + let inheritedMain = { + self.n += 1 + } + _ = inheritedMain + + Task.detached { + print("detached") + } + } +} + +// Range covering only the first closure. +// We should see only the @MainActor entry. +// +// RUN: %sourcekitd-test -req=collect-inferred-isolation -pos=6:1 -length=60 %s -- %s | %FileCheck %s --check-prefix=INHERITED + +// INHERITED: key.results: [ +// INHERITED-NEXT: { +// INHERITED-NEXT: key.kind: "closure", +// INHERITED-NEXT: key.offset: {{[0-9]+}}, +// INHERITED-NEXT: key.length: {{[0-9]+}}, +// INHERITED-NEXT: key.actor_isolation: "@MainActor" +// INHERITED-NEXT: } +// INHERITED-NEXT: ] + +// Range covering only the second closure. +// see only the nonisolated entry; the first closure must be filtered out. +// +// RUN: %sourcekitd-test -req=collect-inferred-isolation -pos=9:1 -length=100 %s -- %s | %FileCheck %s --check-prefix=DETACHED + +// DETACHED: key.results: [ +// DETACHED-NEXT: { +// DETACHED-NEXT: key.kind: "closure", +// DETACHED-NEXT: key.offset: {{[0-9]+}}, +// DETACHED-NEXT: key.length: {{[0-9]+}}, +// DETACHED-NEXT: key.actor_isolation: "@concurrent" +// DETACHED-NEXT: } +// DETACHED-NEXT: ] diff --git a/tools/SourceKit/include/SourceKit/Core/LangSupport.h b/tools/SourceKit/include/SourceKit/Core/LangSupport.h index 639d9a3362c51..f9059f519a288 100644 --- a/tools/SourceKit/include/SourceKit/Core/LangSupport.h +++ b/tools/SourceKit/include/SourceKit/Core/LangSupport.h @@ -145,6 +145,19 @@ struct ExpressionTypesInFile { StringRef TypeBuffer; }; +struct InferredIsolation { + unsigned Offset; + unsigned Length; + /// Offsets into `InferredIsolationsInFile.StringBuffer`. + unsigned IsolationOffset; + unsigned KindOffset; +}; + +struct InferredIsolationsInFile { + std::vector Results; + StringRef StringBuffer; +}; + struct VariableType { /// The variable identifier's offset in the file. unsigned VarOffset; @@ -1264,6 +1277,16 @@ class LangSupport { std::function &)> Receiver) = 0; + /// Collects inferred actor isolation for every closure and declaration in + /// the file. Skips declarations whose isolation is written explicitly. + virtual void collectInferredIsolations( + StringRef PrimaryFilePath, StringRef InputBufferName, + ArrayRef Args, std::optional Offset, + std::optional Length, bool CancelOnSubsequentRequest, + SourceKitCancellationToken CancellationToken, + std::function &)> + Receiver) = 0; + /// Collects variable types for a range defined by `Offset` and `Length` in /// the source file. If `Offset` or `Length` are empty, variable types for /// the entire document are collected. diff --git a/tools/SourceKit/lib/SwiftLang/SwiftLangSupport.h b/tools/SourceKit/lib/SwiftLang/SwiftLangSupport.h index 6eb872aa31760..b1a8d9e891e31 100644 --- a/tools/SourceKit/lib/SwiftLang/SwiftLangSupport.h +++ b/tools/SourceKit/lib/SwiftLang/SwiftLangSupport.h @@ -720,6 +720,14 @@ class SwiftLangSupport : public LangSupport { std::function &)> Receiver) override; + void collectInferredIsolations( + StringRef PrimaryFilePath, StringRef InputBufferName, + ArrayRef Args, std::optional Offset, + std::optional Length, bool CancelOnSubsequentRequest, + SourceKitCancellationToken CancellationToken, + std::function &)> + Receiver) override; + void collectVariableTypes( StringRef PrimaryFilePath, StringRef InputBufferName, ArrayRef Args, std::optional Offset, diff --git a/tools/SourceKit/lib/SwiftLang/SwiftSourceDocInfo.cpp b/tools/SourceKit/lib/SwiftLang/SwiftSourceDocInfo.cpp index fa2edc85b68e3..5459bf7d66955 100644 --- a/tools/SourceKit/lib/SwiftLang/SwiftSourceDocInfo.cpp +++ b/tools/SourceKit/lib/SwiftLang/SwiftSourceDocInfo.cpp @@ -2963,6 +2963,92 @@ void SwiftLangSupport::collectExpressionTypes( llvm::vfs::createPhysicalFileSystem()); } +void SwiftLangSupport::collectInferredIsolations( + StringRef PrimaryFilePath, StringRef InputBufferName, + ArrayRef Args, std::optional Offset, + std::optional Length, bool CancelOnSubsequentRequest, + SourceKitCancellationToken CancellationToken, + std::function &)> + Receiver) { + std::string Error; + SwiftInvocationRef Invok = + ASTMgr->getTypecheckInvocation(Args, PrimaryFilePath, Error); + if (!Invok) { + LOG_WARN_FUNC("failed to create an ASTInvocation: " << Error); + Receiver(RequestResult::fromError(Error)); + return; + } + assert(Invok); + class InferredIsolationsCollector : public SwiftASTConsumer { + std::function &)> + Receiver; + std::string InputFile; + std::optional Offset; + std::optional Length; + + public: + InferredIsolationsCollector( + std::function &)> + Receiver, + StringRef InputFile, std::optional Offset, + std::optional Length) + : Receiver(std::move(Receiver)), InputFile(InputFile.str()), + Offset(Offset), Length(Length) {} + + void handlePrimaryAST(ASTUnitRef AstUnit) override { + SourceFile *SF = + retrieveInputFile(InputFile, AstUnit->getCompilerInstance()); + if (!SF) { + Receiver(RequestResult::fromError( + "Unable to find input file")); + return; + } + + // Construct the range to be searched. If offset/length are unset, the + // (default) range will be used, which corresponds to the entire document. + SourceRange Range; + if (Offset.has_value() && Length.has_value()) { + auto &SM = AstUnit->getCompilerInstance().getSourceMgr(); + unsigned BufferID = SF->getBufferID(); + SourceLoc Start = Lexer::getLocForStartOfToken(SM, BufferID, *Offset); + SourceLoc End = + Lexer::getLocForStartOfToken(SM, BufferID, *Offset + *Length); + Range = SourceRange(Start, End); + } + + std::vector Infos; + std::string InfosBuffer; + llvm::raw_string_ostream OS(InfosBuffer); + InferredIsolationsInFile Result; + + swift::collectInferredIsolations(*SF, Range, Infos, OS); + + for (auto Info : Infos) { + Result.Results.push_back( + {Info.Offset, Info.Length, Info.IsolationOffset, Info.KindOffset}); + } + Result.StringBuffer = OS.str(); + Receiver(RequestResult::fromResult(Result)); + } + + void cancelled() override { + Receiver(RequestResult::cancelled()); + } + + void failed(StringRef Error) override { + Receiver(RequestResult::fromError(Error)); + } + }; + + auto Collector = std::make_shared( + Receiver, InputBufferName, Offset, Length); + static const char OncePerASTToken = 0; + const void *Once = CancelOnSubsequentRequest ? &OncePerASTToken : nullptr; + getASTManager()->processASTAsync(Invok, std::move(Collector), Once, + CancellationToken, + llvm::vfs::createPhysicalFileSystem()); +} + void SwiftLangSupport::collectVariableTypes( StringRef PrimaryFilePath, StringRef InputBufferName, ArrayRef Args, std::optional Offset, diff --git a/tools/SourceKit/tools/sourcekitd-test/TestOptions.cpp b/tools/SourceKit/tools/sourcekitd-test/TestOptions.cpp index 019659e7c916c..9821313ee5e7c 100644 --- a/tools/SourceKit/tools/sourcekitd-test/TestOptions.cpp +++ b/tools/SourceKit/tools/sourcekitd-test/TestOptions.cpp @@ -147,6 +147,8 @@ bool TestOptions::parseArgs(llvm::ArrayRef Args) { .Case("track-compiles", SourceKitRequest::EnableCompileNotifications) .Case("collect-type", SourceKitRequest::CollectExpressionType) .Case("collect-var-type", SourceKitRequest::CollectVariableType) + .Case("collect-inferred-isolation", + SourceKitRequest::CollectInferredIsolation) .Case("global-config", SourceKitRequest::GlobalConfiguration) .Case("dependency-updated", SourceKitRequest::DependencyUpdated) .Case("diags", SourceKitRequest::Diagnostics) diff --git a/tools/SourceKit/tools/sourcekitd-test/TestOptions.h b/tools/SourceKit/tools/sourcekitd-test/TestOptions.h index 33d2a72c8e253..18c56f8efde6a 100644 --- a/tools/SourceKit/tools/sourcekitd-test/TestOptions.h +++ b/tools/SourceKit/tools/sourcekitd-test/TestOptions.h @@ -65,6 +65,7 @@ enum class SourceKitRequest { EnableCompileNotifications, CollectExpressionType, CollectVariableType, + CollectInferredIsolation, GlobalConfiguration, DependencyUpdated, Diagnostics, diff --git a/tools/SourceKit/tools/sourcekitd-test/sourcekitd-test.cpp b/tools/SourceKit/tools/sourcekitd-test/sourcekitd-test.cpp index c270493a15356..473e9d01130e4 100644 --- a/tools/SourceKit/tools/sourcekitd-test/sourcekitd-test.cpp +++ b/tools/SourceKit/tools/sourcekitd-test/sourcekitd-test.cpp @@ -905,6 +905,17 @@ static int handleTestInvocation(TestOptions Opts, TestOptions &InitOpts) { break; } + case SourceKitRequest::CollectInferredIsolation: { + sourcekitd_request_dictionary_set_uid(Req, KeyRequest, + RequestCollectInferredIsolation); + if (Opts.Length) { + sourcekitd_request_dictionary_set_int64(Req, KeyOffset, ByteOffset); + sourcekitd_request_dictionary_set_int64(Req, KeyLength, Opts.Length); + } + addRequestOptionsDirect(Req, Opts); + break; + } + #define SEMANTIC_REFACTORING(KIND, NAME, ID) \ case SourceKitRequest::KIND: \ setRefactoringFields(Req, Opts, KindRefactoring##KIND, SourceBuf.get()); \ @@ -1504,6 +1515,10 @@ static bool handleResponse(sourcekitd_response_t Resp, const TestOptions &Opts, printVariableType(Info, SourceBuf.get(), llvm::outs()); break; + case SourceKitRequest::CollectInferredIsolation: + sourcekitd_response_description_dump_filedesc(Resp, STDOUT_FILENO); + break; + case SourceKitRequest::DocInfo: printDocInfo(Info, SourceFile); break; diff --git a/tools/SourceKit/tools/sourcekitd/lib/Service/Requests.cpp b/tools/SourceKit/tools/sourcekitd/lib/Service/Requests.cpp index 9b9c448765b7e..b4bc94f41c639 100644 --- a/tools/SourceKit/tools/sourcekitd/lib/Service/Requests.cpp +++ b/tools/SourceKit/tools/sourcekitd/lib/Service/Requests.cpp @@ -1837,6 +1837,75 @@ handleRequestCollectExpressionType(const RequestDict &Req, }); } +static void +reportInferredIsolations(const RequestResult &Result, + ResponseReceiver Rec) { + if (Result.isCancelled()) + return Rec(createErrorRequestCancelled()); + if (Result.isError()) + return Rec(createErrorRequestFailed(Result.getError())); + + const InferredIsolationsInFile &Info = Result.value(); + StringRef Strings = Info.StringBuffer; + auto stringAt = [&](unsigned Off) -> StringRef { + if (Off >= Strings.size()) + return StringRef(); + return Strings.drop_front(Off).take_until([](char c) { return c == '\0'; }); + }; + + ResponseBuilder Builder; + auto Arr = Builder.getDictionary().setArray(KeyResults); + for (auto &R : Info.Results) { + auto Elem = Arr.appendDictionary(); + Elem.set(KeyOffset, R.Offset); + Elem.set(KeyLength, R.Length); + Elem.set(KeyActorIsolation, stringAt(R.IsolationOffset)); + Elem.set(KeyKind, stringAt(R.KindOffset)); + } + Rec(Builder.createResponse()); +} + +static void handleRequestCollectInferredIsolation( + const RequestDict &Req, SourceKitCancellationToken CancellationToken, + ResponseReceiver Rec) { + if (checkVFSNotSupported(Req, Rec)) + return; + + handleSemanticRequest(Req, Rec, [Req, CancellationToken, Rec]() { + LangSupport &Lang = getGlobalContext().getSwiftLangSupport(); + + std::optional PrimaryFilePath = + getPrimaryFilePathForRequestOrEmitError(Req, Rec); + if (!PrimaryFilePath) + return; + + StringRef InputBufferName = getInputBufferNameForRequest(Req, Rec); + + SmallVector Args; + if (getCompilerArgumentsForRequestOrEmitError(Req, Args, Rec)) + return; + + std::optional Offset = + swift::transform(Req.getOptionalInt64(KeyOffset), + [](int64_t v) -> unsigned { return v; }); + std::optional Length = + swift::transform(Req.getOptionalInt64(KeyLength), + [](int64_t v) -> unsigned { return v; }); + + // TODO: copied this from collect-variable-type. Is it the right default? + int64_t CancelOnSubsequentRequest = 1; + Req.getInt64(KeyCancelOnSubsequentRequest, CancelOnSubsequentRequest, + /*isOptional=*/true); + + return Lang.collectInferredIsolations( + *PrimaryFilePath, InputBufferName, Args, Offset, Length, + CancelOnSubsequentRequest, CancellationToken, + [Rec](const RequestResult &Result) { + reportInferredIsolations(Result, Rec); + }); + }); +} + static void handleRequestCollectVariableType(const RequestDict &Req, SourceKitCancellationToken CancellationToken, @@ -2315,6 +2384,8 @@ void handleRequestImpl(sourcekitd_object_t ReqObj, HANDLE_REQUEST(RequestCollectExpressionType, handleRequestCollectExpressionType) HANDLE_REQUEST(RequestCollectVariableType, handleRequestCollectVariableType) + HANDLE_REQUEST(RequestCollectInferredIsolation, + handleRequestCollectInferredIsolation) HANDLE_REQUEST(RequestFindLocalRenameRanges, handleRequestFindLocalRenameRanges) HANDLE_REQUEST(RequestNameTranslation, handleRequestNameTranslation) diff --git a/tools/swift-ide-test/swift-ide-test.cpp b/tools/swift-ide-test/swift-ide-test.cpp index ae327bb9d1c89..1ca90f3627915 100644 --- a/tools/swift-ide-test/swift-ide-test.cpp +++ b/tools/swift-ide-test/swift-ide-test.cpp @@ -111,6 +111,7 @@ enum class ActionType { PrintTypeInterface, PrintIndexedSymbols, PrintExpressionTypes, + PrintInferredIsolation, TestCreateCompilerInvocation, CompilerInvocationFromModule, GenerateModuleAPIDescription, @@ -252,6 +253,10 @@ Action(llvm::cl::desc("Mode:"), llvm::cl::init(ActionType::None), clEnumValN(ActionType::PrintExpressionTypes, "print-expr-type", "Print types for all expressions in the file"), + clEnumValN(ActionType::PrintInferredIsolation, + "print-inferred-isolation", + "Print inferred actor isolation for every closure in " + "the file"), clEnumValN(ActionType::ConformingMethodList, "conforming-methods", "Perform conforming method analysis for expression"), @@ -2883,6 +2888,62 @@ static int doPrintExpressionTypes(const CompilerInvocation &InitInvok, return EXIT_SUCCESS; } +static int doPrintInferredIsolation(const CompilerInvocation &InitInvok, + StringRef SourceFilename) { + CompilerInvocation Invocation(InitInvok); + Invocation.getFrontendOptions().InputsAndOutputs.addPrimaryInputFile( + SourceFilename); + CompilerInstance CI; + + PrintingDiagnosticConsumer PrintDiags; + CI.addDiagnosticConsumer(&PrintDiags); + std::string InstanceSetupError; + if (CI.setup(Invocation, InstanceSetupError)) { + llvm::errs() << InstanceSetupError << '\n'; + return EXIT_FAILURE; + } + registerIDERequestFunctions(CI.getASTContext().evaluator); + CI.performSema(); + + std::vector Infos; + llvm::SmallString<256> StrBuffer; + llvm::raw_svector_ostream OS(StrBuffer); + SourceFile &SF = *CI.getPrimarySourceFile(); + auto Source = + SF.getASTContext().SourceMgr.getRangeForBuffer(SF.getBufferID()).str(); + + collectInferredIsolations(SF, SourceRange(), Infos, OS); + + std::vector> SortedTags; + for (auto &R : Infos) { + auto iso = StrBuffer.str().substr(R.IsolationOffset, R.IsolationLength); + auto kind = StrBuffer.str().substr(R.KindOffset, R.KindLength); + SortedTags.push_back( + {R.Offset, (llvm::Twine("") + .str()}); + SortedTags.push_back({R.Offset + R.Length, ""}); + } + std::stable_sort(SortedTags.begin(), SortedTags.end(), + [](std::pair T1, + std::pair T2) { + return T1.first < T2.first; + }); + + ArrayRef> SortedTagsRef = SortedTags; + unsigned Cur = 0; + do { + while (!SortedTagsRef.empty() && SortedTagsRef.front().first == Cur) { + llvm::outs() << SortedTagsRef.front().second; + SortedTagsRef = SortedTagsRef.drop_front(); + } + auto Start = Cur; + Cur = SortedTagsRef.empty() ? Source.size() : SortedTagsRef.front().first; + llvm::outs() << Source.substr(Start, Cur - Start); + } while (!SortedTagsRef.empty()); + return EXIT_SUCCESS; +} + static int doPrintLocalTypes(const CompilerInvocation &InitInvok, const std::vector ModulesToPrint, const PrintOptions &PO) { @@ -4942,6 +5003,10 @@ int main(int argc, char *argv[]) { options::SourceFilename); break; + case ActionType::PrintInferredIsolation: + ExitCode = doPrintInferredIsolation(InitInvok, options::SourceFilename); + break; + case ActionType::ConformingMethodList: if (options::CodeCompletionToken.empty()) { diff --git a/utils/gyb_sourcekit_support/UIDs.py b/utils/gyb_sourcekit_support/UIDs.py index f0d7c7052fce2..85333df5cd625 100644 --- a/utils/gyb_sourcekit_support/UIDs.py +++ b/utils/gyb_sourcekit_support/UIDs.py @@ -193,6 +193,7 @@ def __init__(self, internal_name, external_name): KEY('VariableLength', 'key.variable_length'), KEY('VariableType', 'key.variable_type'), KEY('VariableTypeExplicit', 'key.variable_type_explicit'), + KEY('ActorIsolation', 'key.actor_isolation'), KEY('FullyQualified', 'key.fully_qualified'), KEY('CanonicalizeType', 'key.canonicalize_type'), KEY('InternalDiagnostic', 'key.internal_diagnostic'), @@ -297,6 +298,8 @@ def __init__(self, internal_name, external_name): REQUEST('TestNotification', 'source.request.test_notification'), REQUEST('CollectExpressionType', 'source.request.expression.type'), REQUEST('CollectVariableType', 'source.request.variable.type'), + REQUEST('CollectInferredIsolation', + 'source.request.inferred_isolation.collect'), REQUEST('GlobalConfiguration', 'source.request.configuration.global'), REQUEST('DependencyUpdated', 'source.request.dependency_updated'), REQUEST('Diagnostics', 'source.request.diagnostics'),