Skip to content

Commit e65e572

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 83f8460 commit e65e572

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
@@ -717,10 +717,22 @@ extension Driver {
717717
input: TypedVirtualPath?,
718718
flag: String
719719
) throws {
720-
// Handle directory-based options and file maps for SIL and LLVM IR when finalOutputPath is nil
721-
if finalOutputPath == nil && (outputType == .sil || outputType == .llvmIR) {
722-
let directoryOption: Option = outputType == .sil ? .silOutputDir : .irOutputDir
723-
let directory = parsedOptions.getLastArgument(directoryOption)?.asSingle
720+
// Handle directory-based options and file maps for SIL, LLVM IR, and optimization records when finalOutputPath is nil
721+
if finalOutputPath == nil && (outputType == .sil || outputType == .llvmIR || outputType.isOptimizationRecord) {
722+
let directoryOption: Option?
723+
switch outputType {
724+
case .sil:
725+
directoryOption = .silOutputDir
726+
case .llvmIR:
727+
directoryOption = .irOutputDir
728+
case .yamlOptimizationRecord, .bitstreamOptimizationRecord:
729+
// Optimization records don't have a directory option
730+
directoryOption = nil
731+
default:
732+
fatalError("Unexpected output type")
733+
}
734+
735+
let directory = directoryOption.flatMap { parsedOptions.getLastArgument($0)?.asSingle }
724736
let hasFileMapEntries = outputFileMap?.hasEntries(for: outputType) ?? false
725737

726738
if directory != nil || hasFileMapEntries || (parsedOptions.hasArgument(.saveTemps) && !hasFileMapEntries) {
@@ -747,11 +759,17 @@ extension Driver {
747759
// use the final output.
748760
let outputPath: VirtualPath.Handle
749761
if let input = input {
762+
// Check if the output file map has an entry for this specific input and output type
750763
if let outputFileMapPath = try outputFileMap?.existingOutput(inputFile: input.fileHandle, outputType: outputType) {
751764
outputPath = outputFileMapPath
752765
} else if let output = inputOutputMap[input]?.first, output.file != .standardOutput, compilerOutputType != nil {
753-
// Alongside primary output
754-
outputPath = try output.file.replacingExtension(with: outputType).intern()
766+
// For optimization records with an explicit final output path and no file map entry, use the final output path
767+
if outputType.isOptimizationRecord {
768+
outputPath = finalOutputPath
769+
} else {
770+
// Otherwise, derive path alongside primary output
771+
outputPath = try output.file.replacingExtension(with: outputType).intern()
772+
}
755773
} else {
756774
outputPath = try VirtualPath.createUniqueTemporaryFile(RelativePath(validating: input.file.basenameWithoutExt.appendingFileTypeExtension(outputType))).intern()
757775
}
@@ -811,22 +829,18 @@ extension Driver {
811829
input: input,
812830
flag: "-emit-reference-dependencies-path")
813831

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

826-
// Add SIL and IR outputs when explicitly requested via directory options, file maps, or -save-temps
838+
// Add SIL, IR, and optimization record outputs when explicitly requested via directory options, file maps, or -save-temps
827839
let saveTempsWithoutFileMap = parsedOptions.hasArgument(.saveTemps) && outputFileMap == nil
828840
let hasSilFileMapEntries = outputFileMap?.hasEntries(for: .sil) ?? false
829841
let hasIrFileMapEntries = outputFileMap?.hasEntries(for: .llvmIR) ?? false
842+
let optRecordType = self.optimizationRecordFileType ?? .yamlOptimizationRecord
843+
let hasOptRecordFileMapEntries = outputFileMap?.hasEntries(for: optRecordType) ?? false
830844

831845
let silOutputPathSupported = Driver.isOptionFound("-sil-output-path", allOpts: supportedFrontendFlags)
832846
let irOutputPathSupported = Driver.isOptionFound("-ir-output-path", allOpts: supportedFrontendFlags)
@@ -841,6 +855,9 @@ extension Driver {
841855

842856
let shouldAddSilOutput = silOutputPathSupported && (parsedOptions.hasArgument(.silOutputDir) || saveTempsWithoutFileMap || hasSilFileMapEntries)
843857
let shouldAddIrOutput = irOutputPathSupported && (parsedOptions.hasArgument(.irOutputDir) || saveTempsWithoutFileMap || hasIrFileMapEntries)
858+
let shouldAddOptRecordOutput = parsedOptions.hasArgument(.saveOptimizationRecord) ||
859+
parsedOptions.hasArgument(.saveOptimizationRecordEQ) ||
860+
hasOptRecordFileMapEntries
844861

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

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

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

0 commit comments

Comments
 (0)