Skip to content

Commit b4731ad

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.
1 parent c54107d commit b4731ad

File tree

3 files changed

+296
-13
lines changed

3 files changed

+296
-13
lines changed

Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -705,10 +705,22 @@ extension Driver {
705705
input: TypedVirtualPath?,
706706
flag: String
707707
) throws {
708-
// Handle directory-based options and file maps for SIL and LLVM IR when finalOutputPath is nil
709-
if finalOutputPath == nil && (outputType == .sil || outputType == .llvmIR) {
710-
let directoryOption: Option = outputType == .sil ? .silOutputDir : .irOutputDir
711-
let directory = parsedOptions.getLastArgument(directoryOption)?.asSingle
708+
// Handle directory-based options and file maps for SIL, LLVM IR, and optimization records when finalOutputPath is nil
709+
if finalOutputPath == nil && (outputType == .sil || outputType == .llvmIR || outputType.isOptimizationRecord) {
710+
let directoryOption: Option?
711+
switch outputType {
712+
case .sil:
713+
directoryOption = .silOutputDir
714+
case .llvmIR:
715+
directoryOption = .irOutputDir
716+
case .yamlOptimizationRecord, .bitstreamOptimizationRecord:
717+
// Optimization records don't have a directory option
718+
directoryOption = nil
719+
default:
720+
fatalError("Unexpected output type")
721+
}
722+
723+
let directory = directoryOption.flatMap { parsedOptions.getLastArgument($0)?.asSingle }
712724
let hasFileMapEntries = outputFileMap?.hasEntries(for: outputType) ?? false
713725

714726
if directory != nil || hasFileMapEntries || (parsedOptions.hasArgument(.saveTemps) && !hasFileMapEntries) {
@@ -735,11 +747,17 @@ extension Driver {
735747
// use the final output.
736748
let outputPath: VirtualPath.Handle
737749
if let input = input {
750+
// Check if the output file map has an entry for this specific input and output type
738751
if let outputFileMapPath = try outputFileMap?.existingOutput(inputFile: input.fileHandle, outputType: outputType) {
739752
outputPath = outputFileMapPath
740753
} else if let output = inputOutputMap[input]?.first, output.file != .standardOutput, compilerOutputType != nil {
741-
// Alongside primary output
742-
outputPath = try output.file.replacingExtension(with: outputType).intern()
754+
// For optimization records with an explicit final output path and no file map entry, use the final output path
755+
if outputType.isOptimizationRecord {
756+
outputPath = finalOutputPath
757+
} else {
758+
// Otherwise, derive path alongside primary output
759+
outputPath = try output.file.replacingExtension(with: outputType).intern()
760+
}
743761
} else {
744762
outputPath = try VirtualPath.createUniqueTemporaryFile(RelativePath(validating: input.file.basenameWithoutExt.appendingFileTypeExtension(outputType))).intern()
745763
}
@@ -799,22 +817,18 @@ extension Driver {
799817
input: input,
800818
flag: "-emit-reference-dependencies-path")
801819

802-
try addOutputOfType(
803-
outputType: self.optimizationRecordFileType ?? .yamlOptimizationRecord,
804-
finalOutputPath: optimizationRecordPath,
805-
input: input,
806-
flag: "-save-optimization-record-path")
807-
808820
try addOutputOfType(
809821
outputType: .diagnostics,
810822
finalOutputPath: serializedDiagnosticsFilePath,
811823
input: input,
812824
flag: "-serialize-diagnostics-path")
813825

814-
// Add SIL and IR outputs when explicitly requested via directory options, file maps, or -save-temps
826+
// Add SIL, IR, and optimization record outputs when explicitly requested via directory options, file maps, or -save-temps
815827
let saveTempsWithoutFileMap = parsedOptions.hasArgument(.saveTemps) && outputFileMap == nil
816828
let hasSilFileMapEntries = outputFileMap?.hasEntries(for: .sil) ?? false
817829
let hasIrFileMapEntries = outputFileMap?.hasEntries(for: .llvmIR) ?? false
830+
let optRecordType = self.optimizationRecordFileType ?? .yamlOptimizationRecord
831+
let hasOptRecordFileMapEntries = outputFileMap?.hasEntries(for: optRecordType) ?? false
818832

819833
let silOutputPathSupported = Driver.isOptionFound("-sil-output-path", allOpts: supportedFrontendFlags)
820834
let irOutputPathSupported = Driver.isOptionFound("-ir-output-path", allOpts: supportedFrontendFlags)
@@ -829,6 +843,9 @@ extension Driver {
829843

830844
let shouldAddSilOutput = silOutputPathSupported && (parsedOptions.hasArgument(.silOutputDir) || saveTempsWithoutFileMap || hasSilFileMapEntries)
831845
let shouldAddIrOutput = irOutputPathSupported && (parsedOptions.hasArgument(.irOutputDir) || saveTempsWithoutFileMap || hasIrFileMapEntries)
846+
let shouldAddOptRecordOutput = parsedOptions.hasArgument(.saveOptimizationRecord) ||
847+
parsedOptions.hasArgument(.saveOptimizationRecordEQ) ||
848+
hasOptRecordFileMapEntries
832849

833850
if shouldAddSilOutput {
834851
try addOutputOfType(
@@ -845,6 +862,26 @@ extension Driver {
845862
input: input,
846863
flag: "-ir-output-path")
847864
}
865+
866+
if shouldAddOptRecordOutput {
867+
let inputHasOptRecordEntry = input != nil &&
868+
(try? outputFileMap?.existingOutput(inputFile: input!.fileHandle, outputType: optRecordType)) != nil
869+
870+
if hasOptRecordFileMapEntries && optimizationRecordPath != nil {
871+
diagnosticEngine.emit(.warning(
872+
"ignoring -save-optimization-record-path because output file map contains optimization record entries"
873+
))
874+
}
875+
876+
// Pass nil for finalOutputPath when this specific input has a file map entry,
877+
// so that the file map entry will be used. Otherwise, use the explicit path if provided.
878+
let finalPath = inputHasOptRecordEntry ? nil : optimizationRecordPath
879+
try addOutputOfType(
880+
outputType: optRecordType,
881+
finalOutputPath: finalPath,
882+
input: input,
883+
flag: "-save-optimization-record-path")
884+
}
848885
}
849886

850887
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)