Skip to content

[SPIR-V] Expose an API call to initialize SPIRV target and translate input LLVM IR module to SPIR-V #107216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

VyacheslavLevytskyy
Copy link
Contributor

The goal of this PR is to facilitate integration of SPIRV Backend into misc 3rd party tools and libraries by means of exposing an API call that translate LLVM module to SPIR-V and write results into a string as binary SPIR-V output, providing diagnostics on fail and means of configuring translation in a style of command line options.

An example of a use case may be Khronos Translator that provides bidirectional translation LLVM IR <=> SPIR-V, where LLVM IR => SPIR-V step may be substituted by the call to SPIR-V Backend API, implemented by this PR.

Copy link

github-actions bot commented Sep 4, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

@VyacheslavLevytskyy VyacheslavLevytskyy marked this pull request as ready for review September 4, 2024 14:02
@llvmbot
Copy link
Member

llvmbot commented Sep 4, 2024

@llvm/pr-subscribers-backend-spir-v

Author: Vyacheslav Levytskyy (VyacheslavLevytskyy)

Changes

The goal of this PR is to facilitate integration of SPIRV Backend into misc 3rd party tools and libraries by means of exposing an API call that translate LLVM module to SPIR-V and write results into a string as binary SPIR-V output, providing diagnostics on fail and means of configuring translation in a style of command line options.

An example of a use case may be Khronos Translator that provides bidirectional translation LLVM IR <=> SPIR-V, where LLVM IR => SPIR-V step may be substituted by the call to SPIR-V Backend API, implemented by this PR.


Full diff: https://github.com/llvm/llvm-project/pull/107216.diff

5 Files Affected:

  • (modified) llvm/lib/Target/SPIRV/CMakeLists.txt (+1)
  • (added) llvm/lib/Target/SPIRV/SPIRVAPI.cpp (+167)
  • (added) llvm/lib/Target/SPIRV/SPIRVAPI.h (+23)
  • (modified) llvm/unittests/Target/SPIRV/CMakeLists.txt (+2)
  • (added) llvm/unittests/Target/SPIRV/SPIRVAPITest.cpp (+152)
