diff --git a/Sources/SwiftDriver/Driver/OutputFileMap.swift b/Sources/SwiftDriver/Driver/OutputFileMap.swift index a6863d424..90a10ad0a 100644 --- a/Sources/SwiftDriver/Driver/OutputFileMap.swift +++ b/Sources/SwiftDriver/Driver/OutputFileMap.swift @@ -134,9 +134,65 @@ public struct OutputFileMap: Hashable, Codable { var outputFileMap = OutputFileMap() outputFileMap.entries = try result.toVirtualOutputFileMap() + outputFileMap.fixDuplicateOptimizationRecordPaths(diagnosticEngine: diagnosticEngine) + return outputFileMap } + /// Detect and fix duplicate optimization record paths in the output file map. + /// When multiple inputs point to the same optimization record file, synthesize unique paths. + private mutating func fixDuplicateOptimizationRecordPaths(diagnosticEngine: DiagnosticsEngine) { + var optRecordPaths: [VirtualPath.Handle: [VirtualPath.Handle]] = [:] + + for (inputFile, outputs) in entries { + for outputType in [FileType.yamlOptimizationRecord, FileType.bitstreamOptimizationRecord] { + if let optRecordPath = outputs[outputType] { + optRecordPaths[optRecordPath, default: []].append(inputFile) + } + } + } + + // Fix duplicates by synthesizing unique paths + for (optRecordPath, inputFiles) in optRecordPaths where inputFiles.count > 1 { + let pathName = VirtualPath.lookup(optRecordPath).basename + diagnosticEngine.emit(.warning( + "output file map contains duplicate optimization record path '\(pathName)' for \(inputFiles.count) inputs; synthesizing unique paths" + )) + + // Use first input path as-is and synthesize for the rest + for (index, inputFile) in inputFiles.enumerated() where index > 0 { + guard var outputs = entries[inputFile] else { continue } + + let outputType: FileType + if outputs[.yamlOptimizationRecord] == optRecordPath { + outputType = .yamlOptimizationRecord + } else if outputs[.bitstreamOptimizationRecord] == optRecordPath { + outputType = .bitstreamOptimizationRecord + } else { + continue + } + + let optRecordPathVirtual = VirtualPath.lookup(optRecordPath) + let inputFileVirtual = VirtualPath.lookup(inputFile) + let inputBaseName = inputFileVirtual.basenameWithoutExt + + let filename = optRecordPathVirtual.basename + let synthesized: String + if let firstDotIndex = filename.firstIndex(of: ".") { + let baseName = String(filename[.. 1 && inputs.count > 1 && + optRecordPathCount > 0 && optRecordPathCount < inputs.count && !hasOptRecordFileMapEntries { + diagnosticEngine.emit(.error_single_opt_record_path_with_multi_threaded_wmo) + throw ErrorDiagnostics.emitted + } + + // If we have N explicit optimization record paths for N files, collect them + var explicitOptRecordPaths: [VirtualPath.Handle]? = nil + if optRecordPathCount == inputs.count && !hasOptRecordFileMapEntries { + let allPaths = parsedOptions.arguments(for: .saveOptimizationRecordPath) + + // In multi-threaded WMO, all paths go to the single job + if !compilerMode.usesPrimaryFileInputs && numThreads > 1 { + for optPath in allPaths { + commandLine.appendFlag("-save-optimization-record-path") + try commandLine.appendPath(VirtualPath(path: optPath.argument.asSingle)) + } + } + // In non-WMO mode, collect paths to pass to addFrontendSupplementaryOutputArguments + else if compilerMode.usesPrimaryFileInputs { + explicitOptRecordPaths = try allPaths.map { try VirtualPath(path: $0.argument.asSingle).intern() } + } + } + outputs += try addFrontendSupplementaryOutputArguments( commandLine: &commandLine, primaryInputs: primaryInputs, @@ -280,7 +318,8 @@ extension Driver { moduleOutputPaths: self.moduleOutputPaths, includeModuleTracePath: emitModuleTrace, indexFilePaths: indexFilePaths, - allInputs: inputs) + allInputs: inputs, + explicitOptRecordPaths: explicitOptRecordPaths) // Forward migrator flags. try commandLine.appendLast(.apiDiffDataFile, from: &parsedOptions) diff --git a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift index 7deaaa528..f18e3eae8 100644 --- a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift +++ b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift @@ -682,30 +682,77 @@ extension Driver { moduleOutputPaths: SupplementalModuleTargetOutputPaths, includeModuleTracePath: Bool, indexFilePaths: [TypedVirtualPath], - allInputs: [TypedVirtualPath] = []) throws -> [TypedVirtualPath] { + allInputs: [TypedVirtualPath] = [], + explicitOptRecordPaths: [VirtualPath.Handle]? = nil) throws -> [TypedVirtualPath] { var flaggedInputOutputPairs: [(flag: String, input: TypedVirtualPath?, output: TypedVirtualPath)] = [] + // Create mapping from input file to explicit opt-record path + var explicitOptRecordPathMap: [VirtualPath.Handle: VirtualPath.Handle] = [:] + if let paths = explicitOptRecordPaths { + let swiftInputs = allInputs.filter {$0.type.isPartOfSwiftCompilation} + for (index, input) in swiftInputs.enumerated() where index < paths.count { + explicitOptRecordPathMap[input.file.intern()] = paths[index] + } + } + /// Generate directory-based output path for supplementary outputs func generateSupplementaryOutputPath(for input: TypedVirtualPath, outputType: FileType, directory: String) throws -> TypedVirtualPath { let inputBasename = input.file.basenameWithoutExt - let fileExtension = outputType == .sil ? "sil" : "ll" + let fileExtension: String + switch outputType { + case .sil: + fileExtension = "sil" + case .llvmIR: + fileExtension = "ll" + case .yamlOptimizationRecord: + fileExtension = "opt.yaml" + case .bitstreamOptimizationRecord: + fileExtension = "opt.bitstream" + default: + fileExtension = outputType.rawValue + } let filename = "\(inputBasename).\(fileExtension)" let individualPath = try VirtualPath(path: directory).appending(component: filename) let outputPath = individualPath.intern() return TypedVirtualPath(file: outputPath, type: outputType) } - /// Process inputs for supplementary output generation (SIL/IR) + /// Synthesize a per-file path from a module-level path + func synthesizePerFilePath(from modulePath: VirtualPath.Handle, for input: TypedVirtualPath, outputType: FileType) throws -> VirtualPath.Handle { + let modulePathVirtual = VirtualPath.lookup(modulePath) + let inputBaseName = input.file.basenameWithoutExt + + let filename = modulePathVirtual.basename + + if let firstDotIndex = filename.firstIndex(of: ".") { + let baseName = String(filename[.. 1 && + parsedOptions.hasArgument(.saveOptimizationRecord) && + optimizationRecordPath != nil + + // Determine the effective path to use + // Priority: explicit path > output file map entry > multi-threaded WMO auto-generated > module-level path + var effectiveFinalPath: VirtualPath.Handle? = nil + if let inp = input, let explicitPath = explicitOptRecordPathMap[inp.file.intern()] { + effectiveFinalPath = explicitPath + } else if inputHasOptRecordEntry || isMultiThreadedWMOWithAutoGenPaths { + effectiveFinalPath = nil // Use output file map or generate per-file paths + } else { + effectiveFinalPath = optimizationRecordPath // Use module-level path + } + + try addOutputOfType( + outputType: optRecordType, + finalOutputPath: effectiveFinalPath, + input: input, + flag: "-save-optimization-record-path") + } + } + + // Emit warning once if both explicit -save-optimization-record-path and file map entries are provided + let optRecordTypeWarning = self.optimizationRecordFileType ?? .yamlOptimizationRecord + let hasOptRecordFileMapEntriesWarning = outputFileMap?.hasEntries(for: optRecordTypeWarning) ?? false + let hasExplicitOptRecordPath = parsedOptions.hasArgument(.saveOptimizationRecordPath) + if hasOptRecordFileMapEntriesWarning && hasExplicitOptRecordPath { + diagnosticEngine.emit(.warning( + "ignoring -save-optimization-record-path because output file map contains optimization record entries" + )) } if compilerMode.usesPrimaryFileInputs { @@ -950,7 +1064,12 @@ extension Driver { output: TypedVirtualPath(file: tracePath, type: .moduleTrace))) } - if inputsGeneratingCodeCount * FileType.allCases.count > fileListThreshold { + // When we have multiple opt records in flaggedInputOutputPairs, we must use a supplementary + // output file map to pass all the per-file paths to the frontend. + let hasMultipleOptRecords = flaggedInputOutputPairs + .filter { $0.flag == "-save-optimization-record-path" }.count > 1 + + if inputsGeneratingCodeCount * FileType.allCases.count > fileListThreshold || hasMultipleOptRecords { var entries = [VirtualPath.Handle: [FileType: VirtualPath.Handle]]() for input in primaryInputs { if let output = inputOutputMap[input]?.first { diff --git a/Sources/SwiftDriver/Utilities/Diagnostics.swift b/Sources/SwiftDriver/Utilities/Diagnostics.swift index 8b41e2d05..47aab626e 100644 --- a/Sources/SwiftDriver/Utilities/Diagnostics.swift +++ b/Sources/SwiftDriver/Utilities/Diagnostics.swift @@ -182,4 +182,8 @@ extension Diagnostic.Message { static var error_no_objc_interop_embedded: Diagnostic.Message { .error("Objective-C interop cannot be enabled with embedded Swift.") } + + static var error_single_opt_record_path_with_multi_threaded_wmo: Diagnostic.Message { + .error("multi-threaded whole-module optimization requires one '-save-optimization-record-path' per source file") + } } diff --git a/Sources/SwiftDriver/Utilities/FileType.swift b/Sources/SwiftDriver/Utilities/FileType.swift index db5d455d3..935ce7ef4 100644 --- a/Sources/SwiftDriver/Utilities/FileType.swift +++ b/Sources/SwiftDriver/Utilities/FileType.swift @@ -310,6 +310,13 @@ extension FileType { } } +extension FileType { + /// Whether this file type represents an optimization record + public var isOptimizationRecord: Bool { + self == .yamlOptimizationRecord || self == .bitstreamOptimizationRecord + } +} + extension FileType { private static let typesByName = Dictionary(uniqueKeysWithValues: FileType.allCases.map { ($0.name, $0) }) diff --git a/Tests/SwiftDriverTests/SwiftDriverTests.swift b/Tests/SwiftDriverTests/SwiftDriverTests.swift index 457ecc560..304d5d0a3 100644 --- a/Tests/SwiftDriverTests/SwiftDriverTests.swift +++ b/Tests/SwiftDriverTests/SwiftDriverTests.swift @@ -3811,6 +3811,512 @@ final class SwiftDriverTests: XCTestCase { try checkSupplementaryOutputFileMap(format: "bitstream", .bitstreamOptimizationRecord) } + func testOptimizationRecordPathUserProvidedPath() throws { + + do { + var driver = try Driver(args: [ + "swiftc", "-save-optimization-record", "-save-optimization-record-path", "/tmp/test.opt.yaml", + "-c", "test.swift" + ]) + let plannedJobs = try driver.planBuild() + let compileJob = try XCTUnwrap(plannedJobs.first { $0.kind == .compile }) + + XCTAssertTrue(compileJob.commandLine.contains(.path(VirtualPath.absolute(try AbsolutePath(validating: "/tmp/test.opt.yaml"))))) + XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path"))) + } + + // Test primary file mode with multiple files and explicit path + do { + var driver = try Driver(args: [ + "swiftc", "-save-optimization-record", "-save-optimization-record-path", "/tmp/primary.opt.yaml", + "-c", "file1.swift", "file2.swift" + ]) + let plannedJobs = try driver.planBuild() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + XCTAssertEqual(compileJobs.count, 2, "Should have two compile jobs in primary file mode") + + for compileJob in compileJobs { + XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")), + "Each compile job should have -save-optimization-record-path flag") + XCTAssertTrue(compileJob.commandLine.contains(.path(VirtualPath.absolute(try AbsolutePath(validating: "/tmp/primary.opt.yaml")))), + "Each compile job should have the user-provided path") + } + } + + do { + var driver = try Driver(args: [ + "swiftc", "-wmo", "-save-optimization-record", "-save-optimization-record-path", "/tmp/wmo.opt.yaml", + "-c", "test.swift" + ]) + let plannedJobs = try driver.planBuild() + let compileJob = try XCTUnwrap(plannedJobs.first { $0.kind == .compile }) + + XCTAssertTrue(compileJob.commandLine.contains(.path(VirtualPath.absolute(try AbsolutePath(validating: "/tmp/wmo.opt.yaml"))))) + XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path"))) + } + + // Test multithreaded WMO with multiple optimization record paths + do { + var driver = try Driver(args: [ + "swiftc", "-wmo", "-num-threads", "4", "-save-optimization-record", + "-save-optimization-record-path", "/tmp/mt1.opt.yaml", + "-save-optimization-record-path", "/tmp/mt2.opt.yaml", + "-c", "test1.swift", "test2.swift" + ]) + let plannedJobs = try driver.planBuild() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + + XCTAssertGreaterThanOrEqual(compileJobs.count, 1, "Should have at least one compile job") + + var foundPaths: Set = [] + for compileJob in compileJobs { + XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")), + "Each compile job should have -save-optimization-record-path flag") + + if compileJob.commandLine.contains(.path(VirtualPath.absolute(try AbsolutePath(validating: "/tmp/mt1.opt.yaml")))) { + foundPaths.insert("/tmp/mt1.opt.yaml") + } + if compileJob.commandLine.contains(.path(VirtualPath.absolute(try AbsolutePath(validating: "/tmp/mt2.opt.yaml")))) { + foundPaths.insert("/tmp/mt2.opt.yaml") + } + } + + XCTAssertGreaterThanOrEqual(foundPaths.count, 1, + "At least one of the provided optimization record paths should be used") + } + } + + func testOptimizationRecordWithOutputFileMap() throws { + try withTemporaryDirectory { path in + let outputFileMap = path.appending(component: "outputFileMap.json") + let file1 = path.appending(component: "file1.swift") + let file2 = path.appending(component: "file2.swift") + let optRecord1 = path.appending(component: "file1.opt.yaml") + let optRecord2 = path.appending(component: "file2.opt.yaml") + + try localFileSystem.writeFileContents(outputFileMap) { + $0.send(""" + { + "\(file1.pathString)": { + "object": "\(path.appending(component: "file1.o").pathString)", + "yaml-opt-record": "\(optRecord1.pathString)" + }, + "\(file2.pathString)": { + "object": "\(path.appending(component: "file2.o").pathString)", + "yaml-opt-record": "\(optRecord2.pathString)" + } + } + """) + } + + try localFileSystem.writeFileContents(file1) { $0.send("func foo() {}") } + try localFileSystem.writeFileContents(file2) { $0.send("func bar() {}") } + + // Test primary file mode with output file map containing optimization record entries + var driver = try Driver(args: [ + "swiftc", "-save-optimization-record", + "-output-file-map", outputFileMap.pathString, + "-c", file1.pathString, file2.pathString + ]) + let plannedJobs = try driver.planBuild() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + + XCTAssertEqual(compileJobs.count, 2, "Should have two compile jobs in primary file mode") + + for (index, compileJob) in compileJobs.enumerated() { + XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")), + "Compile job \(index) should have -save-optimization-record-path flag") + + if let primaryFileIndex = compileJob.commandLine.firstIndex(of: .flag("-primary-file")), + primaryFileIndex + 1 < compileJob.commandLine.count { + let primaryFile = compileJob.commandLine[primaryFileIndex + 1] + + if let optRecordIndex = compileJob.commandLine.firstIndex(of: .flag("-save-optimization-record-path")), + optRecordIndex + 1 < compileJob.commandLine.count { + let optRecordPath = compileJob.commandLine[optRecordIndex + 1] + + if case .path(let primaryPath) = primaryFile, case .path(let optPath) = optRecordPath { + if primaryPath == .absolute(file1) { + XCTAssertEqual(optPath, .absolute(optRecord1), + "Compile job with file1.swift as primary should use file1.opt.yaml from output file map") + } else if primaryPath == .absolute(file2) { + XCTAssertEqual(optPath, .absolute(optRecord2), + "Compile job with file2.swift as primary should use file2.opt.yaml from output file map") + } + } + } + } + } + } + } + + func testOptimizationRecordConflictingOptions() throws { + try withTemporaryDirectory { path in + let outputFileMap = path.appending(component: "outputFileMap.json") + let file1 = path.appending(component: "file1.swift") + let optRecord1 = path.appending(component: "file1.opt.yaml") + let explicitPath = path.appending(component: "explicit.opt.yaml") + + try localFileSystem.writeFileContents(outputFileMap) { + $0.send(""" + { + "\(file1.pathString)": { + "object": "\(path.appending(component: "file1.o").pathString)", + "yaml-opt-record": "\(optRecord1.pathString)" + } + } + """) + } + + try localFileSystem.writeFileContents(file1) { $0.send("func foo() {}") } + + // Test that providing both -save-optimization-record-path and file map entry produces a warning + try assertDriverDiagnostics(args: [ + "swiftc", "-save-optimization-record", + "-save-optimization-record-path", explicitPath.pathString, + "-output-file-map", outputFileMap.pathString, + "-c", file1.pathString + ]) { + _ = try? $0.planBuild() + $1.expect(.warning("ignoring -save-optimization-record-path because output file map contains optimization record entries")) + } + } + } + + func testOptimizationRecordPartialFileMapCoverage() throws { + try withTemporaryDirectory { path in + let outputFileMap = path.appending(component: "outputFileMap.json") + let file1 = path.appending(component: "file1.swift") + let file2 = path.appending(component: "file2.swift") + let optRecord1 = path.appending(component: "file1.opt.yaml") + + try localFileSystem.writeFileContents(outputFileMap) { + $0.send(""" + { + "\(file1.pathString)": { + "object": "\(path.appending(component: "file1.o").pathString)", + "yaml-opt-record": "\(optRecord1.pathString)" + }, + "\(file2.pathString)": { + "object": "\(path.appending(component: "file2.o").pathString)" + } + } + """) + } + + try localFileSystem.writeFileContents(file1) { $0.send("func foo() {}") } + try localFileSystem.writeFileContents(file2) { $0.send("func bar() {}") } + + // Test primary file mode with partial file map coverage + var driver = try Driver(args: [ + "swiftc", "-save-optimization-record", + "-output-file-map", outputFileMap.pathString, + "-c", file1.pathString, file2.pathString + ]) + let plannedJobs = try driver.planBuild() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + + XCTAssertEqual(compileJobs.count, 2, "Should have two compile jobs in primary file mode") + + // file1 should use the path from the file map, file2 should use a derived path + for compileJob in compileJobs { + if let primaryFileIndex = compileJob.commandLine.firstIndex(of: .flag("-primary-file")), + primaryFileIndex + 1 < compileJob.commandLine.count { + let primaryFile = compileJob.commandLine[primaryFileIndex + 1] + + if case .path(let primaryPath) = primaryFile { + if primaryPath == .absolute(file1) { + XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")), + "file1 compile job should have -save-optimization-record-path flag") + if let optRecordIndex = compileJob.commandLine.firstIndex(of: .flag("-save-optimization-record-path")), + optRecordIndex + 1 < compileJob.commandLine.count, + case .path(let optPath) = compileJob.commandLine[optRecordIndex + 1] { + XCTAssertEqual(optPath, .absolute(optRecord1), + "file1 should use the optimization record path from the file map") + } + } else if primaryPath == .absolute(file2) { + XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")), + "file2 compile job should have -save-optimization-record-path flag") + if let optRecordIndex = compileJob.commandLine.firstIndex(of: .flag("-save-optimization-record-path")), + optRecordIndex + 1 < compileJob.commandLine.count, + case .path(let optPath) = compileJob.commandLine[optRecordIndex + 1] { + XCTAssertNotEqual(optPath, .absolute(optRecord1), + "file2 should not use file1's optimization record path") + } + } + } + } + } + } + } + func testMultiThreadedWMOOptimizationRecordWithOutputFileMap() throws { + try withTemporaryDirectory { path in + let outputFileMap = path.appending(component: "outputFileMap.json") + let source1 = path.appending(component: "source1.swift") + let source2 = path.appending(component: "source2.swift") + let optRecord1 = path.appending(component: "source1.opt.yaml") + let optRecord2 = path.appending(component: "source2.opt.yaml") + + // Test multi-threaded WMO with per-file opt-record entries + try localFileSystem.writeFileContents(outputFileMap) { + $0.send(""" + { + "\(source1.pathString)": { + "object": "\(path.appending(component: "source1.o").pathString)", + "yaml-opt-record": "\(optRecord1.pathString)" + }, + "\(source2.pathString)": { + "object": "\(path.appending(component: "source2.o").pathString)", + "yaml-opt-record": "\(optRecord2.pathString)" + } + } + """) + } + + try localFileSystem.writeFileContents(source1) { $0.send("public func func1() -> Int { return 42 }") } + try localFileSystem.writeFileContents(source2) { $0.send("public func func2() -> String { return \"test\" }") } + + var driver = try Driver(args: [ + "swiftc", "-wmo", "-num-threads", "2", "-O", "-save-optimization-record", + "-output-file-map", outputFileMap.pathString, + "-c", source1.pathString, source2.pathString + ]) + let plannedJobs = try driver.planBuild() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + + XCTAssertGreaterThanOrEqual(compileJobs.count, 1, "Should have at least one compile job for multi-threaded WMO") + + // In multi-threaded WMO with output file map, the driver uses -supplementary-output-file-map + // instead of individual -save-optimization-record-path flags + let compileJob = compileJobs[0] + XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record")), + "Compile job should have -save-optimization-record flag") + XCTAssertTrue(compileJob.commandLine.contains(.flag("-supplementary-output-file-map")), + "Compile job should use -supplementary-output-file-map for per-file paths") + + let optRecordOutputs = compileJob.outputs.filter { $0.type == .yamlOptimizationRecord } + XCTAssertEqual(optRecordOutputs.count, 2, "Should have two optimization record outputs") + XCTAssertTrue(optRecordOutputs.contains(where: { $0.file == VirtualPath.absolute(optRecord1) }), + "Should include source1's opt-record path in outputs") + XCTAssertTrue(optRecordOutputs.contains(where: { $0.file == VirtualPath.absolute(optRecord2) }), + "Should include source2's opt-record path in outputs") + } + } + + func testMultiThreadedWMOOptimizationRecordSwiftPMStyle() throws { + try withTemporaryDirectory { path in + let outputFileMap = path.appending(component: "outputFileMap.json") + let source1 = path.appending(component: "source1.swift") + let source2 = path.appending(component: "source2.swift") + let moduleOptRecord = path.appending(component: "TestModule.opt.yaml") + + // SwiftPM-style: module-level opt-record entry + try localFileSystem.writeFileContents(outputFileMap) { + $0.send(""" + { + "": { + "yaml-opt-record": "\(moduleOptRecord.pathString)" + }, + "\(source1.pathString)": { + "object": "\(path.appending(component: "source1.o").pathString)" + }, + "\(source2.pathString)": { + "object": "\(path.appending(component: "source2.o").pathString)" + } + } + """) + } + + try localFileSystem.writeFileContents(source1) { $0.send("public func func1() -> Int { return 42 }") } + try localFileSystem.writeFileContents(source2) { $0.send("public func func2() -> String { return \"test\" }") } + + var driver = try Driver(args: [ + "swiftc", "-wmo", "-num-threads", "2", "-O", "-module-name", "TestModule", + "-output-file-map", outputFileMap.pathString, + "-c", source1.pathString, source2.pathString + ]) + let plannedJobs = try driver.planBuild() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + + XCTAssertGreaterThanOrEqual(compileJobs.count, 1, "Should have at least one compile job") + + // Synthesizes per-file paths from module-level entry + let compileJob = compileJobs[0] + XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record")), + "Compile job should have -save-optimization-record flag") + XCTAssertTrue(compileJob.commandLine.contains(.flag("-supplementary-output-file-map")), + "Compile job should use -supplementary-output-file-map") + + // Should synthesize: TestModule-source1.opt.yaml and TestModule-source2.opt.yaml + let synthesized1 = path.appending(component: "TestModule-source1.opt.yaml") + let synthesized2 = path.appending(component: "TestModule-source2.opt.yaml") + + let optRecordOutputs = compileJob.outputs.filter { $0.type == .yamlOptimizationRecord } + XCTAssertEqual(optRecordOutputs.count, 2, "Should have two optimization record outputs") + XCTAssertTrue(optRecordOutputs.contains(where: { $0.file == VirtualPath.absolute(synthesized1) }), + "Should include synthesized opt-record path for source1") + XCTAssertTrue(optRecordOutputs.contains(where: { $0.file == VirtualPath.absolute(synthesized2) }), + "Should include synthesized opt-record path for source2") + } + } + + func testMultiThreadedWMOOptimizationRecordExplicitPaths() throws { + try withTemporaryDirectory { path in + let source1 = path.appending(component: "source1.swift") + let source2 = path.appending(component: "source2.swift") + let optRecord1 = path.appending(component: "custom1.opt.yaml") + let optRecord2 = path.appending(component: "custom2.opt.yaml") + + try localFileSystem.writeFileContents(source1) { $0.send("public func func1() -> Int { return 42 }") } + try localFileSystem.writeFileContents(source2) { $0.send("public func func2() -> String { return \"test\" }") } + + // Test multi-threaded WMO with explicit -save-optimization-record-path flags + var driver = try Driver(args: [ + "swiftc", "-wmo", "-num-threads", "2", "-O", "-save-optimization-record", + "-save-optimization-record-path", optRecord1.pathString, + "-save-optimization-record-path", optRecord2.pathString, + "-c", source1.pathString, source2.pathString + ]) + let plannedJobs = try driver.planBuild() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + + XCTAssertGreaterThanOrEqual(compileJobs.count, 1, "Should have at least one compile job") + + let compileJob = compileJobs[0] + XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record-path")), + "Compile job should have -save-optimization-record-path flag") + XCTAssertTrue(compileJob.commandLine.contains(.path(.absolute(optRecord1))), + "Compile job should include first explicit opt-record path") + XCTAssertTrue(compileJob.commandLine.contains(.path(.absolute(optRecord2))), + "Compile job should include second explicit opt-record path") + } + } + + func testMultiThreadedWMOOptimizationRecordBitstreamFormat() throws { + try withTemporaryDirectory { path in + let outputFileMap = path.appending(component: "outputFileMap.json") + let source1 = path.appending(component: "source1.swift") + let source2 = path.appending(component: "source2.swift") + let optRecord1 = path.appending(component: "source1.opt.bitstream") + let optRecord2 = path.appending(component: "source2.opt.bitstream") + + try localFileSystem.writeFileContents(outputFileMap) { + $0.send(""" + { + "\(source1.pathString)": { + "object": "\(path.appending(component: "source1.o").pathString)", + "bitstream-opt-record": "\(optRecord1.pathString)" + }, + "\(source2.pathString)": { + "object": "\(path.appending(component: "source2.o").pathString)", + "bitstream-opt-record": "\(optRecord2.pathString)" + } + } + """) + } + + try localFileSystem.writeFileContents(source1) { $0.send("public func func1() -> Int { return 42 }") } + try localFileSystem.writeFileContents(source2) { $0.send("public func func2() -> String { return \"test\" }") } + + var driver = try Driver(args: [ + "swiftc", "-wmo", "-num-threads", "2", "-O", "-save-optimization-record=bitstream", + "-output-file-map", outputFileMap.pathString, + "-c", source1.pathString, source2.pathString + ]) + let plannedJobs = try driver.planBuild() + let compileJobs = plannedJobs.filter { $0.kind == .compile } + + XCTAssertGreaterThanOrEqual(compileJobs.count, 1, "Should have at least one compile job") + + let compileJob = compileJobs[0] + XCTAssertTrue(compileJob.commandLine.contains(.flag("-save-optimization-record=bitstream")), + "Compile job should have -save-optimization-record=bitstream flag") + XCTAssertTrue(compileJob.commandLine.contains(.flag("-supplementary-output-file-map")), + "Compile job should use -supplementary-output-file-map") + + let optRecordOutputs = compileJob.outputs.filter { $0.type == .bitstreamOptimizationRecord } + XCTAssertEqual(optRecordOutputs.count, 2, "Should have two bitstream optimization record outputs") + XCTAssertTrue(optRecordOutputs.contains(where: { $0.file == VirtualPath.absolute(optRecord1) }), + "Should include source1's bitstream opt-record path") + XCTAssertTrue(optRecordOutputs.contains(where: { $0.file == VirtualPath.absolute(optRecord2) }), + "Should include source2's bitstream opt-record path") + } + } + + + func testOptimizationRecordMultipleProducersError() throws { + try withTemporaryDirectory { path in + // Create multiple source files + let file1 = path.appending(component: "Atomic.swift") + let file2 = path.appending(component: "AtomicBool.swift") + let file3 = path.appending(component: "AtomicCounter.swift") + let file4 = path.appending(component: "AtomicLazyReference.swift") + let file5 = path.appending(component: "AtomicOptionalReference.swift") + let file6 = path.appending(component: "AtomicReference.swift") + + let sourceFiles = [file1, file2, file3, file4, file5, file6] + + for (index, file) in sourceFiles.enumerated() { + try localFileSystem.writeFileContents(file) { $0.send("public func test\(index)() {}") } + } + + // Create output file map where all inputs point to the same optimization record path + let outputFileMapPath = path.appending(component: "output-file-map.json") + var outputFileMapContent = "{\n" + for file in sourceFiles { + let objFile = path.appending(component: file.basenameWithoutExt + ".o") + let optFile = path.appending(component: "TestModule.opt.bitstream") + outputFileMapContent += """ + "\(file.pathString)": { + "object": "\(objFile.pathString)", + "bitstream-opt-record": "\(optFile.pathString)" + }, + + """ + } + outputFileMapContent += "}\n" + + try localFileSystem.writeFileContents(outputFileMapPath) { $0.send(outputFileMapContent) } + + // Multi-threaded compilation (non-WMO) with all inputs pointing to the same opt-record path + var driver = try Driver(args: [ + "swiftc", "-j", "3", "-num-threads", "3", "-O", + "-save-optimization-record=bitstream", + "-output-file-map", outputFileMapPath.pathString, + "-c"] + sourceFiles.map { $0.pathString } + [ + "-module-name", "TestModule" + ]) + + let jobs = try driver.planBuild() + + var outputFilePaths: [String: [Job]] = [:] + for job in jobs { + for output in job.outputs { + let outputPath = output.file.name + if let existingJobs = outputFilePaths[outputPath] { + outputFilePaths[outputPath] = existingJobs + [job] + } else { + outputFilePaths[outputPath] = [job] + } + } + } + + // Verify no output file is produced by multiple jobs (would cause multiple producers error) + var foundMultipleProducers = false + var duplicateFile = "" + for (outputPath, producingJobs) in outputFilePaths { + if producingJobs.count > 1 { + foundMultipleProducers = true + duplicateFile = outputPath + break + } + } + + XCTAssertFalse(foundMultipleProducers, + "Found multiple producers for '\(duplicateFile)' - driver should synthesize unique paths from duplicate entries") + } + } + func testUpdateCode() throws { do { var driver = try Driver(args: [