From 58220ed18dd5d5305e3d190be4def16a2a1b05ad Mon Sep 17 00:00:00 2001 From: Ryan Mansfield Date: Thu, 18 Sep 2025 15:54:52 -0400 Subject: [PATCH 1/2] Add -sil-output-dir and -ir-output-dir driver options This enables generating SIL and LLVM IR files during normal compilation using the frontend -sil-output-path and -ir-output-path flags. Supports both single-file and WMO modes with ile map integration. rdar://160297898 --- Sources/SwiftDriver/Driver/Driver.swift | 24 ++ .../SwiftDriver/Driver/OutputFileMap.swift | 5 + Sources/SwiftDriver/Jobs/CompileJob.swift | 3 +- .../SwiftDriver/Jobs/FrontendJobHelpers.swift | 57 ++++- Sources/SwiftOptions/Options.swift | 4 + Tests/SwiftDriverTests/SwiftDriverTests.swift | 214 ++++++++++++++++++ 6 files changed, 305 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftDriver/Driver/Driver.swift b/Sources/SwiftDriver/Driver/Driver.swift index 5705488c4..294d4c0cc 100644 --- a/Sources/SwiftDriver/Driver/Driver.swift +++ b/Sources/SwiftDriver/Driver/Driver.swift @@ -389,6 +389,12 @@ public struct Driver { /// Path to the Objective-C generated header. let objcGeneratedHeaderPath: VirtualPath.Handle? + /// Path to the SIL output file. + let silOutputPath: VirtualPath.Handle? + + /// Path to the LLVM IR output file. + let llvmIROutputPath: VirtualPath.Handle? + /// Path to the loaded module trace file. let loadedModuleTracePath: VirtualPath.Handle? @@ -1296,6 +1302,11 @@ public struct Driver { emitModuleSeparately: emitModuleSeparately, outputFileMap: self.outputFileMap, moduleName: moduleOutputInfo.name) + // SIL and IR outputs are handled through directory options and file maps + // rather than single output paths, so we set these to nil to enable + // the supplementary output path logic in FrontendJobHelpers + self.silOutputPath = nil + self.llvmIROutputPath = nil if let loadedModuleTraceEnvVar = env["SWIFT_LOADED_MODULE_TRACE_FILE"] { self.loadedModuleTracePath = try VirtualPath.intern(path: loadedModuleTraceEnvVar) @@ -3871,6 +3882,19 @@ extension Driver { return try VirtualPath.intern(path: moduleName.appendingFileTypeExtension(type)) } + /// Check if output file map has entries for SIL/IR to enable file map support + static func hasFileMapEntry(outputFileMap: OutputFileMap?, fileType: FileType) -> Bool { + guard let outputFileMap = outputFileMap else { return false } + + // Check if any input has this file type in the output file map + for inputFile in outputFileMap.entries.keys { + if outputFileMap.entries[inputFile]?[fileType] != nil { + return true + } + } + return false + } + /// Determine if the build system has created a Project/ directory for auxiliary outputs. static func computeProjectDirectoryPath(moduleOutputPath: VirtualPath.Handle?, fileSystem: FileSystem) -> VirtualPath.Handle? { diff --git a/Sources/SwiftDriver/Driver/OutputFileMap.swift b/Sources/SwiftDriver/Driver/OutputFileMap.swift index 08e580df9..a6863d424 100644 --- a/Sources/SwiftDriver/Driver/OutputFileMap.swift +++ b/Sources/SwiftDriver/Driver/OutputFileMap.swift @@ -172,6 +172,11 @@ public struct OutputFileMap: Hashable, Codable { } return result } + + /// Check if the output file map has any entries for the given file type + public func hasEntries(for fileType: FileType) -> Bool { + return entries.values.contains { $0[fileType] != nil } + } } /// Struct for loading the JSON file from disk. diff --git a/Sources/SwiftDriver/Jobs/CompileJob.swift b/Sources/SwiftDriver/Jobs/CompileJob.swift index 3900e22d7..a2952864b 100644 --- a/Sources/SwiftDriver/Jobs/CompileJob.swift +++ b/Sources/SwiftDriver/Jobs/CompileJob.swift @@ -279,7 +279,8 @@ extension Driver { moduleOutputInfo: self.moduleOutputInfo, moduleOutputPaths: self.moduleOutputPaths, includeModuleTracePath: emitModuleTrace, - indexFilePaths: indexFilePaths) + indexFilePaths: indexFilePaths, + allInputs: inputs) // Forward migrator flags. try commandLine.appendLast(.apiDiffDataFile, from: &parsedOptions) diff --git a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift index 2ac5cace7..3098d375b 100644 --- a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift +++ b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift @@ -667,9 +667,33 @@ extension Driver { moduleOutputInfo: ModuleOutputInfo, moduleOutputPaths: SupplementalModuleTargetOutputPaths, includeModuleTracePath: Bool, - indexFilePaths: [TypedVirtualPath]) throws -> [TypedVirtualPath] { + indexFilePaths: [TypedVirtualPath], + allInputs: [TypedVirtualPath] = []) throws -> [TypedVirtualPath] { var flaggedInputOutputPairs: [(flag: String, input: TypedVirtualPath?, output: TypedVirtualPath)] = [] + /// 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 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) + func processInputsForSupplementaryOutput(inputs: [TypedVirtualPath], outputType: FileType, flag: String, directory: String?) throws { + for inputFile in inputs { + // Check output file map first, then fall back to directory-based generation + if let outputFileMapPath = try outputFileMap?.existingOutput(inputFile: inputFile.fileHandle, outputType: outputType) { + flaggedInputOutputPairs.append((flag: flag, input: inputFile, output: TypedVirtualPath(file: outputFileMapPath, type: outputType))) + } else if let directory = directory { + let outputPath = try generateSupplementaryOutputPath(for: inputFile, outputType: outputType, directory: directory) + flaggedInputOutputPairs.append((flag: flag, input: inputFile, output: outputPath)) + } + } + } + /// Add output of a particular type, if needed. func addOutputOfType( outputType: FileType, @@ -677,6 +701,25 @@ extension Driver { input: TypedVirtualPath?, flag: String ) throws { + // Handle directory-based options and file maps for SIL and LLVM IR when finalOutputPath is nil + if finalOutputPath == nil && (outputType == .sil || outputType == .llvmIR) { + let directoryOption: Option = outputType == .sil ? .silOutputDir : .irOutputDir + let directory = parsedOptions.getLastArgument(directoryOption)?.asSingle + let hasFileMapEntries = outputFileMap?.hasEntries(for: outputType) ?? false + + if directory != nil || hasFileMapEntries { + let inputsToProcess: [TypedVirtualPath] + if compilerMode.usesPrimaryFileInputs { + inputsToProcess = input.map { [$0] } ?? [] + } else { + inputsToProcess = allInputs + } + + try processInputsForSupplementaryOutput(inputs: inputsToProcess, outputType: outputType, flag: flag, directory: directory) + return + } + } + // If there is no final output, there's nothing to do. guard let finalOutputPath = finalOutputPath else { return } @@ -763,6 +806,18 @@ extension Driver { finalOutputPath: serializedDiagnosticsFilePath, input: input, flag: "-serialize-diagnostics-path") + + try addOutputOfType( + outputType: .sil, + finalOutputPath: silOutputPath, + input: input, + flag: "-sil-output-path") + + try addOutputOfType( + outputType: .llvmIR, + finalOutputPath: llvmIROutputPath, + input: input, + flag: "-ir-output-path") } if compilerMode.usesPrimaryFileInputs { diff --git a/Sources/SwiftOptions/Options.swift b/Sources/SwiftOptions/Options.swift index 68a3442fc..3c8ed1c7f 100644 --- a/Sources/SwiftOptions/Options.swift +++ b/Sources/SwiftOptions/Options.swift @@ -655,6 +655,7 @@ extension Option { public static let Isystem: Option = Option("-Isystem", .separate, attributes: [.frontend, .synthesizeInterface, .argumentIsPath], helpText: "Add directory to the system import search path") public static let I: Option = Option("-I", .joinedOrSeparate, attributes: [.frontend, .synthesizeInterface, .argumentIsPath], helpText: "Add directory to the import search path") public static let i: Option = Option("-i", .flag, group: .modes) + public static let irOutputDir: Option = Option("-ir-output-dir", .separate, attributes: [.frontend, .argumentIsPath, .supplementaryOutput, .cacheInvariant], metaVar: "", helpText: "Output LLVM IR files to directory as additional output during compilation") public static let json: Option = Option("-json", .flag, attributes: [.noDriver], helpText: "Print output in JSON format.") public static let json_: Option = Option("--json", .flag, alias: Option.json, attributes: [.noDriver], helpText: "Print output in JSON format.") public static let j: Option = Option("-j", .joinedOrSeparate, attributes: [.doesNotAffectIncrementalBuild], metaVar: "", helpText: "Number of commands to execute in parallel") @@ -864,6 +865,7 @@ extension Option { public static let silDebugSerialization: Option = Option("-sil-debug-serialization", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Do not eliminate functions in Mandatory Inlining/SILCombine dead functions. (for debugging only)") public static let silInlineCallerBenefitReductionFactor: Option = Option("-sil-inline-caller-benefit-reduction-factor", .separate, attributes: [.helpHidden, .frontend, .noDriver], metaVar: "<2>", helpText: "Controls the aggressiveness of performance inlining in -Osize mode by reducing the base benefits of a caller (lower value permits more inlining!)") public static let silInlineThreshold: Option = Option("-sil-inline-threshold", .separate, attributes: [.helpHidden, .frontend, .noDriver], metaVar: "<50>", helpText: "Controls the aggressiveness of performance inlining") + public static let silOutputDir: Option = Option("-sil-output-dir", .separate, attributes: [.frontend, .argumentIsPath, .supplementaryOutput, .cacheInvariant], metaVar: "", helpText: "Output SIL files to directory as additional output during compilation") public static let silOwnershipVerifyAll: Option = Option("-sil-ownership-verify-all", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Verify ownership after each transform") public static let silStopOptznsBeforeLoweringOwnership: Option = Option("-sil-stop-optzns-before-lowering-ownership", .flag, attributes: [.helpHidden, .frontend, .noDriver], helpText: "Stop optimizing at SIL time before we lower ownership from SIL. Intended only for SIL ossa tests") public static let silUnrollThreshold: Option = Option("-sil-unroll-threshold", .separate, attributes: [.helpHidden, .frontend, .noDriver], metaVar: "<250>", helpText: "Controls the aggressiveness of loop unrolling") @@ -1610,6 +1612,7 @@ extension Option { Option.importPch, Option.importPrescan, Option.importUnderlyingModule, + Option.irOutputDir, Option.inPlace, Option.inProcessPluginServerPath, Option.includeSpiSymbols, @@ -1849,6 +1852,7 @@ extension Option { Option.silDebugSerialization, Option.silInlineCallerBenefitReductionFactor, Option.silInlineThreshold, + Option.silOutputDir, Option.silOwnershipVerifyAll, Option.silStopOptznsBeforeLoweringOwnership, Option.silUnrollThreshold, diff --git a/Tests/SwiftDriverTests/SwiftDriverTests.swift b/Tests/SwiftDriverTests/SwiftDriverTests.swift index 835649acc..3c5a6786f 100644 --- a/Tests/SwiftDriverTests/SwiftDriverTests.swift +++ b/Tests/SwiftDriverTests/SwiftDriverTests.swift @@ -556,6 +556,220 @@ final class SwiftDriverTests: XCTestCase { } } + func testSupplementarySilAndIrOutputDirectories() throws { + // Test SIL output directory with multiple files (single-file compilation mode) + do { + var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-sil-output-dir", "/tmp/sil-output"]) + let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs() + XCTAssertEqual(plannedJobs.count, 2) + + // First job should generate foo.sil in the output directory + XCTAssertEqual(plannedJobs[0].kind, .compile) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/foo.sil")))) + + // Second job should generate bar.sil in the output directory + XCTAssertEqual(plannedJobs[1].kind, .compile) + try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/bar.sil")))) + } + + // Test IR output directory with multiple files (single-file compilation mode) + do { + var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-ir-output-dir", "/tmp/ir-output"]) + let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs() + XCTAssertEqual(plannedJobs.count, 2) + + // First job should generate foo.ll in the output directory + XCTAssertEqual(plannedJobs[0].kind, .compile) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/foo.ll")))) + + // Second job should generate bar.ll in the output directory + XCTAssertEqual(plannedJobs[1].kind, .compile) + try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/bar.ll")))) + } + + // Test both SIL and IR output directories together (single-file compilation mode) + do { + var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-sil-output-dir", "/tmp/sil-output", "-ir-output-dir", "/tmp/ir-output"]) + let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs() + XCTAssertEqual(plannedJobs.count, 2) + + // First job should generate both foo.sil and foo.ll + XCTAssertEqual(plannedJobs[0].kind, .compile) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/foo.sil")))) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/foo.ll")))) + + // Second job should generate both bar.sil and bar.ll + XCTAssertEqual(plannedJobs[1].kind, .compile) + try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/bar.sil")))) + try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/bar.ll")))) + } + + // Test directory options with single-file compilation + do { + var driver = try Driver(args: ["swiftc", "foo.swift", "-emit-object", "-sil-output-dir", "/tmp/sil-output", "-o", "foo.o"]) + let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs() + XCTAssertEqual(plannedJobs.count, 1) + XCTAssertEqual(plannedJobs[0].kind, .compile) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/foo.sil")))) + } + + // Test directory options with WMO (whole module optimization) + do { + var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-wmo", "-sil-output-dir", "/tmp/sil-output", "-ir-output-dir", "/tmp/ir-output"]) + let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs() + XCTAssertEqual(plannedJobs.count, 1) + XCTAssertEqual(plannedJobs[0].kind, .compile) + + // In WMO mode, should generate output for all input files + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/foo.sil")))) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/sil-output/bar.sil")))) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/foo.ll")))) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/ir-output/bar.ll")))) + } + } + + func testSupplementarySilAndIrOutputFileMaps() throws { + // Test SIL and IR output file maps in WMO mode + try withTemporaryFile { fileMapFile in + let outputMapContents: ByteString = """ + { + "foo.swift": { + "sil": "/tmp/build/foo_custom.sil", + "llvm-ir": "/tmp/build/foo_custom.ll" + }, + "bar.swift": { + "sil": "/tmp/build/bar_custom.sil", + "llvm-ir": "/tmp/build/bar_custom.ll" + } + } + """ + try localFileSystem.writeFileContents(fileMapFile.path, bytes: outputMapContents) + + var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-wmo", + "-output-file-map", fileMapFile.path.pathString]) + let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs() + XCTAssertEqual(plannedJobs.count, 1) + XCTAssertEqual(plannedJobs[0].kind, .compile) + + // In WMO mode with file maps, should generate SIL/IR files at specified paths + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_custom.sil")))) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/bar_custom.sil")))) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_custom.ll")))) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/bar_custom.ll")))) + } + + // Test single-file compilation with file maps + try withTemporaryFile { fileMapFile in + let outputMapContents: ByteString = """ + { + "foo.swift": { + "sil": "/tmp/build/foo_single.sil", + "llvm-ir": "/tmp/build/foo_single.ll" + }, + "bar.swift": { + "sil": "/tmp/build/bar_single.sil", + "llvm-ir": "/tmp/build/bar_single.ll" + } + } + """ + try localFileSystem.writeFileContents(fileMapFile.path, bytes: outputMapContents) + + var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", + "-output-file-map", fileMapFile.path.pathString]) + let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs() + XCTAssertEqual(plannedJobs.count, 2) + + // First job for foo.swift + XCTAssertEqual(plannedJobs[0].kind, .compile) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_single.sil")))) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_single.ll")))) + + // Second job for bar.swift + XCTAssertEqual(plannedJobs[1].kind, .compile) + try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/bar_single.sil")))) + try XCTAssertJobInvocationMatches(plannedJobs[1], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/bar_single.ll")))) + } + + // Test partial file map (only some files have SIL/IR entries) + try withTemporaryFile { fileMapFile in + let outputMapContents: ByteString = """ + { + "foo.swift": { + "sil": "/tmp/build/foo_partial.sil" + }, + "bar.swift": { + "llvm-ir": "/tmp/build/bar_partial.ll" + } + } + """ + try localFileSystem.writeFileContents(fileMapFile.path, bytes: outputMapContents) + + var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-wmo", + "-output-file-map", fileMapFile.path.pathString]) + let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs() + XCTAssertEqual(plannedJobs.count, 1) + XCTAssertEqual(plannedJobs[0].kind, .compile) + + // Should only generate files for entries that exist in the file map + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_partial.sil")))) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/bar_partial.ll")))) + } + + // Test that file maps work alongside directory options (file maps should take precedence) + try withTemporaryFile { fileMapFile in + let outputMapContents: ByteString = """ + { + "foo.swift": { + "sil": "/tmp/build/foo_precedence.sil", + "llvm-ir": "/tmp/build/foo_precedence.ll" + } + } + """ + try localFileSystem.writeFileContents(fileMapFile.path, bytes: outputMapContents) + + var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-wmo", + "-sil-output-dir", "/tmp/dir-output", + "-ir-output-dir", "/tmp/dir-output", + "-output-file-map", fileMapFile.path.pathString]) + let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs() + XCTAssertEqual(plannedJobs.count, 1) + XCTAssertEqual(plannedJobs[0].kind, .compile) + + // foo.swift should use file map paths (precedence over directory options) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_precedence.sil")))) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/build/foo_precedence.ll")))) + + // bar.swift should fall back to directory options + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-sil-output-path"), .path(.absolute(.init(validating: "/tmp/dir-output/bar.sil")))) + try XCTAssertJobInvocationMatches(plannedJobs[0], .flag("-ir-output-path"), .path(.absolute(.init(validating: "/tmp/dir-output/bar.ll")))) + } + + // Ensure file maps without SIL/IR entries don't generate spurious flags + try withTemporaryFile { fileMapFile in + let outputMapContents: ByteString = """ + { + "foo.swift": { + "object": "/tmp/build/foo.o" + }, + "bar.swift": { + "object": "/tmp/build/bar.o" + } + } + """ + try localFileSystem.writeFileContents(fileMapFile.path, bytes: outputMapContents) + + var driver = try Driver(args: ["swiftc", "foo.swift", "bar.swift", "-emit-object", "-wmo", + "-output-file-map", fileMapFile.path.pathString]) + let plannedJobs = try driver.planBuild().removingAutolinkExtractJobs() + XCTAssertEqual(plannedJobs.count, 1) + XCTAssertEqual(plannedJobs[0].kind, .compile) + + // Should not generate any SIL/IR flags when file map has no SIL/IR entries + XCTAssertFalse(plannedJobs[0].commandLine.contains(Job.ArgTemplate.flag("-sil-output-path"))) + XCTAssertFalse(plannedJobs[0].commandLine.contains(Job.ArgTemplate.flag("-ir-output-path"))) + } + } + func testMultithreading() throws { XCTAssertNil(try Driver(args: ["swiftc"]).numParallelJobs) From e062312a999b9f5ac621658d256efb56234c8e9d Mon Sep 17 00:00:00 2001 From: Ryan Mansfield Date: Thu, 2 Oct 2025 09:46:28 -0400 Subject: [PATCH 2/2] Add SIL and IR generation support to -save-temps option This extends -save-temps to emit SIL and IR alongside existing temporary files. In the case where an output file map is used with -save-temps, e.g. incremental compilation, only emit the SIL and IR if it has been requested as an output in the filemap. rdar://160297896 --- .../SwiftDriver/Jobs/FrontendJobHelpers.swift | 38 ++++++++++++----- Tests/SwiftDriverTests/JobExecutorTests.swift | 41 ++++++++++++++++++- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift index 3098d375b..def030093 100644 --- a/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift +++ b/Sources/SwiftDriver/Jobs/FrontendJobHelpers.swift @@ -690,6 +690,10 @@ extension Driver { } else if let directory = directory { let outputPath = try generateSupplementaryOutputPath(for: inputFile, outputType: outputType, directory: directory) flaggedInputOutputPairs.append((flag: flag, input: inputFile, output: outputPath)) + } else if parsedOptions.hasArgument(.saveTemps) { + // When using -save-temps without explicit directories, output to current directory + let outputPath = try generateSupplementaryOutputPath(for: inputFile, outputType: outputType, directory: ".") + flaggedInputOutputPairs.append((flag: flag, input: inputFile, output: outputPath)) } } } @@ -707,7 +711,7 @@ extension Driver { let directory = parsedOptions.getLastArgument(directoryOption)?.asSingle let hasFileMapEntries = outputFileMap?.hasEntries(for: outputType) ?? false - if directory != nil || hasFileMapEntries { + if directory != nil || hasFileMapEntries || (parsedOptions.hasArgument(.saveTemps) && !hasFileMapEntries) { let inputsToProcess: [TypedVirtualPath] if compilerMode.usesPrimaryFileInputs { inputsToProcess = input.map { [$0] } ?? [] @@ -807,17 +811,29 @@ extension Driver { input: input, flag: "-serialize-diagnostics-path") - try addOutputOfType( - outputType: .sil, - finalOutputPath: silOutputPath, - input: input, - flag: "-sil-output-path") + // Add SIL and IR outputs when explicitly requested via directory options, file maps, or -save-temps + let saveTempsWithoutFileMap = parsedOptions.hasArgument(.saveTemps) && outputFileMap == nil + let hasSilFileMapEntries = outputFileMap?.hasEntries(for: .sil) ?? false + let hasIrFileMapEntries = outputFileMap?.hasEntries(for: .llvmIR) ?? false - try addOutputOfType( - outputType: .llvmIR, - finalOutputPath: llvmIROutputPath, - input: input, - flag: "-ir-output-path") + let shouldAddSilOutput = parsedOptions.hasArgument(.silOutputDir) || saveTempsWithoutFileMap || hasSilFileMapEntries + let shouldAddIrOutput = parsedOptions.hasArgument(.irOutputDir) || saveTempsWithoutFileMap || hasIrFileMapEntries + + if shouldAddSilOutput { + try addOutputOfType( + outputType: .sil, + finalOutputPath: silOutputPath, + input: input, + flag: "-sil-output-path") + } + + if shouldAddIrOutput { + try addOutputOfType( + outputType: .llvmIR, + finalOutputPath: llvmIROutputPath, + input: input, + flag: "-ir-output-path") + } } if compilerMode.usesPrimaryFileInputs { diff --git a/Tests/SwiftDriverTests/JobExecutorTests.swift b/Tests/SwiftDriverTests/JobExecutorTests.swift index 22daf594b..436af9a46 100644 --- a/Tests/SwiftDriverTests/JobExecutorTests.swift +++ b/Tests/SwiftDriverTests/JobExecutorTests.swift @@ -536,8 +536,12 @@ final class JobExecutorTests: XCTestCase { executor: executor) let jobs = try driver.planBuild() XCTAssertEqual(jobs.removingAutolinkExtractJobs().map(\.kind), [.compile, .link]) - XCTAssertEqual(jobs[0].outputs.count, 1) - let compileOutput = jobs[0].outputs[0].file + // With -save-temps, we now have additional SIL and IR outputs, so expect more outputs + XCTAssertTrue(jobs[0].outputs.count >= 1, "Should have at least the object file output") + // Find the main object file output + let objectOutput = jobs[0].outputs.first { $0.type == .object } + XCTAssertNotNil(objectOutput, "Should have object file output") + let compileOutput = objectOutput!.file guard matchTemporary(compileOutput, "main.o") else { XCTFail("unexpected output") return @@ -585,5 +589,38 @@ final class JobExecutorTests: XCTestCase { ) } } + + // Test that -save-temps also saves SIL and IR intermediate files + do { + try withTemporaryDirectory { path in + let main = path.appending(component: "main.swift") + try localFileSystem.writeFileContents(main, bytes: "print(\"hello, world!\")") + let diags = DiagnosticsEngine() + let executor = try SwiftDriverExecutor(diagnosticsEngine: diags, + processSet: ProcessSet(), + fileSystem: localFileSystem, + env: ProcessEnv.block) + let outputPath = path.appending(component: "finalOutput") + var driver = try Driver(args: ["swiftc", main.pathString, + "-save-temps", + "-o", outputPath.pathString] + getHostToolchainSdkArg(executor), + envBlock: ProcessEnv.block, + diagnosticsOutput: .engine(diags), + fileSystem: localFileSystem, + executor: executor) + let jobs = try driver.planBuild() + let compileJobs = jobs.removingAutolinkExtractJobs() + XCTAssertEqual(compileJobs.map(\.kind), [.compile, .link]) + + let compileJob = compileJobs[0] + + XCTAssertTrue(compileJob.commandLine.contains(.flag("-sil-output-path"))) + XCTAssertTrue(compileJob.commandLine.contains(.flag("-ir-output-path"))) + + // Verify the compile job has additional outputs for SIL and IR + let hasMultipleOutputs = compileJob.outputs.count > 1 + XCTAssertTrue(hasMultipleOutputs, "Should have additional SIL/IR outputs when using -save-temps") + } + } } }