Skip to content

Commit d765ec8

Browse files
authored
Merge pull request #1997 from rjmansfield/fix-save-opt-record-path
Fix optimization record path handling in primary file compilation mode
2 parents 0764248 + 569f448 commit d765ec8

File tree

3 files changed

+300
-13
lines changed

3 files changed

+300
-13
lines changed

Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -719,10 +719,22 @@ extension Driver {
719719
input: TypedVirtualPath?,
720720
flag: String
721721
) throws {
722-
// Handle directory-based options and file maps for SIL and LLVM IR when finalOutputPath is nil
723-
if finalOutputPath == nil && (outputType == .sil || outputType == .llvmIR) {
724-
let directoryOption: Option = outputType == .sil ? .silOutputDir : .irOutputDir
725-
let directory = parsedOptions.getLastArgument(directoryOption)?.asSingle
722+
// Handle directory-based options and file maps for SIL, LLVM IR, and optimization records when finalOutputPath is nil
723+
if finalOutputPath == nil && (outputType == .sil || outputType == .llvmIR || outputType.isOptimizationRecord) {
724+
let directoryOption: Option?
725+
switch outputType {
726+
case .sil:
727+
directoryOption = .silOutputDir
728+
case .llvmIR:
729+
directoryOption = .irOutputDir
730+
case .yamlOptimizationRecord, .bitstreamOptimizationRecord:
731+
// Optimization records don't have a directory option
732+
directoryOption = nil
733+
default:
734+
fatalError("Unexpected output type")
735+
}
736+
737+
let directory = directoryOption.flatMap { parsedOptions.getLastArgument($0)?.asSingle }
726738
let hasFileMapEntries = outputFileMap?.hasEntries(for: outputType) ?? false
727739

728740
if directory != nil || hasFileMapEntries || (parsedOptions.hasArgument(.saveTemps) && !hasFileMapEntries) {
@@ -749,11 +761,17 @@ extension Driver {
749761
// use the final output.
750762
let outputPath: VirtualPath.Handle
751763
if let input = input {
764+
// Check if the output file map has an entry for this specific input and output type
752765
if let outputFileMapPath = try outputFileMap?.existingOutput(inputFile: input.fileHandle, outputType: outputType) {
753766
outputPath = outputFileMapPath
754767
} else if let output = inputOutputMap[input]?.first, output.file != .standardOutput, compilerOutputType != nil {
755-
// Alongside primary output
756-
outputPath = try output.file.replacingExtension(with: outputType).intern()
768+
// For optimization records with an explicit final output path and no file map entry, use the final output path
769+
if outputType.isOptimizationRecord {
770+
outputPath = finalOutputPath
771+
} else {
772+
// Otherwise, derive path alongside primary output
773+
outputPath = try output.file.replacingExtension(with: outputType).intern()
774+
}
757775
} else {
758776
outputPath = try VirtualPath.createUniqueTemporaryFile(RelativePath(validating: input.file.basenameWithoutExt.appendingFileTypeExtension(outputType))).intern()
759777
}
@@ -813,22 +831,18 @@ extension Driver {
813831
input: input,
814832
flag: "-emit-reference-dependencies-path")
815833

816-
try addOutputOfType(
817-
outputType: self.optimizationRecordFileType ?? .yamlOptimizationRecord,
818-
finalOutputPath: optimizationRecordPath,
819-
input: input,
820-
flag: "-save-optimization-record-path")
821-
822834
try addOutputOfType(
823835
outputType: .diagnostics,
824836
finalOutputPath: serializedDiagnosticsFilePath,
825837
input: input,
826838
flag: "-serialize-diagnostics-path")
827839

828-
// Add SIL and IR outputs when explicitly requested via directory options, file maps, or -save-temps
840+
// Add SIL, IR, and optimization record outputs when explicitly requested via directory options, file maps, or -save-temps
829841
let saveTempsWithoutFileMap = parsedOptions.hasArgument(.saveTemps) && outputFileMap == nil
830842
let hasSilFileMapEntries = outputFileMap?.hasEntries(for: .sil) ?? false
831843
let hasIrFileMapEntries = outputFileMap?.hasEntries(for: .llvmIR) ?? false
844+
let optRecordType = self.optimizationRecordFileType ?? .yamlOptimizationRecord
845+
let hasOptRecordFileMapEntries = outputFileMap?.hasEntries(for: optRecordType) ?? false
832846

833847
let silOutputPathSupported = Driver.isOptionFound("-sil-output-path", allOpts: supportedFrontendFlags)
834848
let irOutputPathSupported = Driver.isOptionFound("-ir-output-path", allOpts: supportedFrontendFlags)
@@ -843,6 +857,9 @@ extension Driver {
843857

844858
let shouldAddSilOutput = silOutputPathSupported && (parsedOptions.hasArgument(.silOutputDir) || saveTempsWithoutFileMap || hasSilFileMapEntries)
845859
let shouldAddIrOutput = irOutputPathSupported && (parsedOptions.hasArgument(.irOutputDir) || saveTempsWithoutFileMap || hasIrFileMapEntries)
860+
let shouldAddOptRecordOutput = parsedOptions.hasArgument(.saveOptimizationRecord) ||
861+
parsedOptions.hasArgument(.saveOptimizationRecordEQ) ||
862+
hasOptRecordFileMapEntries
846863

847864
if shouldAddSilOutput {
848865
try addOutputOfType(
@@ -859,6 +876,30 @@ extension Driver {
859876
input: input,
860877
flag: "-ir-output-path")
861878
}
879+
880+
if shouldAddOptRecordOutput {
881+
let inputHasOptRecordEntry = input.flatMap { inp in
882+
(try? outputFileMap?.existingOutput(inputFile: inp.fileHandle, outputType: optRecordType)) != nil
883+
} ?? false
884+
885+
// Pass nil for finalOutputPath when this specific input has a file map entry,
886+
// so that the file map entry will be used. Otherwise, use the explicit path if provided.
887+
let effectiveFinalPath = inputHasOptRecordEntry ? nil : optimizationRecordPath
888+
try addOutputOfType(
889+
outputType: optRecordType,
890+
finalOutputPath: effectiveFinalPath,
891+
input: input,
892+
flag: "-save-optimization-record-path")
893+
}
894+
}
895+
896+
// Emit warning once if both -save-optimization-record-path and file map entries are provided
897+
let optRecordType = self.optimizationRecordFileType ?? .yamlOptimizationRecord
898+
let hasOptRecordFileMapEntries = outputFileMap?.hasEntries(for: optRecordType) ?? false
899+
if hasOptRecordFileMapEntries && optimizationRecordPath != nil {
900+
diagnosticEngine.emit(.warning(
901+
"ignoring -save-optimization-record-path because output file map contains optimization record entries"
902+
))
862903
}
863904

864905
if compilerMode.usesPrimaryFileInputs {

Sources/SwiftDriver/Utilities/FileType.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,13 @@ extension FileType {
310310
}
311311
}
312312

313+
extension FileType {
314+
/// Whether this file type represents an optimization record
315+
public var isOptimizationRecord: Bool {
316+
self == .yamlOptimizationRecord || self == .bitstreamOptimizationRecord
317+
}
318+
}
319+
313320
extension FileType {
314321

315322
private static let typesByName = Dictionary(uniqueKeysWithValues: FileType.allCases.map { ($0.name, $0) })

Tests/SwiftDriverTests/SwiftDriverTests.swift

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3801,6 +3801,245 @@ final class SwiftDriverTests: XCTestCase {
38013801
try checkSupplementaryOutputFileMap(format: "bitstream", .bitstreamOptimizationRecord)
38023802
}
38033803

3804+
func testOptimizationRecordPathUserProvidedPath() throws {
3805+
3806+
do {
3807+
var driver = try Driver(args: [
3808+
"swiftc", "-save-optimization-record", "-save-optimization-record-path", "/tmp/test.opt.yaml",
3809+
"-c", "test.swift"
3810+
])
3811+
let plannedJobs = try driver.planBuild()
3812+
let compileJob = try XCTUnwrap(plannedJobs.first { $0.kind == .compile })
3813+
3814+
XCTAssertTrue(compileJob.commandLine.contains(.path(VirtualPath.absolute(try AbsolutePath(validating: "/tmp/test.opt.yaml")))))
3815+
XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")))
3816+
}
3817+
3818+
// Test primary file mode with multiple files and explicit path
3819+
do {
3820+
var driver = try Driver(args: [
3821+
"swiftc", "-save-optimization-record", "-save-optimization-record-path", "/tmp/primary.opt.yaml",
3822+
"-c", "file1.swift", "file2.swift"
3823+
])
3824+
let plannedJobs = try driver.planBuild()
3825+
let compileJobs = plannedJobs.filter { $0.kind == .compile }
3826+
XCTAssertEqual(compileJobs.count, 2, "Should have two compile jobs in primary file mode")
3827+
3828+
for compileJob in compileJobs {
3829+
XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")),
3830+
"Each compile job should have -save-optimization-record-path flag")
3831+
XCTAssertTrue(compileJob.commandLine.contains(.path(VirtualPath.absolute(try AbsolutePath(validating: "/tmp/primary.opt.yaml")))),
3832+
"Each compile job should have the user-provided path")
3833+
}
3834+
}
3835+
3836+
do {
3837+
var driver = try Driver(args: [
3838+
"swiftc", "-wmo", "-save-optimization-record", "-save-optimization-record-path", "/tmp/wmo.opt.yaml",
3839+
"-c", "test.swift"
3840+
])
3841+
let plannedJobs = try driver.planBuild()
3842+
let compileJob = try XCTUnwrap(plannedJobs.first { $0.kind == .compile })
3843+
3844+
XCTAssertTrue(compileJob.commandLine.contains(.path(VirtualPath.absolute(try AbsolutePath(validating: "/tmp/wmo.opt.yaml")))))
3845+
XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")))
3846+
}
3847+
3848+
// Test multithreaded WMO with multiple optimization record paths
3849+
do {
3850+
var driver = try Driver(args: [
3851+
"swiftc", "-wmo", "-num-threads", "4", "-save-optimization-record",
3852+
"-save-optimization-record-path", "/tmp/mt1.opt.yaml",
3853+
"-save-optimization-record-path", "/tmp/mt2.opt.yaml",
3854+
"-c", "test1.swift", "test2.swift"
3855+
])
3856+
let plannedJobs = try driver.planBuild()
3857+
let compileJobs = plannedJobs.filter { $0.kind == .compile }
3858+
3859+
XCTAssertGreaterThanOrEqual(compileJobs.count, 1, "Should have at least one compile job")
3860+
3861+
var foundPaths: Set<String> = []
3862+
for compileJob in compileJobs {
3863+
XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")),
3864+
"Each compile job should have -save-optimization-record-path flag")
3865+
3866+
if compileJob.commandLine.contains(.path(VirtualPath.absolute(try AbsolutePath(validating: "/tmp/mt1.opt.yaml")))) {
3867+
foundPaths.insert("/tmp/mt1.opt.yaml")
3868+
}
3869+
if compileJob.commandLine.contains(.path(VirtualPath.absolute(try AbsolutePath(validating: "/tmp/mt2.opt.yaml")))) {
3870+
foundPaths.insert("/tmp/mt2.opt.yaml")
3871+
}
3872+
}
3873+
3874+
XCTAssertGreaterThanOrEqual(foundPaths.count, 1,
3875+
"At least one of the provided optimization record paths should be used")
3876+
}
3877+
}
3878+
3879+
func testOptimizationRecordWithOutputFileMap() throws {
3880+
try withTemporaryDirectory { path in
3881+
let outputFileMap = path.appending(component: "outputFileMap.json")
3882+
let file1 = path.appending(component: "file1.swift")
3883+
let file2 = path.appending(component: "file2.swift")
3884+
let optRecord1 = path.appending(component: "file1.opt.yaml")
3885+
let optRecord2 = path.appending(component: "file2.opt.yaml")
3886+
3887+
try localFileSystem.writeFileContents(outputFileMap) {
3888+
$0.send("""
3889+
{
3890+
"\(file1.pathString)": {
3891+
"object": "\(path.appending(component: "file1.o").pathString)",
3892+
"yaml-opt-record": "\(optRecord1.pathString)"
3893+
},
3894+
"\(file2.pathString)": {
3895+
"object": "\(path.appending(component: "file2.o").pathString)",
3896+
"yaml-opt-record": "\(optRecord2.pathString)"
3897+
}
3898+
}
3899+
""")
3900+
}
3901+
3902+
try localFileSystem.writeFileContents(file1) { $0.send("func foo() {}") }
3903+
try localFileSystem.writeFileContents(file2) { $0.send("func bar() {}") }
3904+
3905+
// Test primary file mode with output file map containing optimization record entries
3906+
var driver = try Driver(args: [
3907+
"swiftc", "-save-optimization-record",
3908+
"-output-file-map", outputFileMap.pathString,
3909+
"-c", file1.pathString, file2.pathString
3910+
])
3911+
let plannedJobs = try driver.planBuild()
3912+
let compileJobs = plannedJobs.filter { $0.kind == .compile }
3913+
3914+
XCTAssertEqual(compileJobs.count, 2, "Should have two compile jobs in primary file mode")
3915+
3916+
for (index, compileJob) in compileJobs.enumerated() {
3917+
XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")),
3918+
"Compile job \(index) should have -save-optimization-record-path flag")
3919+
3920+
if let primaryFileIndex = compileJob.commandLine.firstIndex(of: .flag("-primary-file")),
3921+
primaryFileIndex + 1 < compileJob.commandLine.count {
3922+
let primaryFile = compileJob.commandLine[primaryFileIndex + 1]
3923+
3924+
if let optRecordIndex = compileJob.commandLine.firstIndex(of: .flag("-save-optimization-record-path")),
3925+
optRecordIndex + 1 < compileJob.commandLine.count {
3926+
let optRecordPath = compileJob.commandLine[optRecordIndex + 1]
3927+
3928+
if case .path(let primaryPath) = primaryFile, case .path(let optPath) = optRecordPath {
3929+
if primaryPath == .absolute(file1) {
3930+
XCTAssertEqual(optPath, .absolute(optRecord1),
3931+
"Compile job with file1.swift as primary should use file1.opt.yaml from output file map")
3932+
} else if primaryPath == .absolute(file2) {
3933+
XCTAssertEqual(optPath, .absolute(optRecord2),
3934+
"Compile job with file2.swift as primary should use file2.opt.yaml from output file map")
3935+
}
3936+
}
3937+
}
3938+
}
3939+
}
3940+
}
3941+
}
3942+
3943+
func testOptimizationRecordConflictingOptions() throws {
3944+
try withTemporaryDirectory { path in
3945+
let outputFileMap = path.appending(component: "outputFileMap.json")
3946+
let file1 = path.appending(component: "file1.swift")
3947+
let optRecord1 = path.appending(component: "file1.opt.yaml")
3948+
let explicitPath = path.appending(component: "explicit.opt.yaml")
3949+
3950+
try localFileSystem.writeFileContents(outputFileMap) {
3951+
$0.send("""
3952+
{
3953+
"\(file1.pathString)": {
3954+
"object": "\(path.appending(component: "file1.o").pathString)",
3955+
"yaml-opt-record": "\(optRecord1.pathString)"
3956+
}
3957+
}
3958+
""")
3959+
}
3960+
3961+
try localFileSystem.writeFileContents(file1) { $0.send("func foo() {}") }
3962+
3963+
// Test that providing both -save-optimization-record-path and file map entry produces a warning
3964+
try assertDriverDiagnostics(args: [
3965+
"swiftc", "-save-optimization-record",
3966+
"-save-optimization-record-path", explicitPath.pathString,
3967+
"-output-file-map", outputFileMap.pathString,
3968+
"-c", file1.pathString
3969+
]) {
3970+
_ = try? $0.planBuild()
3971+
$1.expect(.warning("ignoring -save-optimization-record-path because output file map contains optimization record entries"))
3972+
}
3973+
}
3974+
}
3975+
3976+
func testOptimizationRecordPartialFileMapCoverage() throws {
3977+
try withTemporaryDirectory { path in
3978+
let outputFileMap = path.appending(component: "outputFileMap.json")
3979+
let file1 = path.appending(component: "file1.swift")
3980+
let file2 = path.appending(component: "file2.swift")
3981+
let optRecord1 = path.appending(component: "file1.opt.yaml")
3982+
3983+
try localFileSystem.writeFileContents(outputFileMap) {
3984+
$0.send("""
3985+
{
3986+
"\(file1.pathString)": {
3987+
"object": "\(path.appending(component: "file1.o").pathString)",
3988+
"yaml-opt-record": "\(optRecord1.pathString)"
3989+
},
3990+
"\(file2.pathString)": {
3991+
"object": "\(path.appending(component: "file2.o").pathString)"
3992+
}
3993+
}
3994+
""")
3995+
}
3996+
3997+
try localFileSystem.writeFileContents(file1) { $0.send("func foo() {}") }
3998+
try localFileSystem.writeFileContents(file2) { $0.send("func bar() {}") }
3999+
4000+
// Test primary file mode with partial file map coverage
4001+
var driver = try Driver(args: [
4002+
"swiftc", "-save-optimization-record",
4003+
"-output-file-map", outputFileMap.pathString,
4004+
"-c", file1.pathString, file2.pathString
4005+
])
4006+
let plannedJobs = try driver.planBuild()
4007+
let compileJobs = plannedJobs.filter { $0.kind == .compile }
4008+
4009+
XCTAssertEqual(compileJobs.count, 2, "Should have two compile jobs in primary file mode")
4010+
4011+
// file1 should use the path from the file map, file2 should use a derived path
4012+
for compileJob in compileJobs {
4013+
if let primaryFileIndex = compileJob.commandLine.firstIndex(of: .flag("-primary-file")),
4014+
primaryFileIndex + 1 < compileJob.commandLine.count {
4015+
let primaryFile = compileJob.commandLine[primaryFileIndex + 1]
4016+
4017+
if case .path(let primaryPath) = primaryFile {
4018+
if primaryPath == .absolute(file1) {
4019+
XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")),
4020+
"file1 compile job should have -save-optimization-record-path flag")
4021+
if let optRecordIndex = compileJob.commandLine.firstIndex(of: .flag("-save-optimization-record-path")),
4022+
optRecordIndex + 1 < compileJob.commandLine.count,
4023+
case .path(let optPath) = compileJob.commandLine[optRecordIndex + 1] {
4024+
XCTAssertEqual(optPath, .absolute(optRecord1),
4025+
"file1 should use the optimization record path from the file map")
4026+
}
4027+
} else if primaryPath == .absolute(file2) {
4028+
XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")),
4029+
"file2 compile job should have -save-optimization-record-path flag")
4030+
if let optRecordIndex = compileJob.commandLine.firstIndex(of: .flag("-save-optimization-record-path")),
4031+
optRecordIndex + 1 < compileJob.commandLine.count,
4032+
case .path(let optPath) = compileJob.commandLine[optRecordIndex + 1] {
4033+
XCTAssertNotEqual(optPath, .absolute(optRecord1),
4034+
"file2 should not use file1's optimization record path")
4035+
}
4036+
}
4037+
}
4038+
}
4039+
}
4040+
}
4041+
}
4042+
38044043
func testUpdateCode() throws {
38054044
do {
38064045
var driver = try Driver(args: [

0 commit comments

Comments
 (0)