diff --git a/llvm/lib/Target/SPIRV/CMakeLists.txt b/llvm/lib/Target/SPIRV/CMakeLists.txt
index 5f8aea5fc8d84d..df7869b1552caa 100644
--- a/llvm/lib/Target/SPIRV/CMakeLists.txt
+++ b/llvm/lib/Target/SPIRV/CMakeLists.txt
@@ -14,6 +14,7 @@ tablegen(LLVM SPIRVGenTables.inc -gen-searchable-tables)
 add_public_tablegen_target(SPIRVCommonTableGen)
 
 add_llvm_target(SPIRVCodeGen
+  SPIRVAPI.cpp
   SPIRVAsmPrinter.cpp
   SPIRVBuiltins.cpp
   SPIRVCallLowering.cpp
diff --git a/llvm/lib/Target/SPIRV/SPIRVAPI.cpp b/llvm/lib/Target/SPIRV/SPIRVAPI.cpp
new file mode 100644
index 00000000000000..b4ada1947a4888
--- /dev/null
+++ b/llvm/lib/Target/SPIRV/SPIRVAPI.cpp
@@ -0,0 +1,167 @@
+//===-- SPIRVAPI.cpp - SPIR-V Backend API ---------------------*- C++ -*---===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Analysis/TargetLibraryInfo.h"
+#include "llvm/CodeGen/CommandFlags.h"
+#include "llvm/CodeGen/MachineFunctionPass.h"
+#include "llvm/CodeGen/MachineModuleInfo.h"
+#include "llvm/CodeGen/TargetPassConfig.h"
+#include "llvm/CodeGen/TargetSubtargetInfo.h"
+#include "llvm/IR/DataLayout.h"
+#include "llvm/IR/LLVMContext.h"
+#include "llvm/IR/LegacyPassManager.h"
+#include "llvm/IR/Module.h"
+#include "llvm/IR/Verifier.h"
+#include "llvm/InitializePasses.h"
+#include "llvm/MC/MCTargetOptionsCommandFlags.h"
+#include "llvm/MC/TargetRegistry.h"
+#include "llvm/Pass.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/FormattedStream.h"
+#include "llvm/Support/InitLLVM.h"
+#include "llvm/Support/TargetSelect.h"
+#include "llvm/Target/TargetLoweringObjectFile.h"
+#include "llvm/Target/TargetMachine.h"
+#include "llvm/TargetParser/SubtargetFeature.h"
+#include "llvm/TargetParser/Triple.h"
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace llvm;
+
+namespace {
+
+// Mimic limited number of command line flags from llc to provide a better
+// user experience when passing options into the translate API call.
+static cl::opt<char> SpvOptLevel(" O", cl::Hidden, cl::Prefix, cl::init('0'));
+static cl::opt<std::string> SpvTargetTriple(" mtriple", cl::Hidden,
+                                            cl::init(""));
+
+// Utility to accept options in a command line style.
+void parseSPIRVCommandLineOptions(const std::vector<std::string> &Options,
+                                  raw_ostream *Errs) {
+  static constexpr const char *Origin = "SPIRVTranslateModule";
+  if (!Options.empty()) {
+    std::vector<const char *> Argv(1, Origin);
+    for (const auto &Arg : Options)
+      Argv.push_back(Arg.c_str());
+    cl::ParseCommandLineOptions(Argv.size(), Argv.data(), Origin, Errs);
+  }
+}
+
+std::once_flag InitOnceFlag;
+void InitializeSPIRVTarget() {
+  std::call_once(InitOnceFlag, []() {
+    LLVMInitializeSPIRVTargetInfo();
+    LLVMInitializeSPIRVTarget();
+    LLVMInitializeSPIRVTargetMC();
+    LLVMInitializeSPIRVAsmPrinter();
+  });
+}
+} // namespace
+
+namespace llvm {
+
+// The goal of this function is to facilitate integration of SPIRV Backend into
+// tools and libraries by means of exposing an API call that translate LLVM
+// module to SPIR-V and write results into a string as binary SPIR-V output,
+// providing diagnostics on fail and means of configuring translation in a style
+// of command line options.
+extern "C" LLVM_EXTERNAL_VISIBILITY bool
+SPIRVTranslateModule(Module *M, std::string &SpirvObj, std::string &ErrMsg,
+                     const std::vector<std::string> &Opts) {
+  // Fallbacks for option values.
+  static const std::string DefaultTriple = "spirv64-unknown-unknown";
+  static const std::string DefaultMArch = "";
+
+  // Parse Opts as if it'd be command line arguments.
+  std::string Errors;
+  raw_string_ostream ErrorStream(Errors);
+  parseSPIRVCommandLineOptions(Opts, &ErrorStream);
+  if (!Errors.empty()) {
+    ErrMsg = Errors;
+    return false;
+  }
+
+  llvm::CodeGenOptLevel OLevel;
+  if (auto Level = CodeGenOpt::parseLevel(SpvOptLevel)) {
+    OLevel = *Level;
+  } else {
+    ErrMsg = "Invalid optimization level!";
+    return false;
+  }
+
+  // SPIR-V-specific target initialization.
+  InitializeSPIRVTarget();
+
+  Triple TargetTriple(SpvTargetTriple.empty()
+                          ? M->getTargetTriple()
+                          : Triple::normalize(SpvTargetTriple));
+  if (TargetTriple.getTriple().empty()) {
+    TargetTriple.setTriple(DefaultTriple);
+    M->setTargetTriple(DefaultTriple);
+  }
+  const Target *TheTarget =
+      TargetRegistry::lookupTarget(DefaultMArch, TargetTriple, ErrMsg);
+  if (!TheTarget)
+    return false;
+
+  // A call to codegen::InitTargetOptionsFromCodeGenFlags(TargetTriple)
+  // hits the following assertion: llvm/lib/CodeGen/CommandFlags.cpp:78:
+  // llvm::FPOpFusion::FPOpFusionMode llvm::codegen::getFuseFPOps(): Assertion
+  // `FuseFPOpsView && "RegisterCodeGenFlags not created."' failed.
+  TargetOptions Options;
+  std::optional<Reloc::Model> RM;
+  std::optional<CodeModel::Model> CM;
+  std::unique_ptr<TargetMachine> Target =
+      std::unique_ptr<TargetMachine>(TheTarget->createTargetMachine(
+          TargetTriple.getTriple(), "", "", Options, RM, CM, OLevel));
+  if (!Target) {
+    ErrMsg = "Could not allocate target machine!";
+    return false;
+  }
+
+  if (M->getCodeModel())
+    Target->setCodeModel(*M->getCodeModel());
+
+  std::string DLStr = M->getDataLayoutStr();
+  Expected<DataLayout> MaybeDL = DataLayout::parse(
+      DLStr.empty() ? Target->createDataLayout().getStringRepresentation()
+                    : DLStr);
+  if (!MaybeDL) {
+    ErrMsg = toString(MaybeDL.takeError());
+    return false;
+  }
+  M->setDataLayout(MaybeDL.get());
+
+  TargetLibraryInfoImpl TLII(Triple(M->getTargetTriple()));
+  legacy::PassManager PM;
+  PM.add(new TargetLibraryInfoWrapperPass(TLII));
+  LLVMTargetMachine &LLVMTM = static_cast<LLVMTargetMachine &>(*Target);
+  MachineModuleInfoWrapperPass *MMIWP =
+      new MachineModuleInfoWrapperPass(&LLVMTM);
+  const_cast<TargetLoweringObjectFile *>(LLVMTM.getObjFileLowering())
+      ->Initialize(MMIWP->getMMI().getContext(), *Target);
+
+  SmallString<4096> OutBuffer;
+  raw_svector_ostream OutStream(OutBuffer);
+  if (Target->addPassesToEmitFile(PM, OutStream, nullptr,
+                                  CodeGenFileType::ObjectFile)) {
+    ErrMsg = "Target machine cannot emit a file of this type";
+    return false;
+  }
+
+  PM.run(*M);
+  SpirvObj = OutBuffer.str();
+
+  return true;
+}
+
+} // namespace llvm
diff --git a/llvm/lib/Target/SPIRV/SPIRVAPI.h b/llvm/lib/Target/SPIRV/SPIRVAPI.h
new file mode 100644
index 00000000000000..c3786c6975a890
--- /dev/null
+++ b/llvm/lib/Target/SPIRV/SPIRVAPI.h
@@ -0,0 +1,23 @@
+//===-- SPIRVAPI.h - SPIR-V Backend API interface ---------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_LIB_TARGET_SPIRV_SPIRVAPI_H
+#define LLVM_LIB_TARGET_SPIRV_SPIRVAPI_H
+
+#include <string>
+#include <vector>
+
+namespace llvm {
+class Module;
+
+extern "C" bool SPIRVTranslateModule(Module *M, std::string &Buffer,
+                                     std::string &ErrMsg,
+                                     const std::vector<std::string> &Opts);
+} // namespace llvm
+
+#endif // LLVM_LIB_TARGET_SPIRV_SPIRVAPI_H
diff --git a/llvm/unittests/Target/SPIRV/CMakeLists.txt b/llvm/unittests/Target/SPIRV/CMakeLists.txt
index 83ae215c512ca2..e9fe4883e5b024 100644
--- a/llvm/unittests/Target/SPIRV/CMakeLists.txt
+++ b/llvm/unittests/Target/SPIRV/CMakeLists.txt
@@ -6,6 +6,7 @@ include_directories(
 set(LLVM_LINK_COMPONENTS
   Analysis
   AsmParser
+  BinaryFormat
   Core
   SPIRVCodeGen
   SPIRVAnalysis
@@ -14,5 +15,6 @@ set(LLVM_LINK_COMPONENTS
 
 add_llvm_target_unittest(SPIRVTests
   SPIRVConvergenceRegionAnalysisTests.cpp
+  SPIRVAPITest.cpp
   )
 
diff --git a/llvm/unittests/Target/SPIRV/SPIRVAPITest.cpp b/llvm/unittests/Target/SPIRV/SPIRVAPITest.cpp
new file mode 100644
index 00000000000000..d58c1f3fe9b460
--- /dev/null
+++ b/llvm/unittests/Target/SPIRV/SPIRVAPITest.cpp
@@ -0,0 +1,152 @@
+//===- llvm/unittest/CodeGen/SPIRVAPITest.cpp -----------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+/// \file
+/// Test that SPIR-V Backend provides an API call that translates LLVM IR Module
+/// into SPIR-V.
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/AsmParser/Parser.h"
+#include "llvm/BinaryFormat/Magic.h"
+#include "llvm/IR/Module.h"
+#include "llvm/Support/SourceMgr.h"
+#include "gtest/gtest.h"
+#include <gmock/gmock.h>
+#include <string>
+#include <utility>
+
+using ::testing::StartsWith;
+
+namespace llvm {
+
+extern "C" bool SPIRVTranslateModule(Module *M, std::string &Buffer,
+                                     std::string &ErrMsg,
+                                     const std::vector<std::string> &Opts);
+
+class SPIRVAPITest : public testing::Test {
+protected:
+  bool toSpirv(StringRef Assembly, std::string &Result, std::string &ErrMsg,
+               const std::vector<std::string> &Opts) {
+    SMDiagnostic ParseError;
+    M = parseAssemblyString(Assembly, ParseError, Context);
+    if (!M) {
+      ParseError.print("IR parsing failed: ", errs());
+      report_fatal_error("Can't parse input assembly.");
+    }
+    bool Status = SPIRVTranslateModule(M.get(), Result, ErrMsg, Opts);
+    if (!Status)
+      errs() << ErrMsg;
+    return Status;
+  }
+
+  LLVMContext Context;
+  std::unique_ptr<Module> M;
+
+  static constexpr StringRef ExtensionAssembly = R"(
+    define dso_local spir_func void @test1() {
+    entry:
+      %res1 = tail call spir_func i32 @_Z26__spirv_GroupBitwiseAndKHR(i32 2, i32 0, i32 0)
+      ret void
+    }
+
+    declare dso_local spir_func i32  @_Z26__spirv_GroupBitwiseAndKHR(i32, i32, i32)
+  )";
+  static constexpr StringRef OkAssembly = R"(
+    %struct = type { [1 x i64] }
+
+    define spir_kernel void @foo(ptr noundef byval(%struct) %arg) {
+    entry:
+      call spir_func void @bar(<2 x i32> noundef <i32 0, i32 1>)
+      ret void
+    }
+
+    define spir_func void @bar(<2 x i32> noundef) {
+    entry:
+      ret void
+    }
+  )";
+};
+
+TEST_F(SPIRVAPITest, checkTranslateOk) {
+  StringRef Assemblies[] = {"", OkAssembly};
+  // Those command line arguments that overlap with registered by llc/codegen
+  // are to be started with the ' ' symbol.
+  std::vector<std::string> SetOfOpts[] = {
+      {}, {"- mtriple=spirv32-unknown-unknown"}};
+  for (const auto &Opts : SetOfOpts) {
+    for (StringRef &Assembly : Assemblies) {
+      std::string Result, Error;
+      bool Status = toSpirv(Assembly, Result, Error, Opts);
+      EXPECT_TRUE(Status && Error.empty() && !Result.empty());
+      EXPECT_EQ(identify_magic(Result), file_magic::spirv_object);
+    }
+  }
+}
+
+TEST_F(SPIRVAPITest, checkTranslateError) {
+  std::string Result, Error;
+  bool Status =
+      toSpirv(OkAssembly, Result, Error, {"-mtriple=spirv32-unknown-unknown"});
+  EXPECT_FALSE(Status);
+  EXPECT_TRUE(Result.empty());
+  EXPECT_THAT(Error,
+              StartsWith("SPIRVTranslateModule: Unknown command line argument "
+                         "'-mtriple=spirv32-unknown-unknown'"));
+  Status = toSpirv(OkAssembly, Result, Error, {"- O 5"});
+  EXPECT_FALSE(Status);
+  EXPECT_TRUE(Result.empty());
+  EXPECT_EQ(Error, "Invalid optimization level!");
+}
+
+TEST_F(SPIRVAPITest, checkTranslateSupportExtension) {
+  std::string Result, Error;
+  std::vector<std::string> Opts{
+      "--spirv-ext=+SPV_KHR_uniform_group_instructions"};
+  bool Status = toSpirv(ExtensionAssembly, Result, Error, Opts);
+  EXPECT_TRUE(Status && Error.empty() && !Result.empty());
+  EXPECT_EQ(identify_magic(Result), file_magic::spirv_object);
+}
+
+TEST_F(SPIRVAPITest, checkTranslateAllExtensions) {
+  std::string Result, Error;
+  std::vector<std::string> Opts{"--spirv-ext=all"};
+  bool Status = toSpirv(ExtensionAssembly, Result, Error, Opts);
+  EXPECT_TRUE(Status && Error.empty() && !Result.empty());
+  EXPECT_EQ(identify_magic(Result), file_magic::spirv_object);
+}
+
+#if !defined(NDEBUG) && GTEST_HAS_DEATH_TEST
+TEST_F(SPIRVAPITest, checkTranslateExtensionError) {
+  std::string Result, Error;
+  std::vector<std::string> Opts;
+  EXPECT_DEATH_IF_SUPPORTED(
+      { toSpirv(ExtensionAssembly, Result, Error, Opts); },
+      "LLVM ERROR: __spirv_GroupBitwiseAndKHR: the builtin requires the "
+      "following SPIR-V extension: SPV_KHR_uniform_group_instructions");
+}
+
+TEST_F(SPIRVAPITest, checkTranslateUnknownExtension) {
+  std::string Result, Error;
+  std::vector<std::string> Opts{"--spirv-ext=+SPV_XYZ_my_unknown_extension"};
+  EXPECT_DEATH_IF_SUPPORTED(
+      { toSpirv(ExtensionAssembly, Result, Error, Opts); },
+      "SPIRVTranslateModule: for the --spirv-ext option: Unknown SPIR-V");
+}
+
+TEST_F(SPIRVAPITest, checkTranslateWrongExtension) {
+  std::string Result, Error;
+  std::vector<std::string> Opts{"--spirv-ext=+SPV_KHR_subgroup_rotate"};
+  EXPECT_DEATH_IF_SUPPORTED(
+      { toSpirv(ExtensionAssembly, Result, Error, Opts); },
+      "LLVM ERROR: __spirv_GroupBitwiseAndKHR: the builtin requires the "
+      "following SPIR-V extension: SPV_KHR_uniform_group_instructions");
+}
+#endif
+
+} // end namespace llvm

Copy link
Contributor

@Keenuts Keenuts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with how 3rd party use LLVM, but AFAIK they do have access to the public LLVM headers no?
If that's correct, why not require a struct with the option we need in an already parsed format?

struct BuildOptions {
    CodeGenOptLevel OptLevel;
    Triple TargetTriple;
    std::vector<std::string> AllowedExtensions;
    ...
};
SPIRVTranslateModule(Module *M, const BuildOptions& Options, ...)

?

Duplicating the CLI feels a bit weird IMO

@VyacheslavLevytskyy
Copy link
Contributor Author

I'm not super familiar with how 3rd party use LLVM, but AFAIK they do have access to the public LLVM headers no? If that's correct, why not require a struct with the option we need in an already parsed format?

struct BuildOptions {
    CodeGenOptLevel OptLevel;
    Triple TargetTriple;
    std::vector<std::string> AllowedExtensions;
    ...
};
SPIRVTranslateModule(Module *M, const BuildOptions& Options, ...)

?

Duplicating the CLI feels a bit weird IMO

The idea was to introduce a loose coupling between SPIRV BE and its user to achieve mainly two goals: avoid forcing a user to include one more header file that would describe this interface in C++ terms (like struct BuildOptions data type), and to allow to move development of SPIRV BE and its user library forward with different speed, meaning lack of synchronization with respect of supported and/or mandatory options. Just as an example, Khronos Translator, as a potential user of this feature, doesn't rely on explicitly defined in a command line target triple but expect it to be defined inside the module, so it would create an inconvenience to require this option. Basically, this feature is exactly what you've mentioned, it's a duplication of CLI for cases when usage of existing CLI tools is a poor choice, meaning to substitute existing integration way of calling llc as an external executable. The latter seems like a bad practice, so this PR tries to improve user experience (avoiding issues with performance, debug, profile and so on) by providing a direct API call, leaving the same feeling of loose coupling as in the case of preparing a command line to execute.

@Keenuts
Copy link
Contributor

Keenuts commented Sep 9, 2024

The idea was to introduce a loose coupling between SPIRV BE and its user to achieve mainly two goals:
avoid forcing a user to include one more header file that would describe this interface in C++ terms (like struct BuildOptions data type)
allow to move development of SPIRV BE and its user library forward with different speed, meaning lack of synchronization with respect of supported and/or mandatory options.

Ok, so if I understand, you want the 3rd party library to be able to call the function on a newer LLVM version, without having to relink/recompile again with the updated headers.
So if in the future, we add another argument in the struct, then LLVM would read garbage on those new fields when called from the old library.

Vulkan already does something for that: the chain of structs, with the pNext argument.
An older version would have a linked list of struct with 2 nodes, one with the triple, one with the OptLevel, and last pNext is nullptr. Adding new fields means adding new nodes in the list, without breaking back-compat.

But once again, not very knowledgeable about the LLVM API interfaces, so maybe parsing command-line is accepted the way for this kind of back-compat.

@VyacheslavLevytskyy
Copy link
Contributor Author

@Keenuts Thank you for the insight and advices. I've tweaked the interface a bit to account for the fact that the list of extensions is one of the most frequently used command line argument with more complicated composition and parsing. After the latest change the list of extensions is an argument to the API call. What is your opinion about the change, does it look slightly better now?
CC: @michalpaszkowski

@Keenuts
Copy link
Contributor

Keenuts commented Sep 10, 2024

What is your opinion about the change, does it look slightly better now?

If back/forward-compatibility is the most important part, I'd prefer to either:

  • go full parsing (as you did before), losing compile-time checks and self-documenting code, but simple to use.
  • do it like Vulkan (linked list of structs, head passed by pointer), bringing more compile-time checks/documentations, but making usage slightly more verbose.

I feel like having part in the to-parse command-line, and one part as classic parameter is weird.

@VyacheslavLevytskyy
Copy link
Contributor Author

I've been thinking about this. Explicit extensions may simplify the client part though and slightly improve performance for the expected use cases.

If you don't mind, I'd continue with the current version as the first attempt, to be able to verify this approach in Khronos Translator, Intel XPU backend for the Triton compiler and SYCL offloading. We can re-visit this API call later in case if integration would show that this is needed or recommended.

@MrSidims
Copy link
Contributor

MrSidims commented Sep 10, 2024

From my perspective as SPIR-V To LLVM IR translator developer current approach is OK. For the proposed options above:

go full parsing (as you did before), losing compile-time checks and self-documenting code, but simple to use.

This won't affect the use case described in the PR description, but it may affect other 3rd party projects, as they would need to parse options once to get extensions string and then pass it to llvm:toSpirv call along with other options, while the whole parsing might be done just here in the SPIR-V backend. So I'm slightly favorable for this proposal, but don't have a strong opinion about this.

do it like Vulkan (linked list of structs, head passed by pointer), bringing more compile-time checks/documentations, but making usage slightly more verbose.

Personally that would be my least favorable option. But I'm not an expert in llvm's guideline in the regard of how to parse and pass options to binaries and library calls.

@VyacheslavLevytskyy VyacheslavLevytskyy merged commit bca2b6d into llvm:main Sep 10, 2024
7 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants