Skip to content

Commit 447cc9e

Browse files
committed
Teach sourcekit-lsp diagnose how to reduce swift-frontend crashes
`sourcekit-lsp diagnose` tries to automatically scrape crash reports on the user’s machine for compiler arguments. If it finds any, it tries to reduce that swift-frontend crash. Alternatively, a swift-frontend crash can be reduced by running `sourcekit-lsp reduce-frontend [--toolchain /path/to/toolchain] --frontend-args -all -the frontend --args`
1 parent bf31f4a commit 447cc9e

16 files changed

+974
-207
lines changed

Sources/Diagnose/CMakeLists.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
add_library(Diagnose STATIC
22
CommandLineArgumentsReducer.swift
33
DiagnoseCommand.swift
4+
MergeSwiftFiles.swift
45
OSLogScraper.swift
56
ReduceCommand.swift
7+
ReduceFrontendCommand.swift
8+
ReduceSourceKitDRequest.swift
9+
ReduceSwiftFrontend.swift
610
ReductionError.swift
711
ReproducerBundle.swift
812
RequestInfo.swift
913
SourceKitD+RunWithYaml.swift
1014
SourceKitDRequestExecutor.swift
15+
SourceReducer.swift
1116
SourcekitdRequestCommand.swift
12-
SourceReducer.swift)
17+
SwiftFrontendCrashScraper.swift
18+
Toolchain+SwiftFrontend.swift)
1319

1420
set_target_properties(Diagnose PROPERTIES
1521
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})

Sources/Diagnose/CommandLineArgumentsReducer.swift

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Foundation
14+
import LSPLogging
1415

1516
// MARK: - Entry point
1617

@@ -48,60 +49,65 @@ fileprivate class CommandLineArgumentReducer {
4849
self.progressUpdate = progressUpdate
4950
}
5051

