diff --git a/Documentation/Configuration File.md b/Documentation/Configuration File.md index 6ca95bac6..78353feb6 100644 --- a/Documentation/Configuration File.md +++ b/Documentation/Configuration File.md @@ -60,3 +60,7 @@ The structure of the file is currently not guaranteed to be stable. Options may - `sourcekitdRequestTimeout: number`: The maximum duration that a sourcekitd request should be allowed to execute before being declared as timed out. In general, editors should cancel requests that they are no longer interested in, but in case editors don't cancel requests, this ensures that a long-running non-cancelled request is not blocking sourcekitd and thus most semantic functionality. In particular, VS Code does not cancel the semantic tokens request, which can cause a long-running AST build that blocks sourcekitd. - `semanticServiceRestartTimeout: number`: If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging for some reason and won't recover. To restore semantic functionality, we terminate and restart it. - `buildServerWorkspaceRequestsTimeout: number`: Duration how long to wait for responses to `workspace/buildTargets` or `buildTarget/sources` request by the build server before defaulting to an empty response. +- `preparationBatchingStrategy: object`: Defines the batch size for target preparation. If nil, defaults to preparing 1 target at a time. + - This is a tagged union discriminated by the `strategy` field. Each case has the following structure: + - `strategy: "target"`: Prepare a fixed number of targets in a single batch. `batchSize`: The number of targets to prepare in each batch. + - `batchSize: integer`: The number of targets to prepare in each batch. diff --git a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/JSONSchema.swift b/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/JSONSchema.swift index 55f578633..3facce2bd 100644 --- a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/JSONSchema.swift +++ b/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/JSONSchema.swift @@ -38,6 +38,8 @@ struct JSONSchema: Encodable { case additionalProperties case markdownDescription case markdownEnumDescriptions + case oneOf + case const } var _schema: String? var id: String? @@ -59,6 +61,9 @@ struct JSONSchema: Encodable { /// VSCode extension: Markdown formatted descriptions for rich hover for enum values /// https://github.com/microsoft/vscode-wiki/blob/main/Setting-Descriptions.md var markdownEnumDescriptions: [String]? + + var oneOf: [JSONSchema]? + var const: String? func encode(to encoder: any Encoder) throws { // Manually implement encoding to use `encodeIfPresent` for HeapBox-ed fields @@ -82,6 +87,10 @@ struct JSONSchema: Encodable { if let markdownEnumDescriptions { try container.encode(markdownEnumDescriptions, forKey: .markdownEnumDescriptions) } + if let oneOf = oneOf, !oneOf.isEmpty { + try container.encode(oneOf, forKey: .oneOf) + } + try container.encodeIfPresent(const, forKey: .const) } } @@ -126,13 +135,53 @@ struct JSONSchemaBuilder { schema.properties = properties schema.required = required case .enum(let enumInfo): - schema.type = "string" - schema.enum = enumInfo.cases.map(\.name) - // Set `markdownEnumDescriptions` for better rendering in VSCode rich hover - // Unlike `description`, `enumDescriptions` field is not a part of JSON Schema spec, - // so we only set `markdownEnumDescriptions` here. - if enumInfo.cases.contains(where: { $0.description != nil }) { - schema.markdownEnumDescriptions = enumInfo.cases.map { $0.description ?? "" } + let hasAssociatedTypes = enumInfo.cases.contains { $0.associatedProperties != nil && !$0.associatedProperties!.isEmpty } + + if hasAssociatedTypes { + let discriminatorFieldName = enumInfo.discriminatorFieldName ?? "type" + var oneOfSchemas: [JSONSchema] = [] + + for caseInfo in enumInfo.cases { + var caseSchema = JSONSchema() + caseSchema.type = "object" + caseSchema.description = caseInfo.description + caseSchema.markdownDescription = caseInfo.description + + var caseProperties: [String: JSONSchema] = [:] + var caseRequired: [String] = [discriminatorFieldName] + + var discriminatorSchema = JSONSchema() + discriminatorSchema.const = caseInfo.name + caseProperties[discriminatorFieldName] = discriminatorSchema + + if let associatedProperties = caseInfo.associatedProperties { + for property in associatedProperties { + let propertyType = property.type + var propertySchema = try buildJSONSchema(from: propertyType) + propertySchema.description = property.description + propertySchema.markdownDescription = property.description + caseProperties[property.name] = propertySchema + if !propertyType.isOptional { + caseRequired.append(property.name) + } + } + } + + caseSchema.properties = caseProperties + caseSchema.required = caseRequired + oneOfSchemas.append(caseSchema) + } + + schema.oneOf = oneOfSchemas + } else { + schema.type = "string" + schema.enum = enumInfo.cases.map(\.name) + // Set `markdownEnumDescriptions` for better rendering in VSCode rich hover + // Unlike `description`, `enumDescriptions` field is not a part of JSON Schema spec, + // so we only set `markdownEnumDescriptions` here. + if enumInfo.cases.contains(where: { $0.description != nil }) { + schema.markdownEnumDescriptions = enumInfo.cases.map { $0.description ?? "" } + } } } return schema diff --git a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionDocument.swift b/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionDocument.swift index 23271d64c..837daaf5a 100644 --- a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionDocument.swift +++ b/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionDocument.swift @@ -66,14 +66,34 @@ struct OptionDocumentBuilder { try appendProperty(property, indentLevel: indentLevel + 1) } case .enum(let schema): - for caseInfo in schema.cases { - // Add detailed description for each case if available - guard let description = caseInfo.description else { - continue + let hasAssociatedTypes = schema.cases.contains { $0.associatedProperties != nil && !$0.associatedProperties!.isEmpty } + + if hasAssociatedTypes { + let discriminatorFieldName = schema.discriminatorFieldName ?? "type" + doc += "\(indent) - This is a tagged union discriminated by the `\(discriminatorFieldName)` field. Each case has the following structure:\n" + + for caseInfo in schema.cases { + doc += "\(indent) - `\(discriminatorFieldName): \"\(caseInfo.name)\"`" + if let description = caseInfo.description { + doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent) ") + } + doc += "\n" + + if let associatedProperties = caseInfo.associatedProperties { + for assocProp in associatedProperties { + try appendProperty(assocProp, indentLevel: indentLevel + 2) + } + } + } + } else { + for caseInfo in schema.cases { + guard let description = caseInfo.description else { + continue + } + doc += "\(indent) - `\(caseInfo.name)`" + doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent) ") + doc += "\n" } - doc += "\(indent) - `\(caseInfo.name)`" - doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent) ") - doc += "\n" } default: break } @@ -100,8 +120,13 @@ struct OptionDocumentBuilder { case .struct(let structInfo): return structInfo.name case .enum(let enumInfo): - let cases = enumInfo.cases.map { "\"\($0.name)\"" }.joined(separator: "|") - return shouldWrap ? "(\(cases))" : cases + let hasAssociatedTypes = enumInfo.cases.contains { $0.associatedProperties != nil && !$0.associatedProperties!.isEmpty } + if hasAssociatedTypes { + return "object" + } else { + let cases = enumInfo.cases.map { "\"\($0.name)\"" }.joined(separator: "|") + return shouldWrap ? "(\(cases))" : cases + } } } } diff --git a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionSchema.swift b/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionSchema.swift index 64876c1db..f6dbe0a09 100644 --- a/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionSchema.swift +++ b/SourceKitLSPDevUtils/Sources/ConfigSchemaGen/OptionSchema.swift @@ -31,11 +31,13 @@ struct OptionTypeSchama { struct Case { var name: String var description: String? + var associatedProperties: [Property]? } struct Enum { var name: String var cases: [Case] + var discriminatorFieldName: String? } enum Kind { @@ -146,14 +148,13 @@ struct OptionSchemaContext { } private func buildEnumCases(_ node: EnumDeclSyntax) throws -> OptionTypeSchama.Enum { + let discriminatorFieldName = Self.extractDiscriminatorFieldName(node.leadingTrivia) + let cases = try node.memberBlock.members.flatMap { member -> [OptionTypeSchama.Case] in guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { return [] } return try caseDecl.elements.compactMap { - guard $0.parameterClause == nil else { - throw ConfigSchemaGenError("Associated values in enum cases are not supported: \(caseDecl)") - } let name: String if let rawValue = $0.rawValue?.value { if let stringLiteral = rawValue.as(StringLiteralExprSyntax.self), @@ -172,11 +173,44 @@ struct OptionSchemaContext { if description?.contains("- Note: Internal option") ?? false { return nil } - return OptionTypeSchama.Case(name: name, description: description) + + var associatedProperties: [OptionTypeSchama.Property]? = nil + if let parameterClause = $0.parameterClause { + let caseDescription = description + associatedProperties = try parameterClause.parameters.map { param in + let propertyName: String + if let firstName = param.firstName, firstName.tokenKind != .wildcard { + propertyName = firstName.text + } else if let secondName = param.secondName { + propertyName = secondName.text + } else { + propertyName = name + } + + let propertyType = try resolveType(param.type) + let propertyDescription = Self.extractParameterDescription( + from: caseDescription, + parameterName: propertyName + ) ?? Self.extractDocComment(param.leadingTrivia) + + return OptionTypeSchama.Property( + name: propertyName, + type: propertyType, + description: propertyDescription, + defaultValue: nil + ) + } + } + + return OptionTypeSchama.Case( + name: name, + description: description, + associatedProperties: associatedProperties + ) } } let typeName = node.name.text - return .init(name: typeName, cases: cases) + return .init(name: typeName, cases: cases, discriminatorFieldName: discriminatorFieldName) } private func buildStructProperties(_ node: StructDeclSyntax) throws -> OptionTypeSchama.Struct { @@ -234,4 +268,52 @@ struct OptionSchemaContext { } return docLines.joined(separator: " ") } + + private static func extractDiscriminatorFieldName(_ trivia: Trivia) -> String? { + let docLines = trivia.flatMap { piece -> [Substring] in + switch piece { + case .docBlockComment(let text): + assert(text.hasPrefix("/**") && text.hasSuffix("*/"), "Unexpected doc block comment format: \(text)") + return text.dropFirst(3).dropLast(2).split { $0.isNewline } + case .docLineComment(let text): + assert(text.hasPrefix("///"), "Unexpected doc line comment format: \(text)") + let text = text.dropFirst(3) + return [text] + default: + return [] + } + } + + for line in docLines { + var trimmed = line + while trimmed.first?.isWhitespace == true { + trimmed = trimmed.dropFirst() + } + if trimmed.hasPrefix("- discriminator:") { + let fieldName = trimmed.dropFirst("- discriminator:".count).trimmingCharacters(in: .whitespaces) + return fieldName.isEmpty ? nil : fieldName + } + } + return nil + } + + private static func extractParameterDescription(from docComment: String?, parameterName: String) -> String? { + guard let docComment = docComment else { + return nil + } + + let pattern = "`\(parameterName)`:" + guard let range = docComment.range(of: pattern) else { + return nil + } + + let afterPattern = docComment[range.upperBound...] + let lines = afterPattern.split(separator: "\n", maxSplits: 1, omittingEmptySubsequences: false) + guard let firstLine = lines.first else { + return nil + } + + let description = firstLine.trimmingCharacters(in: .whitespaces) + return description.isEmpty ? nil : description + } } diff --git a/Sources/SKOptions/PreparationBatchingStrategy.swift b/Sources/SKOptions/PreparationBatchingStrategy.swift new file mode 100644 index 000000000..705e42e7d --- /dev/null +++ b/Sources/SKOptions/PreparationBatchingStrategy.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Defines the batch size for target preparation. +/// +/// If nil, SourceKit-LSP will default to preparing 1 target at a time. +/// +/// - discriminator: strategy +public enum PreparationBatchingStrategy: Sendable, Equatable { + /// Prepare a fixed number of targets in a single batch. + /// + /// `batchSize`: The number of targets to prepare in each batch. + case target(batchSize: Int) +} + +extension PreparationBatchingStrategy: Codable { + private enum CodingKeys: String, CodingKey { + case strategy + case batchSize + } + + private enum StrategyValue: String, Codable { + case target + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let strategy = try container.decode(StrategyValue.self, forKey: .strategy) + + switch strategy { + case .target: + let batchSize = try container.decode(Int.self, forKey: .batchSize) + self = .target(batchSize: batchSize) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .target(let batchSize): + try container.encode(StrategyValue.target, forKey: .strategy) + try container.encode(batchSize, forKey: .batchSize) + } + } +} \ No newline at end of file diff --git a/Sources/SKOptions/SourceKitLSPOptions.swift b/Sources/SKOptions/SourceKitLSPOptions.swift index 4a7897118..c08af3be3 100644 --- a/Sources/SKOptions/SourceKitLSPOptions.swift +++ b/Sources/SKOptions/SourceKitLSPOptions.swift @@ -441,14 +441,13 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable { if let buildServerWorkspaceRequestsTimeout { return .seconds(buildServerWorkspaceRequestsTimeout) } - // The default value needs to strike a balance: If the build server is slow to respond, we don't want to constantly - // run into this timeout, which causes somewhat expensive computations because we trigger the `buildTargetsChanged` - // chain. - // At the same time, we do want to provide functionality based on fallback settings after some time. - // 15s seems like it should strike a balance here but there is no data backing this value up. return .seconds(15) } + /// Defines the batch size for target preparation. + /// If nil, defaults to preparing 1 target at a time. + public var preparationBatchingStrategy: PreparationBatchingStrategy? + public init( swiftPM: SwiftPMOptions? = .init(), fallbackBuildSystem: FallbackBuildSystemOptions? = .init(), @@ -462,6 +461,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable { generatedFilesPath: String? = nil, backgroundIndexing: Bool? = nil, backgroundPreparationMode: BackgroundPreparationMode? = nil, + preparationBatchingStrategy: PreparationBatchingStrategy? = nil, cancelTextDocumentRequestsOnEditAndClose: Bool? = nil, experimentalFeatures: Set? = nil, swiftPublishDiagnosticsDebounceDuration: Double? = nil, @@ -482,6 +482,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable { self.defaultWorkspaceType = defaultWorkspaceType self.backgroundIndexing = backgroundIndexing self.backgroundPreparationMode = backgroundPreparationMode + self.preparationBatchingStrategy = preparationBatchingStrategy self.cancelTextDocumentRequestsOnEditAndClose = cancelTextDocumentRequestsOnEditAndClose self.experimentalFeatures = experimentalFeatures self.swiftPublishDiagnosticsDebounceDuration = swiftPublishDiagnosticsDebounceDuration @@ -545,6 +546,7 @@ public struct SourceKitLSPOptions: Sendable, Codable, Equatable { generatedFilesPath: override?.generatedFilesPath ?? base.generatedFilesPath, backgroundIndexing: override?.backgroundIndexing ?? base.backgroundIndexing, backgroundPreparationMode: override?.backgroundPreparationMode ?? base.backgroundPreparationMode, + preparationBatchingStrategy: override?.preparationBatchingStrategy ?? base.preparationBatchingStrategy, cancelTextDocumentRequestsOnEditAndClose: override?.cancelTextDocumentRequestsOnEditAndClose ?? base.cancelTextDocumentRequestsOnEditAndClose, experimentalFeatures: override?.experimentalFeatures ?? base.experimentalFeatures, diff --git a/Sources/SemanticIndex/PreparationTaskDescription.swift b/Sources/SemanticIndex/PreparationTaskDescription.swift index 5e5d932b1..5def2b7d1 100644 --- a/Sources/SemanticIndex/PreparationTaskDescription.swift +++ b/Sources/SemanticIndex/PreparationTaskDescription.swift @@ -48,6 +48,8 @@ package struct PreparationTaskDescription: IndexTaskDescription { /// Hooks that should be called when the preparation task finishes. private let hooks: IndexHooks + private let purpose: TargetPreparationPurpose + /// The task is idempotent because preparing the same target twice produces the same result as preparing it once. package var isIdempotent: Bool { true } @@ -69,13 +71,15 @@ package struct PreparationTaskDescription: IndexTaskDescription { @escaping @Sendable ( _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind ) -> Void, - hooks: IndexHooks + hooks: IndexHooks, + purpose: TargetPreparationPurpose ) { self.targetsToPrepare = targetsToPrepare self.buildServerManager = buildServerManager self.preparationUpToDateTracker = preparationUpToDateTracker self.logMessageToIndexLog = logMessageToIndexLog self.hooks = hooks + self.purpose = purpose } package func execute() async { @@ -121,11 +125,9 @@ package struct PreparationTaskDescription: IndexTaskDescription { to currentlyExecutingTasks: [PreparationTaskDescription] ) -> [TaskDependencyAction] { return currentlyExecutingTasks.compactMap { (other) -> TaskDependencyAction? in - if other.targetsToPrepare.count > self.targetsToPrepare.count { - // If there is an prepare operation with more targets already running, suspend it. - // The most common use case for this is if we prepare all targets simultaneously during the initial preparation - // when a project is opened and need a single target indexed for user interaction. We should suspend the - // workspace-wide preparation and just prepare the currently needed target. + if other.purpose == .forIndexing && self.purpose == .forEditorFunctionality { + // If we're running a background indexing operation but need a target indexed for user interaction, + // we should prioritize the latter. return .cancelAndRescheduleDependency(other) } return .waitAndElevatePriorityOfDependency(other) diff --git a/Sources/SemanticIndex/SemanticIndexManager.swift b/Sources/SemanticIndex/SemanticIndexManager.swift index 49c43bae1..6abd9e7c9 100644 --- a/Sources/SemanticIndex/SemanticIndexManager.swift +++ b/Sources/SemanticIndex/SemanticIndexManager.swift @@ -16,6 +16,7 @@ import Foundation package import LanguageServerProtocol import LanguageServerProtocolExtensions import SKLogging +package import SKOptions import SwiftExtensions /// The logging subsystem that should be used for all index-related logging. @@ -154,7 +155,7 @@ private struct InProgressPrepareForEditorTask { } /// The reason why a target is being prepared. This is used to determine the `IndexProgressStatus`. -private enum TargetPreparationPurpose: Comparable { +package enum TargetPreparationPurpose: Comparable { /// We are preparing the target so we can index files in it. case forIndexing @@ -232,6 +233,9 @@ package final actor SemanticIndexManager { /// The parameter is the number of files that were scheduled to be indexed. private let indexTasksWereScheduled: @Sendable (_ numberOfFileScheduled: Int) -> Void + /// The size of the batches in which the `SemanticIndexManager` should dispatch preparation tasks. + private let preparationBatchingStrategy: PreparationBatchingStrategy? + /// Callback that is called when `progressStatus` might have changed. private let indexProgressStatusDidChange: @Sendable () -> Void @@ -271,6 +275,7 @@ package final actor SemanticIndexManager { updateIndexStoreTimeout: Duration, hooks: IndexHooks, indexTaskScheduler: TaskScheduler, + preparationBatchingStrategy: PreparationBatchingStrategy?, logMessageToIndexLog: @escaping @Sendable ( _ message: String, _ type: WindowMessageType, _ structure: LanguageServerProtocol.StructuredLogKind @@ -283,6 +288,7 @@ package final actor SemanticIndexManager { self.updateIndexStoreTimeout = updateIndexStoreTimeout self.hooks = hooks self.indexTaskScheduler = indexTaskScheduler + self.preparationBatchingStrategy = preparationBatchingStrategy self.logMessageToIndexLog = logMessageToIndexLog self.indexTasksWereScheduled = indexTasksWereScheduled self.indexProgressStatusDidChange = indexProgressStatusDidChange @@ -665,7 +671,8 @@ package final actor SemanticIndexManager { buildServerManager: self.buildServerManager, preparationUpToDateTracker: preparationUpToDateTracker, logMessageToIndexLog: logMessageToIndexLog, - hooks: hooks + hooks: hooks, + purpose: purpose ) ) if Task.isCancelled { @@ -919,7 +926,16 @@ package final actor SemanticIndexManager { // TODO: When we can index multiple targets concurrently in SwiftPM, increase the batch size to half the // processor count, so we can get parallelism during preparation. // (https://github.com/swiftlang/sourcekit-lsp/issues/1262) - for targetsBatch in sortedTargets.partition(intoBatchesOfSize: 1) { + let batchSize: Int + switch preparationBatchingStrategy { + case .target(let size): + batchSize = max(size, 1) + case nil: + batchSize = 1 // Default: prepare 1 target at a time + } + let partitionedTargets = sortedTargets.partition(intoBatchesOfSize: batchSize) + + for targetsBatch in partitionedTargets { let preparationTaskID = UUID() let filesToIndex = targetsBatch.flatMap { (target) -> [FileIndexInfo] in guard let files = filesByTarget[target] else { diff --git a/Sources/SourceKitLSP/Workspace.swift b/Sources/SourceKitLSP/Workspace.swift index d1a28cbcd..bc889bcd2 100644 --- a/Sources/SourceKitLSP/Workspace.swift +++ b/Sources/SourceKitLSP/Workspace.swift @@ -240,6 +240,7 @@ package final class Workspace: Sendable, BuildServerManagerDelegate { updateIndexStoreTimeout: options.indexOrDefault.updateIndexStoreTimeoutOrDefault, hooks: hooks.indexHooks, indexTaskScheduler: indexTaskScheduler, + preparationBatchingStrategy: options.preparationBatchingStrategy, logMessageToIndexLog: { [weak sourceKitLSPServer] in sourceKitLSPServer?.logMessageToIndexLog(message: $0, type: $1, structure: $2) }, diff --git a/config.schema.json b/config.schema.json index b286aa0c2..a178682ff 100644 --- a/config.schema.json +++ b/config.schema.json @@ -186,6 +186,31 @@ }, "type" : "object" }, + "preparationBatchingStrategy" : { + "description" : "Defines the batch size for target preparation. If nil, defaults to preparing 1 target at a time.", + "markdownDescription" : "Defines the batch size for target preparation. If nil, defaults to preparing 1 target at a time.", + "oneOf" : [ + { + "description" : "Prepare a fixed number of targets in a single batch. `batchSize`: The number of targets to prepare in each batch.", + "markdownDescription" : "Prepare a fixed number of targets in a single batch. `batchSize`: The number of targets to prepare in each batch.", + "properties" : { + "batchSize" : { + "description" : "The number of targets to prepare in each batch.", + "markdownDescription" : "The number of targets to prepare in each batch.", + "type" : "integer" + }, + "strategy" : { + "const" : "target" + } + }, + "required" : [ + "strategy", + "batchSize" + ], + "type" : "object" + } + ] + }, "semanticServiceRestartTimeout" : { "description" : "If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging for some reason and won't recover. To restore semantic functionality, we terminate and restart it.", "markdownDescription" : "If a request to sourcekitd or clangd exceeds this timeout, we assume that the semantic service provider is hanging for some reason and won't recover. To restore semantic functionality, we terminate and restart it.",