Skip to content

Commit 569f448

Browse files
committed
Fix optimization record path handling in primary file compilation mode
When using -save-optimization-record-path in primary file mode, the user provided path was being ignored and used a derived path instead. -save-optimization-record-path was working correctly in WMO mode due to taking a different code path. rdar://164884975
1 parent f09b136 commit 569f448

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
@@ -3775,6 +3775,245 @@ final class SwiftDriverTests: XCTestCase {
37753775
try checkSupplementaryOutputFileMap(format: "bitstream", .bitstreamOptimizationRecord)
37763776
}
37773777

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

0 commit comments

Comments
 (0)