51-
func logSuccessfulReduction(_ requestInfo: RequestInfo) {
52-
progressUpdate(
53-
1 - (Double(requestInfo.compilerArgs.count) / Double(initialCommandLineCount)),
54-
"Reduced compiler arguments to \(requestInfo.compilerArgs.count)"
55-
)
52+
func run(initialRequestInfo: RequestInfo) async throws -> RequestInfo {
53+
var requestInfo = initialRequestInfo
54+
requestInfo = try await reduce(initialRequestInfo: requestInfo, simultaneousRemove: 10)
55+
requestInfo = try await reduce(initialRequestInfo: requestInfo, simultaneousRemove: 1)
56+
return requestInfo
5657
}
5758

58-
func run(initialRequestInfo: RequestInfo) async throws -> RequestInfo {
59+
/// Reduce the command line arguments of the given `RequestInfo`.
60+
///
61+
/// If `simultaneousRemove` is set, the reducer will try to remove that many arguments at once. This is useful to
62+
/// quickly remove multiple arguments from the request.
63+
private func reduce(initialRequestInfo: RequestInfo, simultaneousRemove: Int) async throws -> RequestInfo {
64+
guard initialRequestInfo.compilerArgs.count > simultaneousRemove else {
65+
// Trying to remove more command line arguments than we have. This isn't going to work.
66+
return initialRequestInfo
67+
}
68+
5969
var requestInfo = initialRequestInfo
6070
self.initialCommandLineCount = requestInfo.compilerArgs.count
6171

6272
var argumentIndexToRemove = requestInfo.compilerArgs.count - 1
63-
while argumentIndexToRemove >= 0 {
64-
var numberOfArgumentsToRemove = 1
73+
while argumentIndexToRemove + 1 >= simultaneousRemove {
74+
defer {
75+
// argumentIndexToRemove can become negative by being decremented in the code below
76+
let progress = 1 - (Double(max(argumentIndexToRemove, 0)) / Double(initialCommandLineCount))
77+
progressUpdate(progress, "Reduced compiler arguments to \(requestInfo.compilerArgs.count)")
78+
}
79+
var numberOfArgumentsToRemove = simultaneousRemove
6580
// If the argument is preceded by -Xswiftc or -Xcxx, we need to remove the `-X` flag as well.
66-
if argumentIndexToRemove - numberOfArgumentsToRemove >= 0
67-
&& requestInfo.compilerArgs[argumentIndexToRemove - numberOfArgumentsToRemove].hasPrefix("-X")
68-
{
81+
if requestInfo.compilerArgs[safe: argumentIndexToRemove - numberOfArgumentsToRemove]?.hasPrefix("-X") ?? false {
6982
numberOfArgumentsToRemove += 1
7083
}
7184

72-
if let reduced = try await tryRemoving(
73-
(argumentIndexToRemove - numberOfArgumentsToRemove + 1)...argumentIndexToRemove,
74-
from: requestInfo
75-
) {
85+
let rangeToRemove = (argumentIndexToRemove - numberOfArgumentsToRemove + 1)...argumentIndexToRemove
86+
if let reduced = try await tryRemoving(rangeToRemove, from: requestInfo) {
7687
requestInfo = reduced
7788
argumentIndexToRemove -= numberOfArgumentsToRemove
7889
continue
7990
}
8091

81-
// If removing the argument failed and the argument is preceded by an argument starting with `-`, try removing that as well.
82-
// E.g. removing `-F` followed by a search path.
83-
if argumentIndexToRemove - numberOfArgumentsToRemove >= 0
84-
&& requestInfo.compilerArgs[argumentIndexToRemove - numberOfArgumentsToRemove].hasPrefix("-")
85-
{
92+
// If removing the argument failed and the argument is preceded by an argument starting with `-`, try removing
93+
// that as well. E.g. removing `-F` followed by a search path.
94+
if requestInfo.compilerArgs[safe: argumentIndexToRemove - numberOfArgumentsToRemove]?.hasPrefix("-") ?? false {
8695
numberOfArgumentsToRemove += 1
87-
}
8896

89-
// If the argument is preceded by -Xswiftc or -Xcxx, we need to remove the `-X` flag as well.
90-
if argumentIndexToRemove - numberOfArgumentsToRemove >= 0
91-
&& requestInfo.compilerArgs[argumentIndexToRemove - numberOfArgumentsToRemove].hasPrefix("-X")
92-
{
93-
numberOfArgumentsToRemove += 1
97+
// If the argument is preceded by -Xswiftc or -Xcxx, we need to remove the `-X` flag as well.
98+
if requestInfo.compilerArgs[safe: argumentIndexToRemove - numberOfArgumentsToRemove]?.hasPrefix("-X") ?? false {
99+
numberOfArgumentsToRemove += 1
100+
}
101+
102+
let rangeToRemove = (argumentIndexToRemove - numberOfArgumentsToRemove + 1)...argumentIndexToRemove
103+
if let reduced = try await tryRemoving(rangeToRemove, from: requestInfo) {
104+
requestInfo = reduced
105+
argumentIndexToRemove -= numberOfArgumentsToRemove
106+
continue
107+
}
94108
}
95109

96-
if let reduced = try await tryRemoving(
97-
(argumentIndexToRemove - numberOfArgumentsToRemove + 1)...argumentIndexToRemove,
98-
from: requestInfo
99-
) {
100-
requestInfo = reduced
101-
argumentIndexToRemove -= numberOfArgumentsToRemove
102-
continue
103-
}
104-
argumentIndexToRemove -= 1
110+
argumentIndexToRemove -= simultaneousRemove
105111
}
106112

107113
return requestInfo
@@ -111,16 +117,28 @@ fileprivate class CommandLineArgumentReducer {
111117
_ argumentsToRemove: ClosedRange<Int>,
112118
from requestInfo: RequestInfo
113119
) async throws -> RequestInfo? {
120+
logger.debug("Try removing the following compiler arguments:\n\(requestInfo.compilerArgs[argumentsToRemove])")
114121
var reducedRequestInfo = requestInfo
115122
reducedRequestInfo.compilerArgs.removeSubrange(argumentsToRemove)
116123

117124
let result = try await sourcekitdExecutor.run(request: reducedRequestInfo)
118125
if case .reproducesIssue = result {
119-
logSuccessfulReduction(reducedRequestInfo)
126+
logger.debug("Reduction successful")
120127
return reducedRequestInfo
121128
} else {
122129
// The reduced request did not crash. We did not find a reduced test case, so return `nil`.
130+
logger.debug("Reduction did not reproduce the issue")
131+
return nil
132+
}
133+
}
134+
}
135+
136+
fileprivate extension Array {
137+
/// Access index in the array if it's in bounds or return `nil` if `index` is outside of the array's bounds.
138+
subscript(safe index: Int) -> Element? {
139+
if index < 0 || index >= count {
123140
return nil
124141
}
142+
return self[index]
125143
}
126144
}

0 commit comments

Comments
 (0)