#include "emit.h" #include "cache.h" #include "diagnostics.h" #include "partition.h" #include "runtime-import.h" #include "target.h" #include "llvm/ADT/SmallString.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringSet.h" #include "llvm/Bitcode/BitcodeReader.h" #include "llvm/Bitcode/BitcodeWriter.h" #include "llvm/Bitcode/BitcodeWriterPass.h" #include "llvm/IR/DiagnosticInfo.h" #include "llvm/IR/DiagnosticPrinter.h" #include "llvm/IR/LegacyPassManager.h" #include "llvm/IR/Module.h" #include "llvm/IR/Verifier.h" #include "llvm/IRReader/IRReader.h" #include "llvm/LTO/LTO.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Support/Caching.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/SHA256.h" #include "llvm/Support/SourceMgr.h" #include "llvm/Support/ThreadPool.h" #include "llvm/Support/Threading.h" #include "llvm/Support/raw_ostream.h" #include "llvm/Target/TargetMachine.h" #include "llvm/TargetParser/Triple.h" #include "llvm/Transforms/Utils/SplitModule.h" #include #include #include #include #include using namespace llvm; namespace scriptc { static constexpr size_t MaxPartitions = 64; std::optional parseEmitOptions(int Argc, char **Argv) { EmitOptions Options; Options.Target = DefaultTarget.str(); for (int I = 2; I < Argc; ++I) { StringRef Arg(Argv[I]); if (!Arg.starts_with("--") || I + 1 >= Argc) return std::nullopt; StringRef Value(Argv[++I]); if (Arg == "--input") Options.Input = Value.str(); else if (Arg == "--output") Options.Outputs.push_back(Value.str()); else if (Arg == "--filetype") Options.FileType = Value.str(); else if (Arg == "--target") Options.Target = Value.str(); else if (Arg == "--opt-level") Options.OptLevel = Value.str(); else if (Arg == "--relocation-model") Options.RelocationModel = Value.str(); else if (Arg == "--diagnostic-format") Options.DiagnosticFormat = Value.str(); else if (Arg == "--source-path") Options.SourcePath = Value.str(); else if (Arg == "--import-bitcode") Options.ImportBitcode.push_back(Value.str()); else if (Arg == "--cache-dir") Options.CacheDirectory = Value.str(); else return std::nullopt; } if (Options.Input.empty() || Options.Outputs.empty()) return std::nullopt; return Options; } static OptimizationLevel optimizationLevel(StringRef Level) { if (Level == "0") return OptimizationLevel::O0; if (Level == "1") return OptimizationLevel::O1; if (Level == "3") return OptimizationLevel::O3; if (Level == "s") return OptimizationLevel::Os; if (Level == "z") return OptimizationLevel::Oz; return OptimizationLevel::O2; } namespace { struct EmitFailure { std::string Code; std::string Message; }; // Library defaults leave SLP vectorization disabled. Enable both // vectorizers for speed builds, without permitting floating-point // reassociation or changing the size/debug optimization policies. PipelineTuningOptions tuningFor(OptimizationLevel Level) { PipelineTuningOptions Tuning; if (Level == OptimizationLevel::O2 || Level == OptimizationLevel::O3) { Tuning.LoopVectorization = true; Tuning.SLPVectorization = true; } return Tuning; } // Owns the analysis managers a pipeline needs for one module. struct Pipeline { LoopAnalysisManager LAM; FunctionAnalysisManager FAM; CGSCCAnalysisManager CGAM; ModuleAnalysisManager MAM; PassBuilder PB; Pipeline(TargetMachine &Machine, OptimizationLevel Level) : PB(&Machine, tuningFor(Level)) { PB.registerModuleAnalyses(MAM); PB.registerCGSCCAnalyses(CGAM); PB.registerFunctionAnalyses(FAM); PB.registerLoopAnalyses(LAM); PB.crossRegisterProxies(LAM, FAM, CGAM, MAM); } }; std::optional verify(Module &M, StringRef Code) { std::string Error; raw_string_ostream Stream(Error); if (verifyModule(M, &Stream)) return EmitFailure{Code.str(), Stream.str()}; return std::nullopt; } // Writes code for an optimized module to a unique sibling of OutputPath, // which is appended to Temporaries once it exists. std::optional generateCode(Module &M, TargetMachine &Machine, CodeGenFileType Type, StringRef OutputPath, std::vector> &Temporaries) { SmallString<256> TemporaryPath(OutputPath); TemporaryPath.append(".tmp-%%%%%%"); int TemporaryFd = -1; if (std::error_code EC = sys::fs::createUniqueFile(TemporaryPath, TemporaryFd, TemporaryPath)) return EmitFailure{"output_open_failed", EC.message()}; Temporaries.push_back(TemporaryPath); raw_fd_ostream Output(TemporaryFd, true); legacy::PassManager CodeGeneration; if (Machine.addPassesToEmitFile(CodeGeneration, Output, nullptr, Type)) return EmitFailure{"emission_not_supported", "target does not support the requested file type"}; CodeGeneration.run(M); Output.flush(); if (Output.has_error()) return EmitFailure{"output_write_failed", Output.error().message()}; return std::nullopt; } std::optional publish(StringRef TemporaryPath, StringRef OutputPath) { uint64_t Size = 0; if (std::error_code EC = sys::fs::file_size(TemporaryPath, Size)) return EmitFailure{"output_verify_failed", EC.message()}; if (Size == 0) return EmitFailure{"output_verify_failed", "LLVM emitted an empty file"}; if (std::error_code EC = sys::fs::rename(TemporaryPath, OutputPath)) return EmitFailure{"output_publish_failed", EC.message()}; return std::nullopt; } std::optional emitModule(Module &M, TargetMachine &Machine, OptimizationLevel Level, CodeGenFileType Type, StringRef OutputPath, std::vector> &Temporaries) { Pipeline P(Machine, Level); P.PB.buildPerModuleDefaultPipeline(Level).run(M, P.MAM); if (std::optional Failure = verify(M, "post_optimization_verification_failed")) return Failure; return generateCode(M, Machine, Type, OutputPath, Temporaries); } // Bytes of each partition's module, as LLVM assembly or bitcode, plus the // names of definitions that were externally visible before partitioning. struct PartitionPlan { std::vector Modules; StringSet<> PublicSymbols; }; // Fallback when the input is outside the text partitioner's grammar: parse // the whole module once and split it with LLVM, which makes every local // definition hidden so partitions can reference each other. std::optional splitParsedModule(const EmitOptions &Options, PartitionPlan &Plan) { SMDiagnostic ParseDiagnostic; LLVMContext Context; std::unique_ptr Mod = parseIRFile(Options.Input, ParseDiagnostic, Context); if (!Mod) { std::string Detail; raw_string_ostream Stream(Detail); ParseDiagnostic.print("scriptc-llvm-codegen", Stream); return EmitFailure{"invalid_ir", Stream.str()}; } if (std::optional Failure = verify(*Mod, "verification_failed")) return Failure; for (const GlobalValue &GV : Mod->global_values()) if (!GV.isDeclaration() && !GV.hasLocalLinkage()) Plan.PublicSymbols.insert(GV.getName()); SplitModule(*Mod, static_cast(Options.Outputs.size()), [&](std::unique_ptr Partition) { Plan.Modules.emplace_back(); raw_string_ostream Stream(Plan.Modules.back()); WriteBitcodeToFile(*Partition, Stream); }); if (Plan.Modules.size() != Options.Outputs.size()) return EmitFailure{"partition_failed", "LLVM produced an unexpected number " "of program partitions"}; return std::nullopt; } // Runs Work(I) for every partition on a pool sized to the host. Outputs never // depend on the pool size or scheduling order. void forEachPartition(size_t Count, function_ref Work) { DefaultThreadPool Pool(hardware_concurrency()); for (size_t I = 0; I < Count; ++I) Pool.async([&Work, I] { Work(I); }); Pool.wait(); } class SharedFailure { public: void set(EmitFailure Failure) { std::lock_guard Lock(Mutex); if (!First) First = std::move(Failure); } std::optional take() { return std::move(First); } private: std::mutex Mutex; std::optional First; }; // Parses one partition into its own context with the requested target. Expected> loadPartition(const EmitOptions &Options, StringRef Bytes, size_t Index, LLVMContext &Context, TargetMachine &Machine) { SMDiagnostic Diagnostic; std::string Name = "partition-" + std::to_string(Index); std::unique_ptr Mod = parseIR(MemoryBufferRef(Bytes, Name), Diagnostic, Context); if (!Mod) { std::string Detail; raw_string_ostream Stream(Detail); Diagnostic.print("scriptc-llvm-codegen", Stream); return createStringError(inconvertibleErrorCode(), Stream.str()); } if (!Options.SourcePath.empty()) Mod->setSourceFileName(Options.SourcePath); Mod->setTargetTriple(Triple(Options.Target)); Mod->setDataLayout(Machine.createDataLayout()); if (std::optional Failure = verify(*Mod, "verification_failed")) return createStringError(inconvertibleErrorCode(), Failure->Message); return std::move(Mod); } // A raw_ostream that only hashes what is written to it. class HashingStream : public raw_ostream { public: explicit HashingStream(SHA256 &Hash) : Hash(Hash) { SetUnbuffered(); } private: void write_impl(const char *Data, size_t Size) override { Hash.update(StringRef(Data, Size)); Position += Size; } uint64_t current_pos() const override { return Position; } SHA256 &Hash; uint64_t Position = 0; }; // Collects one native object per partition in memory. class ObjectStream : public CachedFileStream { public: explicit ObjectStream(SmallVector &Buffer) : CachedFileStream(std::make_unique(Buffer)) {} }; // Unoptimized partitions are independent: each compiles, or comes from the // cache keyed by its exact source, on its own thread. std::optional compileUnoptimizedPartitions(const EmitOptions &Options, const PartitionPlan &Plan, StringRef Salt, const ArtifactCache &Cache, std::vector> &Objects) { SharedFailure Failures; forEachPartition(Plan.Modules.size(), [&](size_t I) { std::string Key = hashParts({Salt, "object", Plan.Modules[I]}); if (std::unique_ptr Hit = Cache.lookup(Key, "o")) { Objects[I].assign(Hit->getBufferStart(), Hit->getBufferEnd()); return; } std::string LookupError; std::unique_ptr Machine = createTargetMachine(Options.Target, Options.OptLevel, LookupError); if (!Machine) return Failures.set({"target_machine_failed", LookupError}); LLVMContext Context; Expected> Mod = loadPartition(Options, Plan.Modules[I], I, Context, *Machine); if (!Mod) return Failures.set({"partition_failed", toString(Mod.takeError())}); // The O0 pipeline still lowers coroutines and always-inline calls. Pipeline P(*Machine, OptimizationLevel::O0); P.PB.buildPerModuleDefaultPipeline(OptimizationLevel::O0) .run(**Mod, P.MAM); raw_svector_ostream Output(Objects[I]); legacy::PassManager CodeGeneration; if (Machine->addPassesToEmitFile(CodeGeneration, Output, nullptr, CodeGenFileType::ObjectFile)) return Failures.set({"emission_not_supported", "target does not support object emission"}); CodeGeneration.run(**Mod); Cache.store(Key, "o", StringRef(Objects[I].data(), Objects[I].size())); }); return Failures.take(); } // Optimized partitions follow the ThinLTO model. Each partition is simplified // on its own thread and summarized; a thin link over the summaries decides // which small functions every partition imports from the others for // inlining and which promoted symbols return to internal linkage; then each // partition is optimized and compiled on its own thread. Simplified bitcode // is cached by the partition's exact source, and objects by the exact module // after importing, so an edit recompiles only the partitions it can affect. std::optional compileOptimizedPartitions(const EmitOptions &Options, const PartitionPlan &Plan, OptimizationLevel Level, StringRef Salt, const ArtifactCache &Cache, std::vector> &Objects) { size_t Count = Plan.Modules.size(); std::vector> Summaries(Count); SharedFailure Failures; forEachPartition(Count, [&](size_t I) { std::string Key = hashParts({Salt, "summary", Plan.Modules[I]}); if ((Summaries[I] = Cache.lookup(Key, "bc"))) return; std::string LookupError; std::unique_ptr Machine = createTargetMachine(Options.Target, Options.OptLevel, LookupError); if (!Machine) return Failures.set({"target_machine_failed", LookupError}); LLVMContext Context; Expected> Loaded = loadPartition(Options, Plan.Modules[I], I, Context, *Machine); if (!Loaded) return Failures.set({"partition_failed", toString(Loaded.takeError())}); Module &Mod = **Loaded; if (!Options.ImportBitcode.empty()) { // See emit(): the attribute is inert without a sanitizer pass and // would block inlining of the imported runtime bodies. for (Function &F : Mod) F.removeFnAttr(Attribute::SanitizeAddress); if (std::optional Error = importRuntimeBitcode(Mod, Options.ImportBitcode)) return Failures.set({"runtime_import_failed", *Error}); if (std::optional Failure = verify(Mod, "runtime_import_verification_failed")) return Failures.set(std::move(*Failure)); } SmallVector Bitcode; { raw_svector_ostream Stream(Bitcode); Pipeline P(*Machine, Level); ModulePassManager MPM = P.PB.buildThinLTOPreLinkDefaultPipeline(Level); MPM.addPass(BitcodeWriterPass(Stream, /*ShouldPreserveUseListOrder=*/false, /*EmitSummaryIndex=*/true, /*EmitModuleHash=*/true)); MPM.run(Mod, P.MAM); } StringRef Contents(Bitcode.data(), Bitcode.size()); Cache.store(Key, "bc", Contents); Summaries[I] = MemoryBuffer::getMemBufferCopy(Contents); }); if (std::optional Failure = Failures.take()) return Failure; lto::Config Config; Triple TargetTriple(Options.Target); Config.CPU = "generic"; Config.Options = targetOptions(TargetTriple); Config.RelocModel = Reloc::PIC_; Config.CodeModel = CodeModel::Small; Config.CGOptLevel = codeGenLevel(Options.OptLevel); Config.OptLevel = Level == OptimizationLevel::O1 ? 1 : Level == OptimizationLevel::O3 ? 3 : 2; Config.PTO = tuningFor(Level); Config.CGFileType = CodeGenFileType::ObjectFile; Config.DefaultTriple = Options.Target; Config.DiagHandler = [&](const DiagnosticInfo &Info) { if (Info.getSeverity() != DS_Error) return; std::string Message; raw_string_ostream Stream(Message); DiagnosticPrinterRawOStream Printer(Stream); Info.print(Printer); Failures.set({"partition_failed", Stream.str()}); }; std::vector ObjectKeys(Count); Config.PostImportModuleHook = [&](unsigned Task, const Module &Mod) { size_t I = Task - 1; if (I >= Count) return true; SHA256 Hash; { HashingStream Stream(Hash); WriteBitcodeToFile(Mod, Stream); } std::string Key = hashParts({Salt, "object", toHex(Hash.final(), /*LowerCase=*/true)}); if (std::unique_ptr Hit = Cache.lookup(Key, "o")) { Objects[I].assign(Hit->getBufferStart(), Hit->getBufferEnd()); return false; } ObjectKeys[I] = std::move(Key); return true; }; // The link refers to module identifiers for its whole lifetime. std::vector Names(Count); for (size_t I = 0; I < Count; ++I) Names[I] = "partition-" + std::to_string(I); lto::LTO Link(std::move(Config), lto::createInProcessThinBackend(hardware_concurrency())); for (size_t I = 0; I < Count; ++I) { Expected> Input = lto::InputFile::create( MemoryBufferRef(Summaries[I]->getBuffer(), Names[I])); if (!Input) return EmitFailure{"partition_failed", toString(Input.takeError())}; std::vector Resolutions; for (const lto::InputFile::Symbol &Symbol : (*Input)->symbols()) { lto::SymbolResolution Resolution; if (!Symbol.isUndefined()) { // Partitions define disjoint symbols. Only definitions that were // external before partitioning are visible to the runtime objects // linked beside them; the rest may become internal again. bool Public = Plan.PublicSymbols.contains(Symbol.getIRName()); Resolution.Prevailing = true; Resolution.VisibleToRegularObj = Public; Resolution.FinalDefinitionInLinkageUnit = !Public; } Resolutions.push_back(Resolution); } if (Error Err = Link.add(std::move(*Input), Resolutions)) return EmitFailure{"partition_failed", toString(std::move(Err))}; } Error Err = Link.run([&](unsigned Task, const Twine &) -> Expected> { size_t I = Task - 1; if (I >= Count) return createStringError(inconvertibleErrorCode(), "unexpected regular LTO output"); Objects[I].clear(); return std::make_unique(Objects[I]); }); if (Err) return EmitFailure{"partition_failed", toString(std::move(Err))}; if (std::optional Failure = Failures.take()) return Failure; for (size_t I = 0; I < Count; ++I) if (!ObjectKeys[I].empty()) Cache.store(ObjectKeys[I], "o", StringRef(Objects[I].data(), Objects[I].size())); return std::nullopt; } std::optional compilePartitions(const EmitOptions &Options, const PartitionPlan &Plan, std::vector> &Objects) { // Everything besides a partition's own bytes that determines its // artifacts. The caller's cache directory already identifies the exact // helper build; source content and partition count arrive per module. std::string Salt = hashParts({"scriptc-partition-v1", LLVM_VERSION_STRING, Options.Target, Options.OptLevel, Options.RelocationModel, Options.SourcePath}); for (const std::string &Path : Options.ImportBitcode) { ErrorOr> Bitcode = MemoryBuffer::getFile(Path, /*IsText=*/false, /*RequiresNullTerminator=*/false); if (!Bitcode) return EmitFailure{"runtime_import_failed", Path + ": " + Bitcode.getError().message()}; Salt = hashParts({Salt, (*Bitcode)->getBuffer()}); } ArtifactCache Cache(Options.CacheDirectory); Objects.assign(Plan.Modules.size(), {}); if (Options.OptLevel == "0") return compileUnoptimizedPartitions(Options, Plan, Salt, Cache, Objects); return compileOptimizedPartitions(Options, Plan, optimizationLevel(Options.OptLevel), Salt, Cache, Objects); } // Publishes in-memory partition objects through private sibling files. std::optional writePartitionObjects(const EmitOptions &Options, const std::vector> &Objects, std::vector> &Temporaries) { for (size_t I = 0; I < Objects.size(); ++I) { SmallString<256> TemporaryPath(Options.Outputs[I]); TemporaryPath.append(".tmp-%%%%%%"); int TemporaryFd = -1; if (std::error_code EC = sys::fs::createUniqueFile( TemporaryPath, TemporaryFd, TemporaryPath)) return EmitFailure{"output_open_failed", EC.message()}; Temporaries.push_back(TemporaryPath); raw_fd_ostream Output(TemporaryFd, true); Output.write(Objects[I].data(), Objects[I].size()); Output.close(); if (Output.has_error()) { std::string Message = Output.error().message(); Output.clear_error(); return EmitFailure{"output_write_failed", Message}; } } return std::nullopt; } // Several outputs: divide the program into that many partitions, then // compile them concurrently. The division is a deterministic function of the // module and the partition count, so output never depends on host // parallelism. Splitting the assembly text lets partitions parse // concurrently; any input outside the text partitioner's grammar, or a // partition it produced that does not load, falls back to LLVM's splitter. int emitPartitions(const EmitOptions &Options) { // Every partition may come from the cache, so the optimizer's target // lookups cannot rely on an earlier target machine having registered it. initializeTargets(); std::vector> Objects; std::optional Failure; bool Compiled = false; { ErrorOr> Input = MemoryBuffer::getFile(Options.Input, /*IsText=*/false, /*RequiresNullTerminator=*/false); if (Input) { std::optional Text = partitionProgramText( (*Input)->getBuffer(), static_cast(Options.Outputs.size())); if (Text) { Input->reset(); PartitionPlan Plan{std::move(Text->Sources), std::move(Text->PublicSymbols)}; Compiled = !compilePartitions(Options, Plan, Objects); } } } if (!Compiled) { PartitionPlan Plan; Failure = splitParsedModule(Options, Plan); if (!Failure) Failure = compilePartitions(Options, Plan, Objects); } std::vector> Temporaries; if (!Failure) Failure = writePartitionObjects(Options, Objects, Temporaries); for (size_t I = 0; !Failure && I < Temporaries.size(); ++I) Failure = publish(Temporaries[I], Options.Outputs[I]); if (Failure) { for (const SmallString<256> &Temporary : Temporaries) sys::fs::remove(Temporary); return reportError(Failure->Code, Failure->Message, Options.DiagnosticFormat); } return 0; } } // namespace int emit(const EmitOptions &Options) { if (!supportsTarget(Options.Target)) return reportError("unsupported_target", Twine("unsupported target '") + Options.Target + "' (supported: " + AllowedTargets + ")", Options.DiagnosticFormat); if (Options.FileType != "obj" && Options.FileType != "asm") return reportError("invalid_filetype", "filetype must be obj or asm", Options.DiagnosticFormat); if (Options.OptLevel != "0" && Options.OptLevel != "1" && Options.OptLevel != "2" && Options.OptLevel != "3" && Options.OptLevel != "s" && Options.OptLevel != "z") return reportError("invalid_opt_level", "opt-level must be 0, 1, 2, 3, s, or z", Options.DiagnosticFormat); if (Options.Outputs.size() > MaxPartitions) return reportError("invalid_partitions", Twine("at most ") + Twine(MaxPartitions) + " outputs are supported", Options.DiagnosticFormat); if (Options.Outputs.size() > 1 && (Options.FileType != "obj" || Options.OptLevel == "s" || Options.OptLevel == "z")) return reportError("invalid_partitions", "several outputs require object emission at opt-level " "0, 1, 2, or 3", Options.DiagnosticFormat); if (!Options.CacheDirectory.empty() && Options.Outputs.size() == 1) return reportError("invalid_cache_directory", "a cache directory requires several outputs", Options.DiagnosticFormat); if (Options.RelocationModel != "pic") return reportError("invalid_relocation_model", "only the pic relocation model is supported", Options.DiagnosticFormat); if (!Options.ImportBitcode.empty() && Options.OptLevel == "0") return reportError("invalid_import", "runtime bitcode import requires an optimized build", Options.DiagnosticFormat); if (Options.Outputs.size() > 1) return emitPartitions(Options); SMDiagnostic ParseDiagnostic; LLVMContext Context; std::unique_ptr Mod = parseIRFile(Options.Input, ParseDiagnostic, Context); if (!Mod) { std::string Detail; raw_string_ostream Stream(Detail); ParseDiagnostic.print("scriptc-llvm-codegen", Stream); return reportError("invalid_ir", Stream.str(), Options.DiagnosticFormat); } if (!Options.SourcePath.empty()) Mod->setSourceFileName(Options.SourcePath); std::string LookupError; std::unique_ptr Machine = createTargetMachine(Options.Target, Options.OptLevel, LookupError); if (!Machine) return reportError("target_machine_failed", LookupError, Options.DiagnosticFormat); Triple TargetTriple(Options.Target); Mod->setTargetTriple(TargetTriple); Mod->setDataLayout(Machine->createDataLayout()); std::string VerificationError; raw_string_ostream VerificationStream(VerificationError); if (verifyModule(*Mod, &VerificationStream)) return reportError("verification_failed", VerificationStream.str(), Options.DiagnosticFormat); // Runtime bitcode import is the `speed` posture's opt-in; without it the // module reaches the pipeline exactly as emitted. if (!Options.ImportBitcode.empty()) { // The emitter marks every function sanitize_address so the sanitized // lane's clang link can instrument it. This helper never runs a // sanitizer pass, so the attribute is inert here except that it // suppresses speculative loads and blocks inlining of runtime bodies, // whose sanitizer attributes must match. Dropping it does not change // program semantics. for (Function &F : *Mod) F.removeFnAttr(Attribute::SanitizeAddress); if (std::optional Error = importRuntimeBitcode(*Mod, Options.ImportBitcode)) return reportError("runtime_import_failed", *Error, Options.DiagnosticFormat); std::string ImportError; raw_string_ostream ImportStream(ImportError); if (verifyModule(*Mod, &ImportStream)) return reportError("runtime_import_verification_failed", ImportStream.str(), Options.DiagnosticFormat); } OptimizationLevel Level = optimizationLevel(Options.OptLevel); CodeGenFileType Type = Options.FileType == "obj" ? CodeGenFileType::ObjectFile : CodeGenFileType::AssemblyFile; std::vector> Temporaries; std::optional Failure = emitModule( *Mod, *Machine, Level, Type, Options.Outputs[0], Temporaries); if (!Failure) Failure = publish(Temporaries[0], Options.Outputs[0]); if (Failure) { for (const SmallString<256> &Temporary : Temporaries) sys::fs::remove(Temporary); return reportError(Failure->Code, Failure->Message, Options.DiagnosticFormat); } return 0; } } // namespace scriptc