https://github.com/bader updated https://github.com/llvm/llvm-project/pull/202829
>From ef16010d9e601f41b6eaa4cd595784d6798f6fc9 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Tue, 9 Jun 2026 10:07:46 -0700 Subject: [PATCH 1/8] [clang-sycl-linker] Add static archive (.a) support Add support for static archives of LLVM bitcode files to clang-sycl-linker. The archive member-selection engine (a symbol-driven fixed-point lazy extraction loop) is simplified to bitcode-only handling. clang-sycl-linker gains -l/--library, --whole-archive/--no-whole-archive, and -u/--undefined options. The previous --bc-library option is removed in favor of the standard -l mechanism. Inputs (positional files and -l libraries) are now resolved to in-memory buffers; archive members are pulled in lazily only when they resolve undefined symbols, and bitcode is loaded with parseBitcodeFile. Co-Authored-By: Claude --- clang/docs/ClangSYCLLinker.rst | 71 ++- .../clang-sycl-linker/archive-extras.ll | 106 ++++ .../OffloadTools/clang-sycl-linker/basic.ll | 105 ++-- .../OffloadTools/clang-sycl-linker/link.ll | 133 ++++- .../clang-sycl-linker/split-mode.ll | 6 +- .../OffloadTools/clang-sycl-linker/triple.ll | 2 + .../clang-sycl-linker/weak-symbols.ll | 117 +++++ clang/tools/clang-sycl-linker/CMakeLists.txt | 1 + .../clang-sycl-linker/ClangSYCLLinker.cpp | 469 +++++++++++++----- clang/tools/clang-sycl-linker/SYCLLinkOpts.td | 21 +- 10 files changed, 863 insertions(+), 168 deletions(-) create mode 100644 clang/test/OffloadTools/clang-sycl-linker/archive-extras.ll create mode 100644 clang/test/OffloadTools/clang-sycl-linker/weak-symbols.ll diff --git a/clang/docs/ClangSYCLLinker.rst b/clang/docs/ClangSYCLLinker.rst index c28c9fefaace3..2906bf2d9f488 100644 --- a/clang/docs/ClangSYCLLinker.rst +++ b/clang/docs/ClangSYCLLinker.rst @@ -50,7 +50,10 @@ be passed down to downstream AOT compilation tools like 'ocloc' and 'opencl-aot' -help-hidden Display all available options -help Display available options (--help-hidden for more) -L <dir> Add <dir> to the library search path - --bc-library <name> Add LLVM bitcode library <name> (with extension) to the link. A relative <name> is resolved against -L paths; an absolute path is taken as-is. + -l <libname> Search for library <libname> + --whole-archive Include all archive members in the link + --no-whole-archive Only include archive members that resolve undefined symbols (default) + -u <symbol> Force undefined symbol during linking --module-split-mode=<mode> Module split mode: 'source' (default), 'kernel', or 'none' --ocloc-options=<value> Options passed to ocloc for Intel GPU AOT compilation --opencl-aot-options=<value> Options passed to opencl-aot for Intel CPU AOT compilation @@ -61,8 +64,49 @@ be passed down to downstream AOT compilation tools like 'ocloc' and 'opencl-aot' -v Print verbose information -spirv-dump-device-code=<dir> Directory to dump SPIR-V IR code into -Example -======= +Library Linking +=============== + +Device bitcode libraries can be packaged into archive libraries (``.a`` files) +using ``llvm-ar`` and linked using the ``-l`` option: + +.. code-block:: console + + llvm-ar rc libdevice.a func1.bc func2.bc func3.bc + clang-sycl-linker input.bc -l device -L /path/to/libs + +The linker supports standard archive library search semantics: + +* ``-l <name>`` searches for ``lib<name>.a`` in the directories specified by ``-L`` +* ``-l :<exact-name>`` searches for the exact filename in the ``-L`` paths +* Absolute paths can be passed as positional arguments: ``clang-sycl-linker input.bc /path/to/libdevice.a`` + +By default, archive linking is **lazy** - only archive members (individual ``.bc`` files) +that resolve undefined symbols are extracted and linked. This happens at file +granularity: if any symbol in a ``.bc`` file is needed, all symbols in that file +are included. The linker uses a symbol-driven fixed-point algorithm: it +repeatedly scans archives to extract members that resolve currently undefined +symbols until no more extractions occur. + +To force extraction of all archive members regardless of symbol resolution, use +``--whole-archive``: + +.. code-block:: console + + clang-sycl-linker input.bc --whole-archive -l device --no-whole-archive -l other + +The ``-u <symbol>`` option can be used to force a symbol to be undefined, which +can trigger extraction of archive members that define that symbol: + +.. code-block:: console + + clang-sycl-linker input.bc -u my_init_function -l device + +Examples +======== + +Basic Usage +----------- This tool is intended to be invoked when targeting any of the target offloading toolchains. When the --sycl-link option is passed to the clang driver, the @@ -74,3 +118,24 @@ generate the final executable. .. code-block:: console clang-sycl-linker --triple spirv64 --arch bmg_g21 input.bc + +Linking with Device Libraries +------------------------------ + +To link device bitcode libraries, first package them into archive files: + +.. code-block:: console + + # Create device library archives + llvm-ar rc libmath.a sin.bc cos.bc tan.bc + llvm-ar rc libutils.a helper1.bc helper2.bc + + # Link with lazy loading (only needed members extracted) + clang-sycl-linker --triple spirv64 kernel.bc -l math -l utils -L /path/to/libs -o kernel.spv + + # Force all members to be included from libmath.a + clang-sycl-linker --triple spirv64 kernel.bc --whole-archive -l math --no-whole-archive -l utils -L /path/to/libs -o kernel.spv + + # Use exact archive filename or absolute path + clang-sycl-linker --triple spirv64 kernel.bc -l :libmath.a -L /path/to/libs -o kernel.spv + clang-sycl-linker --triple spirv64 kernel.bc /absolute/path/libmath.a -o kernel.spv diff --git a/clang/test/OffloadTools/clang-sycl-linker/archive-extras.ll b/clang/test/OffloadTools/clang-sycl-linker/archive-extras.ll new file mode 100644 index 0000000000000..bd26ddc3994f2 --- /dev/null +++ b/clang/test/OffloadTools/clang-sycl-linker/archive-extras.ll @@ -0,0 +1,106 @@ +; Additional archive-handling edge cases for clang-sycl-linker. +; +; REQUIRES: spirv-registered-target +; +; RUN: rm -rf %t && split-file %s %t +; RUN: llvm-as %t/main.ll -o %t/main.bc +; RUN: llvm-as %t/dup.ll -o %t/dup.bc +; RUN: llvm-as %t/incl.ll -o %t/incl.bc +; RUN: llvm-as %t/extra.ll -o %t/extra.bc +; RUN: llvm-as %t/otherarch.ll -o %t/otherarch.bc +; RUN: llvm-ar rc %t/libdup.a %t/dup.bc +; RUN: llvm-ar rc %t/libincl.a %t/incl.bc +; RUN: llvm-ar rc %t/libextra.a %t/extra.bc +; +; A multiply-defined symbol inside an archive member is harmless while the +; member stays lazy: main.bc already defines bar_func1, so dup.bc is never +; extracted and there is no multiply-defined error. +; RUN: clang-sycl-linker %t/main.bc -l dup -L %t --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-LAZY-OK +; CHECK-LAZY-OK: define {{.*}}bar_func1{{.*}} +; CHECK-LAZY-OK-NOT: error: +; +; Forcing the same member in with --whole-archive extracts dup.bc and the +; conflicting definition now triggers a multiply-defined error. +; RUN: not clang-sycl-linker %t/main.bc --whole-archive -l dup -L %t --dry-run -o /dev/null 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-WHOLE-CONFLICT +; CHECK-WHOLE-CONFLICT: error: Linking globals named {{.*}}bar_func1{{.*}} symbol multiply defined! +; +; --no-whole-archive after --whole-archive restores lazy behavior for a later +; -l: libincl is whole-archived (inclFunc is pulled in unconditionally), while +; libextra is lazy and nothing references extraFunc, so it is not pulled. +; RUN: clang-sycl-linker %t/main.bc --whole-archive -l incl --no-whole-archive -l extra -L %t --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-NO-WHOLE +; CHECK-NO-WHOLE: define {{.*}}bar_func1{{.*}} +; CHECK-NO-WHOLE: define {{.*}}inclFunc{{.*}} +; CHECK-NO-WHOLE-NOT: define {{.*}}extraFunc{{.*}} +; +; -L search must skip a directory whose name matches the requested library and +; fall through to a later -L path that holds the real archive. Here %t/dir1 +; contains a *directory* named libincl.a, while %t (the second -L) has the real +; archive; the real one must be found rather than erroring on the directory. +; RUN: mkdir -p %t/dir1/libincl.a +; RUN: clang-sycl-linker %t/main.bc --whole-archive -l incl -L %t/dir1 -L %t --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-DIR-FALLTHROUGH +; CHECK-DIR-FALLTHROUGH: define {{.*}}inclFunc{{.*}} +; +; A whole-archive member built for a different target triple is silently skipped +; rather than producing a "conflicting target triples" error. otherarch.bc has +; triple spirv32; main.bc is spirv64, so otherarch's member is dropped while +; inclFunc (spirv64) is still linked. +; RUN: llvm-ar rc %t/libother.a %t/incl.bc %t/otherarch.bc +; RUN: clang-sycl-linker %t/main.bc --whole-archive -l other -L %t --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-TRIPLE-SKIP +; CHECK-TRIPLE-SKIP: define {{.*}}inclFunc{{.*}} +; CHECK-TRIPLE-SKIP-NOT: define {{.*}}otherArchFunc{{.*}} +; CHECK-TRIPLE-SKIP-NOT: error: + +;--- main.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @bar_func1(i32 %a, i32 %b) { +entry: + %res = add nsw i32 %b, %a + ret i32 %res +} + +;--- dup.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @bar_func1(i32 %a, i32 %b) { +entry: + %mul = shl nsw i32 %a, 1 + %res = add nsw i32 %mul, %b + ret i32 %res +} + +;--- incl.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @inclFunc(i32 %a) { +entry: + %res = add nsw i32 %a, 7 + ret i32 %res +} + +;--- extra.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @extraFunc(i32 %a) { +entry: + %res = mul nsw i32 %a, 3 + ret i32 %res +} + +;--- otherarch.ll +target triple = "spirv32" + +define spir_func i32 @otherArchFunc(i32 %a) { +entry: + %res = sub nsw i32 %a, 1 + ret i32 %res +} diff --git a/clang/test/OffloadTools/clang-sycl-linker/basic.ll b/clang/test/OffloadTools/clang-sycl-linker/basic.ll index e906d23b90be9..564ef6a67f3e9 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/basic.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/basic.ll @@ -22,76 +22,97 @@ ; ; Test non-existent input file ; RUN: not clang-sycl-linker %t-missing.bc -o %t.out 2>&1 | FileCheck %s --check-prefix=MISSING -; MISSING: input file '{{.*}}-missing.bc' does not exist +; MISSING: input file not found: '{{.*}}-missing.bc' ; ; Test the dry run of a simple case to link two input files. ; Test that IMG_SPIRV image kind is set for non-AOT compilation. ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc %t/input2.bc -o %t/spirv.out 2>&1 \ ; RUN: | FileCheck %s --check-prefix=SIMPLE-FO -; SIMPLE-FO: link: inputs: {{.*}}.bc, {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; SIMPLE-FO: link: inputs: {{.*}}.bc, {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; SIMPLE-FO-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: {{.*}}_0.spv ; SIMPLE-FO-NEXT: sycl-bundle: image kind: spv, triple: spirv64, arch: {{$}} ; SIMPLE-FO-NOT: {{.+}} ; -; Test the dry run of a simple case with device library files specified. +; Test the dry run of a simple case with device library archive specified using --whole-archive. ; RUN: mkdir -p %t/libs -; RUN: touch %t/libs/lib1.bc -; RUN: touch %t/libs/lib2.bc -; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc %t/input2.bc --library-path=%t/libs --bc-library lib1.bc --bc-library lib2.bc -o a.spv 2>&1 \ +; RUN: llvm-as %t/lib1.ll -o %t/libs/lib1.bc +; RUN: llvm-as %t/lib2.ll -o %t/libs/lib2.bc +; RUN: llvm-ar rc %t/libs/libdevice.a %t/libs/lib1.bc %t/libs/lib2.bc +; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc %t/input2.bc --library-path=%t/libs --whole-archive -l device -o /dev/null 2>&1 \ ; RUN: | FileCheck %s --check-prefix=DEVLIBS -; DEVLIBS: link: inputs: {{.*}}.bc libfiles: {{.*}}lib1.bc, {{.*}}lib2.bc output: [[LLVMLINKOUT:.*]].bc -; DEVLIBS-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: a_0.spv +; DEVLIBS: link: inputs: {{.*}}.bc, {{.*}}.bc, {{.*}}libdevice.a(lib1.bc), {{.*}}libdevice.a(lib2.bc) output: [[LLVMLINKOUT:.*]].bc +; DEVLIBS-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: /dev/null_0.spv ; DEVLIBS-NEXT: sycl-bundle: image kind: spv, triple: spirv64, arch: {{$}} ; DEVLIBS-NOT: {{.+}} ; -; Test -L short form (joined) and --bc-library= joined form. -; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc -L%t/libs --bc-library=lib1.bc -o a.spv 2>&1 \ +; Test -L short form (joined) and -l with archive using --whole-archive. +; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc -L%t/libs --whole-archive -l device -o /dev/null 2>&1 \ ; RUN: | FileCheck %s --check-prefix=DEVLIBS-SHORT -; DEVLIBS-SHORT: link: inputs: {{.*}}.bc libfiles: {{.*}}libs{{[/\\]}}lib1.bc output: {{.*}}.bc +; DEVLIBS-SHORT: link: inputs: {{.*}}.bc, {{.*}}libdevice.a(lib1.bc), {{.*}}libdevice.a(lib2.bc) output: {{.*}}.bc ; -; Test that search continues past the first -L when the library is not found there. lib1.bc exists only in %t/libs (the second -L). +; Test that search continues past the first -L when the library is not found there. libdevice.a exists only in %t/libs (the second -L). ; RUN: mkdir -p %t/empty -; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc -L %t/empty -L %t/libs --bc-library lib1.bc -o a.spv 2>&1 \ +; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc -L %t/empty -L %t/libs --whole-archive -l device -o /dev/null 2>&1 \ ; RUN: | FileCheck %s --check-prefix=DEVLIBS-FALLTHROUGH -; DEVLIBS-FALLTHROUGH: link: inputs: {{.*}}.bc libfiles: {{.*}}libs{{[/\\]}}lib1.bc output: {{.*}}.bc -; -; Test that -L paths are searched in order: when the same name exists in multiple -L dirs, the first one wins. -; RUN: mkdir -p %t/libs2 -; RUN: touch %t/libs/shadow.bc %t/libs2/shadow.bc -; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc -L %t/libs2 -L %t/libs --bc-library shadow.bc -o a.spv 2>&1 \ -; RUN: | FileCheck %s --check-prefix=DEVLIBS-ORDER -; DEVLIBS-ORDER: link: inputs: {{.*}}.bc libfiles: {{.*}}libs2{{[/\\]}}shadow.bc output: {{.*}}.bc +; DEVLIBS-FALLTHROUGH: link: inputs: {{.*}}.bc, {{.*}}libdevice.a(lib1.bc), {{.*}}libdevice.a(lib2.bc) output: {{.*}}.bc ; ; Test a simple case with a random file (not bitcode) as input. ; RUN: touch %t/dummy.o -; RUN: not clang-sycl-linker %t/dummy.o -o a.spv 2>&1 \ +; RUN: not clang-sycl-linker %t/dummy.o -o /dev/null 2>&1 \ ; RUN: | FileCheck %s --check-prefix=FILETYPEERROR -; FILETYPEERROR: unsupported file type +; FILETYPEERROR: unsupported file type: '{{.*}}dummy.o' +; +; Test that unsupported file type error includes buffer identifier when found inside an archive. +; Create an archive containing an unsupported file (text file instead of bitcode). +; RUN: echo "not bitcode" > %t/invalid.txt +; RUN: llvm-ar rc %t/libinvalid.a %t/invalid.txt +; RUN: not clang-sycl-linker --dry-run %t/input1.bc -L %t --whole-archive -l invalid -o /dev/null 2>&1 \ +; RUN: | FileCheck %s --check-prefix=ARCHIVE-INVALID-MEMBER +; ARCHIVE-INVALID-MEMBER: unsupported file type: '{{.*}}libinvalid.a(invalid.txt)' +; +; Test mixed archive: valid bitcode member + invalid member. +; The error should clearly identify which member is invalid. +; RUN: llvm-ar rc %t/libmixed.a %t/libs/lib1.bc %t/invalid.txt +; RUN: not clang-sycl-linker --dry-run %t/input1.bc -L %t --whole-archive -l mixed -o /dev/null 2>&1 \ +; RUN: | FileCheck %s --check-prefix=ARCHIVE-MIXED-INVALID +; ARCHIVE-MIXED-INVALID: unsupported file type: '{{.*}}libmixed.a(invalid.txt)' ; ; Test to see if device library related errors are emitted. -; RUN: not clang-sycl-linker --dry-run %t/input1.bc %t/input2.bc --library-path=%t/libs --bc-library lib1.bc --bc-library lib2.bc --bc-library lib3.bc -o a.spv 2>&1 \ +; RUN: not clang-sycl-linker --dry-run %t/input1.bc %t/input2.bc --library-path=%t/libs -l device -l nonexistent -o /dev/null 2>&1 \ ; RUN: | FileCheck %s --check-prefix=DEVLIBSERR -; DEVLIBSERR: '{{.*}}lib3.bc' library file not found +; DEVLIBSERR: unable to find library -lnonexistent ; -; Test that there is no implicit CWD search: a bare bitcode name without any -L +; Test that there is no implicit CWD search: a bare library name without any -L ; must fail to resolve, even if a same-named file exists in the CWD. -; RUN: cd %t && not clang-sycl-linker --dry-run input1.bc --bc-library input1.bc -o a.spv 2>&1 \ +; RUN: cd %t && not clang-sycl-linker --dry-run input1.bc -l mixed -o /dev/null 2>&1 \ ; RUN: | FileCheck %s --check-prefix=NO-CWD-SEARCH -; NO-CWD-SEARCH: 'input1.bc' library file not found +; NO-CWD-SEARCH: unable to find library -lmixed ; ; Test that a directory matching the requested name is not accepted as a library: -; %t/libs is a directory created above; resolving --bc-library libs against -L %t -; would otherwise pick it up and fail later with a confusing bitcode-reader error. -; RUN: not clang-sycl-linker --dry-run %t/input1.bc -L %t --bc-library libs -o a.spv 2>&1 \ +; %t/libs is a directory created above; resolving -l:libs against -L %t skips the +; directory during the search, so the library is reported as not found (and a +; real archive of the same name in a later -L path could still be picked up). +; RUN: not clang-sycl-linker --dry-run %t/input1.bc -L %t -l :libs -o /dev/null 2>&1 \ ; RUN: | FileCheck %s --check-prefix=NO-DIR-AS-LIB -; NO-DIR-AS-LIB: 'libs' library file not found +; NO-DIR-AS-LIB: unable to find library -l:libs +; +; Test that providing only an empty archive results in an error. +; RUN: llvm-ar rc %t/empty.a +; RUN: not clang-sycl-linker --dry-run --whole-archive %t/empty.a -o /dev/null 2>&1 \ +; RUN: | FileCheck %s --check-prefix=NO-RESOLVED-INPUT +; NO-RESOLVED-INPUT: no input files could be resolved +; +; Test that providing only a lazy archive with no extracted members results in an error. +; RUN: not clang-sycl-linker --dry-run %t/libs/libdevice.a -o /dev/null 2>&1 \ +; RUN: | FileCheck %s --check-prefix=NO-RESOLVED-LAZY +; NO-RESOLVED-LAZY: no input files could be resolved ; ; Test AOT compilation for an Intel GPU. ; Test that IMG_Object image kind is set for AOT compilation (Intel GPU). ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none -arch=bmg_g21 %t/input1.bc %t/input2.bc -o %t/aot-gpu.out 2>&1 \ ; RUN: --ocloc-options="-a -b" \ ; RUN: | FileCheck %s --check-prefix=AOT-INTEL-GPU -; AOT-INTEL-GPU: link: inputs: {{.*}}.bc, {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; AOT-INTEL-GPU: link: inputs: {{.*}}.bc, {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; AOT-INTEL-GPU-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: [[SPIRVTRANSLATIONOUT:.*]]_0.spv ; AOT-INTEL-GPU-NEXT: "{{.*}}ocloc{{.*}}" {{.*}}-device bmg_g21 -a -b {{.*}}-output [[SPIRVTRANSLATIONOUT]]_0.out -file [[SPIRVTRANSLATIONOUT]]_0.spv ; AOT-INTEL-GPU-NEXT: sycl-bundle: image kind: o, triple: spirv64, arch: bmg_g21 @@ -102,7 +123,7 @@ ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none -arch=graniterapids %t/input1.bc %t/input2.bc -o %t/aot-cpu.out 2>&1 \ ; RUN: --opencl-aot-options="-a -b" \ ; RUN: | FileCheck %s --check-prefix=AOT-INTEL-CPU -; AOT-INTEL-CPU: link: inputs: {{.*}}.bc, {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; AOT-INTEL-CPU: link: inputs: {{.*}}.bc, {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; AOT-INTEL-CPU-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: [[SPIRVTRANSLATIONOUT:.*]]_0.spv ; AOT-INTEL-CPU-NEXT: "{{.*}}opencl-aot{{.*}}" {{.*}}--device=cpu -a -b {{.*}}-o [[SPIRVTRANSLATIONOUT]]_0.out [[SPIRVTRANSLATIONOUT]]_0.spv ; AOT-INTEL-CPU-NEXT: sycl-bundle: image kind: o, triple: spirv64, arch: graniterapids @@ -164,3 +185,19 @@ target triple = "spirv64" define spir_func i32 @helper() { ret i32 0 } + +;--- lib1.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @lib1_func() { + ret i32 1 +} + +;--- lib2.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @lib2_func() { + ret i32 2 +} diff --git a/clang/test/OffloadTools/clang-sycl-linker/link.ll b/clang/test/OffloadTools/clang-sycl-linker/link.ll index 4114f0a3f3fb1..31d2c9d2344e2 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/link.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/link.ll @@ -7,9 +7,13 @@ ; RUN: llvm-as %t/bar.ll -o %t/bar.bc ; RUN: llvm-as %t/baz.ll -o %t/baz.bc ; RUN: llvm-as %t/libfoo.ll -o %t/libfoo.bc +; RUN: llvm-as %t/addFive.ll -o %t/addFive.bc +; RUN: llvm-as %t/unusedFunc.ll -o %t/unusedFunc.bc +; RUN: llvm-ar rc %t/libfoo.a %t/libfoo.bc +; RUN: llvm-ar rc %t/libdevice.a %t/addFive.bc %t/unusedFunc.bc ; ; Test linking two input files. -; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc --dry-run -o a.spv --print-linked-module 2>&1 \ +; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc --dry-run -o /dev/null --print-linked-module 2>&1 \ ; RUN: | FileCheck %s --check-prefix=CHECK-SIMPLE ; CHECK-SIMPLE: define {{.*}}foo_func1{{.*}} ; CHECK-SIMPLE: define {{.*}}foo_func2{{.*}} @@ -18,22 +22,115 @@ ; CHECK-SIMPLE-NOT: define {{.*}}unusedFunc{{.*}} ; ; Test that multiply defined symbols are reported as errors. -; RUN: not clang-sycl-linker %t/bar.bc %t/baz.bc --dry-run -o a.spv 2>&1 \ +; RUN: not clang-sycl-linker %t/bar.bc %t/baz.bc --dry-run -o /dev/null 2>&1 \ ; RUN: | FileCheck %s --check-prefix=CHECK-MULTIPLE-DEFS ; CHECK-MULTIPLE-DEFS: error: Linking globals named {{.*}}bar_func1{{.*}} symbol multiply defined! ; -; Test linking with a BC library file resolved through -L. -; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc --bc-library libfoo.bc -L %t --dry-run -o a.spv --print-linked-module 2>&1 \ +; Test lazy linking with an archive library: only needed members are extracted. +; foo.bc references addFive, so addFive.bc is extracted from libdevice.a. +; unusedFunc.bc is not needed, so it should NOT be extracted. +; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc -l device -L %t --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-LAZY-LINK +; CHECK-LAZY-LINK: define {{.*}}foo_func1{{.*}} +; CHECK-LAZY-LINK: define {{.*}}foo_func2{{.*}} +; CHECK-LAZY-LINK: define {{.*}}bar_func1{{.*}} +; CHECK-LAZY-LINK: define {{.*}}addFive{{.*}} +; CHECK-LAZY-LINK-NOT: define {{.*}}unusedFunc{{.*}} +; +; Test linking with an archive library file using -l:libname.a syntax. +; Archive linking extracts members at file granularity, so all functions in libfoo.bc are included. +; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc -l :libfoo.a -L %t --dry-run -o /dev/null --print-linked-module 2>&1 \ ; RUN: | FileCheck %s --check-prefix=CHECK-DEVICE-LIB ; CHECK-DEVICE-LIB: define {{.*}}foo_func1{{.*}} ; CHECK-DEVICE-LIB: define {{.*}}foo_func2{{.*}} ; CHECK-DEVICE-LIB: define {{.*}}bar_func1{{.*}} ; CHECK-DEVICE-LIB: define {{.*}}addFive{{.*}} -; CHECK-DEVICE-LIB-NOT: define {{.*}}unusedFunc{{.*}} +; CHECK-DEVICE-LIB: define {{.*}}unusedFunc{{.*}} ; -; Test that an absolute path to --bc-library is taken as-is, with no -L required. -; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc --bc-library %t/libfoo.bc --dry-run -o a.spv --print-linked-module 2>&1 \ -; RUN: | FileCheck %s --check-prefix=CHECK-DEVICE-LIB +; Test that an absolute path as a positional argument still performs lazy member extraction. +; libdevice.a has two members (addFive.bc and unusedFunc.bc). +; Since foo.bc needs addFive, only addFive.bc member is extracted; unusedFunc.bc is not. +; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc %t/libdevice.a --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-DEVICE-LIB-POS +; CHECK-DEVICE-LIB-POS: define {{.*}}foo_func1{{.*}} +; CHECK-DEVICE-LIB-POS: define {{.*}}foo_func2{{.*}} +; CHECK-DEVICE-LIB-POS: define {{.*}}bar_func1{{.*}} +; CHECK-DEVICE-LIB-POS: define {{.*}}addFive{{.*}} +; CHECK-DEVICE-LIB-POS-NOT: define {{.*}}unusedFunc{{.*}} +; +; Test that -L paths are searched in order: when the same name exists in multiple -L dirs, the first one wins. +; RUN: mkdir -p %t/libs1 %t/libs2 +; RUN: rm -f %t/libs1/libshadow.a %t/libs2/libshadow.a +; RUN: llvm-ar rc %t/libs1/libshadow.a %t/addFive.bc +; RUN: llvm-ar rc %t/libs2/libshadow.a %t/unusedFunc.bc +; RUN: clang-sycl-linker %t/foo.bc -L %t/libs2 -L %t/libs1 --whole-archive -l shadow --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-LIB-ORDER +; CHECK-LIB-ORDER: define {{.*}}unusedFunc +; CHECK-LIB-ORDER-NOT: define {{.*}}addFive +; +; Test that -u forces extraction of an otherwise-unreferenced archive member. +; Without -u, unusedFunc is not extracted. With -u unusedFunc, it is pulled in. +; RUN: clang-sycl-linker %t/bar.bc %t/libdevice.a --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-NO-FORCE-UNDEF +; CHECK-NO-FORCE-UNDEF: define {{.*}}bar_func1{{.*}} +; CHECK-NO-FORCE-UNDEF-NOT: define {{.*}}unusedFunc{{.*}} +; CHECK-NO-FORCE-UNDEF-NOT: define {{.*}}addFive{{.*}} +; +; RUN: clang-sycl-linker %t/bar.bc %t/libdevice.a -u unusedFunc --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-FORCE-UNDEF +; CHECK-FORCE-UNDEF: define {{.*}}bar_func1{{.*}} +; CHECK-FORCE-UNDEF: define {{.*}}unusedFunc{{.*}} +; CHECK-FORCE-UNDEF-NOT: define {{.*}}addFive{{.*}} +; +; Test that multiple -u flags work correctly and extract all specified members. +; RUN: clang-sycl-linker %t/bar.bc %t/libdevice.a -u unusedFunc -u addFive --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-MULTI-UNDEF +; CHECK-MULTI-UNDEF: define {{.*}}bar_func1{{.*}} +; CHECK-MULTI-UNDEF: define {{.*}}addFive{{.*}} +; CHECK-MULTI-UNDEF: define {{.*}}unusedFunc{{.*}} +; +; Test that -u works correctly with -l library syntax (not just positional archives). +; RUN: clang-sycl-linker %t/bar.bc -l device -L %t -u unusedFunc --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-UNDEF-WITH-L +; CHECK-UNDEF-WITH-L: define {{.*}}bar_func1{{.*}} +; CHECK-UNDEF-WITH-L: define {{.*}}unusedFunc{{.*}} +; CHECK-UNDEF-WITH-L-NOT: define {{.*}}addFive{{.*}} +; +; Test that -u combined with actual references works correctly (both should be extracted). +; foo.bc references addFive, and -u forces unusedFunc. +; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc %t/libdevice.a -u unusedFunc --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-UNDEF-PLUS-REF +; CHECK-UNDEF-PLUS-REF: define {{.*}}foo_func1{{.*}} +; CHECK-UNDEF-PLUS-REF: define {{.*}}bar_func1{{.*}} +; CHECK-UNDEF-PLUS-REF: define {{.*}}addFive{{.*}} +; CHECK-UNDEF-PLUS-REF: define {{.*}}unusedFunc{{.*}} +; +; Regression test: -u symbol should remain undefined until resolved by archive member. +; This test verifies the fix for the bug where forced-undefined entries were overwritten +; before ResolvesReference was computed, making -u ineffective. +; RUN: clang-sycl-linker %t/bar.bc -u addFive %t/libdevice.a --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-UNDEF-REMAINS +; CHECK-UNDEF-REMAINS: define {{.*}}bar_func1{{.*}} +; CHECK-UNDEF-REMAINS: define {{.*}}addFive{{.*}} +; CHECK-UNDEF-REMAINS-NOT: define {{.*}}unusedFunc{{.*}} +; +; Test -u with archive processed BEFORE the symbol table has been populated by regular inputs. +; This specifically tests that the forced-undefined placeholder survives initial processing. +; RUN: clang-sycl-linker -u addFive %t/libdevice.a %t/bar.bc --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-UNDEF-FIRST +; CHECK-UNDEF-FIRST: define {{.*}}addFive{{.*}} +; CHECK-UNDEF-FIRST: define {{.*}}bar_func1{{.*}} +; CHECK-UNDEF-FIRST-NOT: define {{.*}}unusedFunc{{.*}} +; +; Test that -l with an absolute path works correctly (standard linker behavior). +; An absolute path given to -l should be used directly without searching -L directories. +; RUN: clang-sycl-linker %t/foo.bc %t/bar.bc -l %t/libdevice.a --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-ABSOLUTE-PATH-L +; CHECK-ABSOLUTE-PATH-L: define {{.*}}foo_func1{{.*}} +; CHECK-ABSOLUTE-PATH-L: define {{.*}}foo_func2{{.*}} +; CHECK-ABSOLUTE-PATH-L: define {{.*}}bar_func1{{.*}} +; CHECK-ABSOLUTE-PATH-L: define {{.*}}addFive{{.*}} +; CHECK-ABSOLUTE-PATH-L-NOT: define {{.*}}unusedFunc{{.*}} ;--- foo.ll target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" @@ -93,3 +190,23 @@ entry: %res = mul nsw i32 %a, 5 ret i32 %res } + +;--- addFive.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @addFive(i32 %a) { +entry: + %res = add nsw i32 %a, 5 + ret i32 %res +} + +;--- unusedFunc.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @unusedFunc(i32 %a) { +entry: + %res = mul nsw i32 %a, 5 + ret i32 %res +} diff --git a/clang/test/OffloadTools/clang-sycl-linker/split-mode.ll b/clang/test/OffloadTools/clang-sycl-linker/split-mode.ll index d10dbacf259fe..2def1e6d4d066 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/split-mode.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/split-mode.ll @@ -12,7 +12,7 @@ ; Test the split mode ("none"): kernels from different TUs are not split into separate images. ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t.bc -o %t-none.out 2>&1 \ ; RUN: | FileCheck %s --check-prefix=SPLIT-NONE -; SPLIT-NONE: link: inputs: {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; SPLIT-NONE: link: inputs: {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; SPLIT-NONE-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: {{.*}}_0.spv ; SPLIT-NONE-NEXT: sycl-bundle: image kind: spv, triple: spirv64, arch: {{$}} ; SPLIT-NONE-NOT: {{.+}} @@ -20,7 +20,7 @@ ; Test the split mode ("kernel"): each SPIR_KERNEL function produces its own device image. ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=kernel %t.bc -o %t-split-kernel.out 2>&1 \ ; RUN: | FileCheck %s --check-prefix=SPLIT-KERNEL -; SPLIT-KERNEL: link: inputs: {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; SPLIT-KERNEL: link: inputs: {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; SPLIT-KERNEL-NEXT: sycl-module-split: input: [[LLVMLINKOUT]].bc, mode: kernel ; SPLIT-KERNEL-NEXT: [[SPLIT0:.*]].bc [kernel_c ] ; SPLIT-KERNEL-NEXT: [[SPLIT1:.*]].bc [kernel_b ] @@ -43,7 +43,7 @@ ; Test per-TU split ('source' explicitly provided) ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=source %t.bc -o %t-src.out 2>&1 \ ; RUN: | FileCheck %s --check-prefix=SPLIT-SRC -; SPLIT-SRC: link: inputs: {{.*}}.bc libfiles: output: [[LLVMLINKOUT:.*]].bc +; SPLIT-SRC: link: inputs: {{.*}}.bc output: [[LLVMLINKOUT:.*]].bc ; SPLIT-SRC-NEXT: sycl-module-split: input: [[LLVMLINKOUT]].bc, mode: source ; SPLIT-SRC-NEXT: [[S0:.*]].bc [kernel_b kernel_c ] ; SPLIT-SRC-NEXT: [[S1:.*]].bc [kernel_a ] diff --git a/clang/test/OffloadTools/clang-sycl-linker/triple.ll b/clang/test/OffloadTools/clang-sycl-linker/triple.ll index c0e35b8fc9d36..f122194d9d9b2 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/triple.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/triple.ll @@ -63,6 +63,8 @@ define spir_kernel void @kernel_c() #0 { attributes #0 = { "sycl-module-id"="TU3.cpp" } ;--- no-triple.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" + define spir_kernel void @kernel_d() #0 { ret void } diff --git a/clang/test/OffloadTools/clang-sycl-linker/weak-symbols.ll b/clang/test/OffloadTools/clang-sycl-linker/weak-symbols.ll new file mode 100644 index 0000000000000..b14e1aeebf9a4 --- /dev/null +++ b/clang/test/OffloadTools/clang-sycl-linker/weak-symbols.ll @@ -0,0 +1,117 @@ +; Test weak symbol resolution semantics for clang-sycl-linker. +; +; REQUIRES: spirv-registered-target +; +; RUN: rm -rf %t && split-file %s %t +; RUN: llvm-as %t/main.ll -o %t/main.bc +; RUN: llvm-as %t/weak-archive.ll -o %t/weak-archive.bc +; RUN: llvm-as %t/strong-archive.ll -o %t/strong-archive.bc +; RUN: llvm-as %t/weak-main.ll -o %t/weak-main.bc +; RUN: llvm-as %t/strong-main.ll -o %t/strong-main.bc +; RUN: llvm-as %t/another-weak.ll -o %t/another-weak.bc +; RUN: llvm-ar rc %t/libweak.a %t/weak-archive.bc +; RUN: llvm-ar rc %t/libstrong.a %t/strong-archive.bc +; RUN: llvm-ar rc %t/libmixed.a %t/weak-archive.bc %t/strong-archive.bc +; RUN: llvm-ar rc %t/libanother.a %t/another-weak.bc +; +; Strong definition in main input takes precedence over weak in lazy archive. +; The weak definition in libweak.a should NOT be extracted because main.bc already +; defines commonFunc (strongly), so there's no undefined reference to resolve. +; RUN: clang-sycl-linker %t/main.bc -l weak -L %t --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-STRONG-WINS +; CHECK-STRONG-WINS: define{{.*}}i32 @commonFunc{{.*}} { +; CHECK-STRONG-WINS-NEXT: ret i32 42 +; CHECK-STRONG-WINS-NOT: ret i32 999 +; +; Weak definition in main, strong in lazy archive. +; When weak-main.bc references commonFunc weakly, the strong definition in +; libstrong.a should be extracted and take precedence. +; RUN: clang-sycl-linker %t/weak-main.bc -u commonFunc -l strong -L %t --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-STRONG-FROM-ARCHIVE +; CHECK-STRONG-FROM-ARCHIVE: define{{.*}}i32 @commonFunc{{.*}} { +; CHECK-STRONG-FROM-ARCHIVE-NEXT: ret i32 100 +; CHECK-STRONG-FROM-ARCHIVE-NOT: ret i32 999 +; +; Two weak definitions from different lazy archives. +; Both archives provide weak definitions. The first one encountered (by -L/-l order) +; should be taken. Here libweak.a comes before libanother.a, so weak-archive's +; version (ret 999) should be extracted when -u forces the symbol. +; RUN: clang-sycl-linker %t/strong-main.bc -u commonFunc -l weak -l another -L %t --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-FIRST-WEAK-WINS +; CHECK-FIRST-WEAK-WINS: define{{.*}}weak{{.*}}i32 @commonFunc{{.*}} { +; CHECK-FIRST-WEAK-WINS-NEXT: ret i32 999 +; CHECK-FIRST-WEAK-WINS-NOT: ret i32 777 +; +; Whole-archive with mixed weak and strong definitions in same archive. +; When --whole-archive forces extraction of all members, the strong definition +; should override the weak one. libmixed.a contains both weak-archive.bc and +; strong-archive.bc; the strong one should win. +; RUN: clang-sycl-linker %t/strong-main.bc --whole-archive -l mixed -L %t --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-WHOLE-STRONG-WINS +; CHECK-WHOLE-STRONG-WINS: define{{.*}}i32 @commonFunc{{.*}} { +; CHECK-WHOLE-STRONG-WINS-NEXT: ret i32 100 +; CHECK-WHOLE-STRONG-WINS-NOT: ret i32 999 +; +; Strong definition in one input, weak in another non-archive input. +; Both are non-lazy; the strong definition should be kept. +; RUN: clang-sycl-linker %t/strong-main.bc %t/weak-main.bc --dry-run -o /dev/null --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=CHECK-NON-LAZY-STRONG +; CHECK-NON-LAZY-STRONG: define{{.*}}i32 @mainFunc{{.*}} +; CHECK-NON-LAZY-STRONG: define{{.*}}i32 @commonFunc{{.*}} +; CHECK-NON-LAZY-STRONG-NOT: define{{.*}}weak{{.*}}@commonFunc + +;--- main.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @mainFunc() { + ret i32 0 +} + +define spir_func i32 @commonFunc() { + ret i32 42 +} + +;--- weak-archive.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define weak spir_func i32 @commonFunc() { + ret i32 999 +} + +;--- strong-archive.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @commonFunc() { + ret i32 100 +} + +;--- weak-main.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define weak spir_func i32 @commonFunc() { + ret i32 999 +} + +define spir_func i32 @weakMainFunc() { + ret i32 1 +} + +;--- strong-main.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define spir_func i32 @mainFunc() { + ret i32 2 +} + +;--- another-weak.ll +target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" +target triple = "spirv64" + +define weak spir_func i32 @commonFunc() { + ret i32 777 +} diff --git a/clang/tools/clang-sycl-linker/CMakeLists.txt b/clang/tools/clang-sycl-linker/CMakeLists.txt index a2104084dad87..84f24603ea87d 100644 --- a/clang/tools/clang-sycl-linker/CMakeLists.txt +++ b/clang/tools/clang-sycl-linker/CMakeLists.txt @@ -2,6 +2,7 @@ set(LLVM_LINK_COMPONENTS ${LLVM_TARGETS_TO_BUILD} Analysis BinaryFormat + BitReader BitWriter Core FrontendOffloading diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index 8a563b69f6949..ce76f5841dd52 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -18,9 +18,12 @@ #include "clang/Basic/OffloadArch.h" #include "clang/Basic/Version.h" +#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringMap.h" #include "llvm/ADT/StringSwitch.h" #include "llvm/BinaryFormat/Magic.h" +#include "llvm/Bitcode/BitcodeReader.h" #include "llvm/Bitcode/BitcodeWriter.h" #include "llvm/CodeGen/CommandFlags.h" #include "llvm/Frontend/Offloading/Utility.h" @@ -30,8 +33,10 @@ #include "llvm/LTO/LTO.h" #include "llvm/Linker/Linker.h" #include "llvm/MC/TargetRegistry.h" +#include "llvm/Object/Archive.h" #include "llvm/Object/Binary.h" #include "llvm/Object/IRObjectFile.h" +#include "llvm/Object/IRSymtab.h" #include "llvm/Object/OffloadBinary.h" #include "llvm/Option/ArgList.h" #include "llvm/Option/OptTable.h" @@ -41,6 +46,7 @@ #include "llvm/Support/FileSystem.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/InitLLVM.h" +#include "llvm/Support/MemoryBuffer.h" #include "llvm/Support/Path.h" #include "llvm/Support/Program.h" #include "llvm/Support/Signals.h" @@ -187,84 +193,338 @@ static Error executeCommands(StringRef ExecutablePath, return Error::success(); } -static Expected<SmallVector<std::string>> getInput(const ArgList &Args) { - // Collect all input bitcode files to be passed to the linking stage. - SmallVector<std::string> BitcodeFiles; - auto Inputs = Args.filtered(OPT_INPUT); - if (Inputs.empty()) - return createStringError("no input files provided"); - for (const opt::Arg *Arg : Inputs) { - StringRef Filename = Arg->getValue(); - if (!sys::fs::exists(Filename) || sys::fs::is_directory(Filename)) - return createStringError("input file '" + Filename + "' does not exist"); - file_magic Magic; - if (auto EC = identify_magic(Filename, Magic)) - return createStringError("failed to open file '" + Filename + "'"); - // TODO: Current use case involves LLVM IR bitcode files as input. - // This will be extended to support SPIR-V IR files. - if (Magic != file_magic::bitcode) - return createStringError("unsupported file type for '" + Filename + "'"); - BitcodeFiles.push_back(std::string(Filename)); +namespace { +/// A minimal symbol interface used to drive archive member extraction. Only the +/// flags required by the symbol-resolution fixed-point loop are tracked. +struct Symbol { + enum Flags { + None = 0, + Undefined = 1 << 0, + Weak = 1 << 1, + }; + + Symbol() : SymFlags(None) {} + Symbol(Symbol::Flags F) : SymFlags(F) {} + Symbol(const irsymtab::Reader::SymbolRef Sym) : SymFlags(0) { + if (Sym.isUndefined()) + SymFlags |= Undefined; + if (Sym.isWeak()) + SymFlags |= Weak; } - return BitcodeFiles; -} -/// Handle cases where input file is a LLVM IR bitcode file. -/// When clang-sycl-linker is called via clang-linker-wrapper tool, input files -/// are LLVM IR bitcode files. -// TODO: Support SPIR-V IR files. -static Expected<std::unique_ptr<Module>> getBitcodeModule(StringRef File, - LLVMContext &C) { - SMDiagnostic Err; + bool isWeak() const { return SymFlags & Weak; } + bool isUndefined() const { return SymFlags & Undefined; } - auto M = getLazyIRFileModule(File, Err, C); - if (M) - return std::move(M); - return createStringError(Err.getMessage()); -} + uint32_t SymFlags; +}; + +/// Description of a single input (positional file or -l library). +struct InputDesc { + enum class Kind { File, Library }; + + StringRef Value; // File path, or library name for -l (the value after -l). + Kind InputKind = Kind::File; + bool WholeArchive = false; // --whole-archive state in effect at this input. +}; + +/// An input buffer pending archive-member resolution, together with its parsed +/// IR symbol table. The symbol table is parsed once and reused across all +/// fixed-point passes so members are not re-parsed on every pass. +struct PendingInput { + std::unique_ptr<MemoryBuffer> Buffer; + bool IsLazy = false; + bool FromArchive = false; + IRSymtabFile Symtab; +}; + +/// Resolved input buffers and their target triple. +struct ResolvedInputs { + SmallVector<std::unique_ptr<MemoryBuffer>> Buffers; + llvm::Triple TargetTriple; + StringRef TripleSource; // Source of the triple (--triple= or filename) +}; +} // namespace static std::optional<std::string> findFile(StringRef Dir, const Twine &Name) { - SmallString<128> Path(Dir); - llvm::sys::path::append(Path, Name); + SmallString<128> Path; + sys::path::append(Path, Dir, Name); + // Skip directories so a directory whose name matches the requested library + // does not stop the search; a later -L path may hold the real archive. if (sys::fs::exists(Path) && !sys::fs::is_directory(Path)) - return std::string(Path); + return static_cast<std::string>(Path); return std::nullopt; } static std::optional<std::string> -searchLibrary(StringRef Name, ArrayRef<StringRef> SearchPaths) { - // An absolute path is taken as-is; -L paths are only consulted for relative - // names. - if (sys::path::is_absolute(Name)) { - if (sys::fs::exists(Name) && !sys::fs::is_directory(Name)) - return std::string(Name); - return std::nullopt; - } +findFromSearchPaths(StringRef Name, ArrayRef<StringRef> SearchPaths) { for (StringRef Dir : SearchPaths) if (std::optional<std::string> File = findFile(Dir, Name)) return File; return std::nullopt; } -/// Gather all library files. The list of files and its location are passed from -/// driver. -static Expected<SmallVector<std::string>> -getBCLibraryNames(const ArgList &Args) { +/// Search for static libraries in the linker's library path given input like +/// `-lfoo`, `-l:libfoo.a`, or `-l/absolute/path/to/lib.a`. +static std::optional<std::string> searchLibrary(StringRef Input, + ArrayRef<StringRef> SearchPaths) { + // An absolute path is taken as-is; -L paths are only consulted for relative + // names. + if (sys::path::is_absolute(Input)) { + if (sys::fs::exists(Input) && !sys::fs::is_directory(Input)) + return Input.str(); + return std::nullopt; + } + + if (Input.starts_with(":")) + return findFromSearchPaths(Input.drop_front(), SearchPaths); + SmallString<128> LibName("lib"); + LibName += Input; + LibName += ".a"; + return findFromSearchPaths(LibName, SearchPaths); +} + +/// Scan a member's pre-parsed IR symbol table against \p SymTab and return true +/// if the member should be extracted: it is non-lazy, or it defines a symbol +/// that resolves a currently-undefined reference. Mirrors a linker's archive +/// member selection. +static bool scanSymbols(const IRSymtabFile &Symtab, StringMap<Symbol> &SymTab, + bool IsLazy) { + bool Extracted = !IsLazy; + StringMap<Symbol> PendingSymbols; + for (unsigned I = 0; I != Symtab.Mods.size(); ++I) { + for (const auto &IRSym : Symtab.TheReader.module_symbols(I)) { + if (IRSym.isFormatSpecific() || !IRSym.isGlobal()) + continue; + + bool IsNewSymbol = IsLazy && !SymTab.count(IRSym.getName()); + StringMap<Symbol> &Target = IsNewSymbol ? PendingSymbols : SymTab; + Symbol &OldSym = Target[IRSym.getName()]; + Symbol Sym(IRSym); + + if (OldSym.SymFlags == Symbol::None) { + OldSym = Sym; + if (!IsNewSymbol) + continue; + } + + bool ResolvesReference = + !Sym.isUndefined() && + (OldSym.isUndefined() || (OldSym.isWeak() && !Sym.isWeak())) && + !(OldSym.isWeak() && OldSym.isUndefined() && IsLazy); + Extracted |= ResolvesReference; + + if (ResolvesReference) + OldSym = Sym; + } + } + if (Extracted && IsLazy) + for (const auto &[Name, Sym] : PendingSymbols) + SymTab[Name] = Sym; + return Extracted; +} + +/// Parse \p Buffer's IR symbol table and append it to \p Inputs. Errors if the +/// buffer is not LLVM bitcode (the only member type the SYCL linker supports). +static Error addBitcodeInput(SmallVector<PendingInput> &Inputs, + std::unique_ptr<MemoryBuffer> Buffer, bool IsLazy, + bool FromArchive) { + if (identify_magic(Buffer->getBuffer()) != file_magic::bitcode) + return createStringError("unsupported file type: '" + + Buffer->getBufferIdentifier() + "'"); + Expected<IRSymtabFile> SymtabOrErr = readIRSymtab(Buffer->getMemBufferRef()); + if (!SymtabOrErr) + return SymtabOrErr.takeError(); + Inputs.push_back( + {std::move(Buffer), IsLazy, FromArchive, std::move(*SymtabOrErr)}); + return Error::success(); +} + +/// Resolve archive members from the given inputs using a symbol-driven +/// fixed-point algorithm. For each input: +/// - If it's a Library, search for lib<name>.a or :<name> in SearchPaths +/// - If it's a File, use the path directly +/// - Archives are expanded and members are lazily extracted based on symbol +/// references unless WholeArchive is true +/// - Non-archive bitcode inputs are always included +/// +/// Returns the buffers to link, in extraction order, along with the resolved +/// target triple. All returned buffers have compatible target triples; +/// incompatible archive members are filtered during resolution. +static Expected<ResolvedInputs> +resolveArchiveMembers(ArrayRef<InputDesc> Order, ArrayRef<StringRef> SearchPaths, + ArrayRef<StringRef> ForcedUndefs, StringRef TargetTripleArgValue) { + // Collect every candidate member, parsing each one's IR symbol table once. + SmallVector<PendingInput> Inputs; + + for (const InputDesc &Desc : Order) { + std::optional<std::string> Filename; + + if (Desc.InputKind == InputDesc::Kind::Library) { + Filename = searchLibrary(Desc.Value, SearchPaths); + if (!Filename) + return createStringError("unable to find library -l" + Desc.Value); + } else { + if (!sys::fs::exists(Desc.Value)) + return createStringError("input file not found: '" + Desc.Value + "'"); + if (sys::fs::is_directory(Desc.Value)) + return createStringError("'" + Desc.Value + "': Is a directory"); + Filename = Desc.Value.str(); + } + + auto BufferOrErr = + errorOrToExpected(MemoryBuffer::getFileOrSTDIN(*Filename)); + if (!BufferOrErr) + return createFileError(*Filename, BufferOrErr.takeError()); + + MemoryBufferRef Buffer = (*BufferOrErr)->getMemBufferRef(); + switch (identify_magic(Buffer.getBuffer())) { + case file_magic::bitcode: + if (Error Err = addBitcodeInput(Inputs, std::move(*BufferOrErr), + /*IsLazy=*/false, /*FromArchive=*/false)) + return Err; + break; + case file_magic::archive: { + Expected<std::unique_ptr<object::Archive>> LibFile = + object::Archive::create(Buffer); + if (!LibFile) + return LibFile.takeError(); + Error Err = Error::success(); + for (auto Child : (*LibFile)->children(Err)) { + auto ChildBufferOrErr = Child.getMemoryBufferRef(); + if (!ChildBufferOrErr) + return ChildBufferOrErr.takeError(); + // Include archive name in buffer identifier for better diagnostics. + std::string BufferIdentifier = + (*Filename + "(" + ChildBufferOrErr->getBufferIdentifier() + ")") + .str(); + std::unique_ptr<MemoryBuffer> ChildBuffer = + MemoryBuffer::getMemBufferCopy(ChildBufferOrErr->getBuffer(), + BufferIdentifier); + if (Error E = addBitcodeInput(Inputs, std::move(ChildBuffer), + !Desc.WholeArchive, /*FromArchive=*/true)) + return E; + } + if (Err) + return Err; + break; + } + default: + return createStringError("unsupported file type: '" + *Filename + "'"); + } + } + + // Resolve the target triple: use --triple= if provided, otherwise infer from + // the first non-archive input with a non-empty triple. + llvm::Triple TargetTriple(TargetTripleArgValue); + StringRef TripleSource = TargetTriple.empty() ? "" : "--triple="; + + if (TargetTriple.empty()) { + for (const PendingInput &In : Inputs) { + if (!In.FromArchive && In.Symtab.Mods.size() > 0) { + StringRef Triple = In.Symtab.TheReader.getTargetTriple(); + if (!Triple.empty()) { + TargetTriple = llvm::Triple(Triple); + TripleSource = In.Buffer->getBufferIdentifier(); + break; + } + } + } + } + + // Seed symbol table with forced undefined symbols. + StringMap<Symbol> SymTab; + for (StringRef Sym : ForcedUndefs) + SymTab[Sym] = Symbol(Symbol::Undefined); + + // Fixed-point loop to extract archive members. Each pass may resolve symbols + // that unlock further members; iterate until no new member is extracted. + SmallVector<std::unique_ptr<MemoryBuffer>> Resolved; + bool Extracted = true; + while (Extracted) { + Extracted = false; + for (PendingInput &In : Inputs) { + if (!In.Buffer) + continue; + + // Filter archive members by target triple before symbol scanning. + // Members built for a different target are silently skipped, matching how + // a real linker treats device libraries built for other architectures. + if (In.FromArchive) { + StringRef MemberTriple = In.Symtab.TheReader.getTargetTriple(); + if (!MemberTriple.empty() && MemberTriple != TargetTriple.str()) { + if (Verbose) + errs() << formatv( + "archive resolution: skipping {0}: triple {1} != {2}\n", + In.Buffer->getBufferIdentifier(), MemberTriple, + TargetTriple.str()); + In.Buffer.reset(); + continue; + } + } + + if (!scanSymbols(In.Symtab, SymTab, In.IsLazy)) + continue; + Extracted = true; + Resolved.push_back(std::move(In.Buffer)); + } + } + + return ResolvedInputs{std::move(Resolved), std::move(TargetTriple), TripleSource}; +} + +static Expected<ResolvedInputs> +getInput(const ArgList &Args) { + // Build input descriptors for the archive resolver. + SmallVector<InputDesc> InputDescs; + bool WholeArchive = false; + for (const opt::Arg *Arg : Args.filtered( + OPT_INPUT, OPT_library, OPT_whole_archive, OPT_no_whole_archive)) { + if (Arg->getOption().matches(OPT_whole_archive) || + Arg->getOption().matches(OPT_no_whole_archive)) { + WholeArchive = Arg->getOption().matches(OPT_whole_archive); + continue; + } + + InputDesc Desc; + Desc.Value = Arg->getValue(); + Desc.InputKind = Arg->getOption().matches(OPT_library) + ? InputDesc::Kind::Library + : InputDesc::Kind::File; + Desc.WholeArchive = WholeArchive; + InputDescs.push_back(Desc); + } + + if (InputDescs.empty()) + return createStringError("no input files provided"); + + // Gather search paths and forced undefined symbols. SmallVector<StringRef> LibraryPaths; for (const opt::Arg *Arg : Args.filtered(OPT_library_path)) LibraryPaths.push_back(Arg->getValue()); - SmallVector<std::string> LibraryFiles; - for (const opt::Arg *Arg : Args.filtered(OPT_bc_library)) { - std::optional<std::string> LibName = - searchLibrary(Arg->getValue(), LibraryPaths); - if (!LibName) - return createStringError("'" + Twine(Arg->getValue()) + - "' library file not found"); - LibraryFiles.push_back(std::move(*LibName)); - } + // getAllArgValues returns a temporary vector; retain it so the StringRefs + // remain valid through the resolveArchiveMembers call. + std::vector<std::string> ForcedUndefStorage = Args.getAllArgValues(OPT_u); + SmallVector<StringRef> ForcedUndefs(ForcedUndefStorage.begin(), + ForcedUndefStorage.end()); - return LibraryFiles; + // Get target triple from command line if specified. + StringRef TargetTripleStr = Args.getLastArgValue(OPT_triple_EQ); + + Expected<ResolvedInputs> ResolvedOrErr = + resolveArchiveMembers(InputDescs, LibraryPaths, ForcedUndefs, + TargetTripleStr); + if (!ResolvedOrErr) + return ResolvedOrErr.takeError(); + + if (ResolvedOrErr->Buffers.empty()) + return createStringError("no input files could be resolved"); + + if (ResolvedOrErr->TargetTriple.empty()) + return createStringError( + "target triple must be specified or inferable from inputs"); + + return std::move(*ResolvedOrErr); } namespace { @@ -275,24 +535,18 @@ struct LinkResult { }; } // namespace -/// Following tasks are performed: -/// 1. Resolve the target triple: use --triple= when given, otherwise take the -/// first input that supplies a triple as canonical. Issue an error if any -/// triple inputs disagree. -/// 2. Link all input bitcode images into one image using the linkInModule API. -/// 3. Gather all library bitcode images. -/// 4. Link all the images gathered in Step 3 with the output of Step 2 using -/// linkInModule API. LinkOnlyNeeded flag is used. -static Expected<LinkResult> linkInputs(ArrayRef<std::string> InputFiles, - const ArgList &Args, LLVMContext &C) { +/// Link all resolved input bitcode images into one module. All resolved inputs +/// are guaranteed to have compatible target triples (incompatible archive +/// members are filtered during archive resolution). Triple conflicts between +/// regular (non-archive) inputs are hard errors caught during resolution. +static Expected<LinkResult> +linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> Inputs, + const llvm::Triple &TargetTriple, + StringRef TripleSource, + const ArgList &Args, LLVMContext &C) { llvm::TimeTraceScope TimeScope("Link code"); - assert(InputFiles.size() && "No inputs to link"); - - // Get all library files. - Expected<SmallVector<std::string>> BCLibFiles = getBCLibraryNames(Args); - if (!BCLibFiles) - return BCLibFiles.takeError(); + assert(Inputs.size() && "No inputs to link"); // Create a new file to write the linked file to. auto BitcodeOutput = @@ -301,56 +555,38 @@ static Expected<LinkResult> linkInputs(ArrayRef<std::string> InputFiles, return BitcodeOutput.takeError(); if (Verbose) { - std::string Inputs = llvm::join(InputFiles.begin(), InputFiles.end(), ", "); - std::string LibInputs = - llvm::join((*BCLibFiles).begin(), (*BCLibFiles).end(), ", "); - errs() << formatv("link: inputs: {0} libfiles: {1} output: {2}\n", Inputs, - LibInputs, *BitcodeOutput); + std::string InputList = llvm::join( + llvm::map_range(Inputs, + [](const auto &Buffer) { + return Buffer->getBufferIdentifier(); + }), + ", "); + errs() << formatv("link: inputs: {0} output: {1}\n", InputList, + *BitcodeOutput); } - // Link input files. Resolve the target triple. - llvm::Triple TargetTriple(Args.getLastArgValue(OPT_triple_EQ)); - StringRef TripleSource = TargetTriple.empty() ? "" : "--triple="; auto LinkerOutput = std::make_unique<Module>("linker-output", C); Linker L(*LinkerOutput); - for (auto &File : InputFiles) { - auto ModOrErr = getBitcodeModule(File, C); + for (const auto &Buffer : Inputs) { + auto ModOrErr = parseBitcodeFile(Buffer->getMemBufferRef(), C); if (!ModOrErr) return ModOrErr.takeError(); const llvm::Triple &T = (*ModOrErr)->getTargetTriple(); if (!T.empty() && T != TargetTriple) { - if (TargetTriple.empty()) { - TargetTriple = T; - TripleSource = File; - } else { - return createStringError( - "conflicting target triples: '" + TargetTriple.str() + "' (from " + - TripleSource + ") vs '" + T.str() + "' (from " + File + ")"); - } + // All incompatible archive members should have been filtered during + // resolution, so this is a conflict between regular inputs. + return createStringError( + "conflicting target triples: '" + TargetTriple.str() + "' (from " + + TripleSource + ") vs '" + T.str() + "' (from " + + Buffer->getBufferIdentifier() + ")"); } if (L.linkInModule(std::move(*ModOrErr))) return createStringError("could not link IR"); } - if (TargetTriple.empty()) - return createStringError( - "target triple must be specified or inferable from inputs"); - - // Link in library files. - for (auto &File : *BCLibFiles) { - auto LibMod = getBitcodeModule(File, C); - if (!LibMod) - return LibMod.takeError(); - if ((*LibMod)->getTargetTriple() == TargetTriple) { - unsigned Flags = Linker::Flags::LinkOnlyNeeded; - if (L.linkInModule(std::move(*LibMod), Flags)) - return createStringError("could not link IR"); - } - } - // Dump linked output for testing. if (Args.hasArg(OPT_print_linked_module)) outs() << *LinkerOutput; @@ -693,13 +929,16 @@ static bool canSkipModuleSplit(IRSplitMode Mode, const Module &M, /// 4. Optionally run AOT compilation when targeting an Intel HW arch. /// 5. Pack the resulting images into a single OffloadBinary written to the /// output file. -static Error runSYCLLink(ArrayRef<std::string> Files, const ArgList &Args) { +static Error runSYCLLink(ArrayRef<std::unique_ptr<MemoryBuffer>> Inputs, + const llvm::Triple &TargetTriple, + StringRef TripleSource, + const ArgList &Args) { llvm::TimeTraceScope TimeScope("SYCL linking"); LLVMContext C; // Link all input bitcode files and library files. - Expected<LinkResult> LinkedOrErr = linkInputs(Files, Args, C); + Expected<LinkResult> LinkedOrErr = linkInputs(Inputs, TargetTriple, TripleSource, Args, C); if (!LinkedOrErr) return LinkedOrErr.takeError(); LinkResult &Result = *LinkedOrErr; @@ -849,10 +1088,10 @@ int main(int argc, char **argv) { reportError(createStringError("output file must be specified")); OutputFile = Args.getLastArgValue(OPT_o); - // Get the input files to pass to the linking stage. - auto FilesOrErr = getInput(Args); - if (!FilesOrErr) - reportError(FilesOrErr.takeError()); + // Get the input buffers to pass to the linking stage. + auto ResolvedInputsOrErr = getInput(Args); + if (!ResolvedInputsOrErr) + reportError(ResolvedInputsOrErr.takeError()); if (auto *A = Args.getLastArg(OPT_spirv_dump_device_code_EQ)) { StringRef V = A->getValue(); @@ -871,7 +1110,9 @@ int main(int argc, char **argv) { } // Run SYCL linking process on the generated inputs. - if (Error Err = runSYCLLink(*FilesOrErr, Args)) + if (Error Err = runSYCLLink(ResolvedInputsOrErr->Buffers, + ResolvedInputsOrErr->TargetTriple, + ResolvedInputsOrErr->TripleSource, Args)) reportError(std::move(Err)); // Remove the temporary files created. diff --git a/clang/tools/clang-sycl-linker/SYCLLinkOpts.td b/clang/tools/clang-sycl-linker/SYCLLinkOpts.td index e00e63aa1767d..40f758cc7d837 100644 --- a/clang/tools/clang-sycl-linker/SYCLLinkOpts.td +++ b/clang/tools/clang-sycl-linker/SYCLLinkOpts.td @@ -24,12 +24,21 @@ def library_path_S : Separate<["--", "-"], "library-path">, Flags<[HelpHidden]>, def library_path_EQ : Joined<["--", "-"], "library-path=">, Flags<[HelpHidden]>, Alias<library_path>; -def bc_library : Separate<["--", "-"], "bc-library">, MetaVarName<"<name>">, - HelpText<"Add LLVM bitcode library <name> (with extension) to the link. A " - "relative <name> is resolved against -L paths; an absolute path is " - "taken as-is.">; -def bc_library_EQ : Joined<["--", "-"], "bc-library=">, Flags<[HelpHidden]>, - Alias<bc_library>; +def library : JoinedOrSeparate<["-"], "l">, MetaVarName<"<libname>">, + HelpText<"Search for library <libname>">; +def library_S : Separate<["--", "-"], "library">, Flags<[HelpHidden]>, + Alias<library>; +def library_EQ : Joined<["--", "-"], "library=">, Flags<[HelpHidden]>, + Alias<library>; + +def whole_archive : Flag<["--", "-"], "whole-archive">, + HelpText<"Include all archive members in the link">; +def no_whole_archive : Flag<["--", "-"], "no-whole-archive">, + HelpText<"Only include archive members that resolve undefined symbols (default)">; + +def u : JoinedOrSeparate<["-"], "u">, MetaVarName<"<symbol>">, + HelpText<"Force undefined symbol during linking">; +def undefined : JoinedOrSeparate<["--"], "undefined">, Alias<u>; def arch_EQ : Joined<["--", "-"], "arch=">, Flags<[LinkerOnlyOption]>, >From 7c75be53cb96294e2f280600d2e1c753e3f4df06 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Tue, 9 Jun 2026 21:14:42 -0700 Subject: [PATCH 2/8] Fix formatting and basic.ll check on Windows. --- .../OffloadTools/clang-sycl-linker/basic.ll | 2 +- .../clang-sycl-linker/ClangSYCLLinker.cpp | 50 +++++++++---------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/clang/test/OffloadTools/clang-sycl-linker/basic.ll b/clang/test/OffloadTools/clang-sycl-linker/basic.ll index 564ef6a67f3e9..f3c11b170e02b 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/basic.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/basic.ll @@ -41,7 +41,7 @@ ; RUN: clang-sycl-linker --dry-run -v --module-split-mode=none %t/input1.bc %t/input2.bc --library-path=%t/libs --whole-archive -l device -o /dev/null 2>&1 \ ; RUN: | FileCheck %s --check-prefix=DEVLIBS ; DEVLIBS: link: inputs: {{.*}}.bc, {{.*}}.bc, {{.*}}libdevice.a(lib1.bc), {{.*}}libdevice.a(lib2.bc) output: [[LLVMLINKOUT:.*]].bc -; DEVLIBS-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: /dev/null_0.spv +; DEVLIBS-NEXT: LLVM backend: input: [[LLVMLINKOUT]].bc, output: {{.*}}_0.spv ; DEVLIBS-NEXT: sycl-bundle: image kind: spv, triple: spirv64, arch: {{$}} ; DEVLIBS-NOT: {{.+}} ; diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index ce76f5841dd52..49b0de703a87e 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -265,8 +265,8 @@ findFromSearchPaths(StringRef Name, ArrayRef<StringRef> SearchPaths) { /// Search for static libraries in the linker's library path given input like /// `-lfoo`, `-l:libfoo.a`, or `-l/absolute/path/to/lib.a`. -static std::optional<std::string> searchLibrary(StringRef Input, - ArrayRef<StringRef> SearchPaths) { +static std::optional<std::string> +searchLibrary(StringRef Input, ArrayRef<StringRef> SearchPaths) { // An absolute path is taken as-is; -L paths are only consulted for relative // names. if (sys::path::is_absolute(Input)) { @@ -350,9 +350,9 @@ static Error addBitcodeInput(SmallVector<PendingInput> &Inputs, /// Returns the buffers to link, in extraction order, along with the resolved /// target triple. All returned buffers have compatible target triples; /// incompatible archive members are filtered during resolution. -static Expected<ResolvedInputs> -resolveArchiveMembers(ArrayRef<InputDesc> Order, ArrayRef<StringRef> SearchPaths, - ArrayRef<StringRef> ForcedUndefs, StringRef TargetTripleArgValue) { +static Expected<ResolvedInputs> resolveArchiveMembers( + ArrayRef<InputDesc> Order, ArrayRef<StringRef> SearchPaths, + ArrayRef<StringRef> ForcedUndefs, StringRef TargetTripleArgValue) { // Collect every candidate member, parsing each one's IR symbol table once. SmallVector<PendingInput> Inputs; @@ -469,11 +469,11 @@ resolveArchiveMembers(ArrayRef<InputDesc> Order, ArrayRef<StringRef> SearchPaths } } - return ResolvedInputs{std::move(Resolved), std::move(TargetTriple), TripleSource}; + return ResolvedInputs{std::move(Resolved), std::move(TargetTriple), + TripleSource}; } -static Expected<ResolvedInputs> -getInput(const ArgList &Args) { +static Expected<ResolvedInputs> getInput(const ArgList &Args) { // Build input descriptors for the archive resolver. SmallVector<InputDesc> InputDescs; bool WholeArchive = false; @@ -511,9 +511,8 @@ getInput(const ArgList &Args) { // Get target triple from command line if specified. StringRef TargetTripleStr = Args.getLastArgValue(OPT_triple_EQ); - Expected<ResolvedInputs> ResolvedOrErr = - resolveArchiveMembers(InputDescs, LibraryPaths, ForcedUndefs, - TargetTripleStr); + Expected<ResolvedInputs> ResolvedOrErr = resolveArchiveMembers( + InputDescs, LibraryPaths, ForcedUndefs, TargetTripleStr); if (!ResolvedOrErr) return ResolvedOrErr.takeError(); @@ -541,8 +540,7 @@ struct LinkResult { /// regular (non-archive) inputs are hard errors caught during resolution. static Expected<LinkResult> linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> Inputs, - const llvm::Triple &TargetTriple, - StringRef TripleSource, + const llvm::Triple &TargetTriple, StringRef TripleSource, const ArgList &Args, LLVMContext &C) { llvm::TimeTraceScope TimeScope("Link code"); @@ -555,12 +553,12 @@ linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> Inputs, return BitcodeOutput.takeError(); if (Verbose) { - std::string InputList = llvm::join( - llvm::map_range(Inputs, - [](const auto &Buffer) { - return Buffer->getBufferIdentifier(); - }), - ", "); + std::string InputList = + llvm::join(llvm::map_range(Inputs, + [](const auto &Buffer) { + return Buffer->getBufferIdentifier(); + }), + ", "); errs() << formatv("link: inputs: {0} output: {1}\n", InputList, *BitcodeOutput); } @@ -577,10 +575,10 @@ linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> Inputs, if (!T.empty() && T != TargetTriple) { // All incompatible archive members should have been filtered during // resolution, so this is a conflict between regular inputs. - return createStringError( - "conflicting target triples: '" + TargetTriple.str() + "' (from " + - TripleSource + ") vs '" + T.str() + "' (from " + - Buffer->getBufferIdentifier() + ")"); + return createStringError("conflicting target triples: '" + + TargetTriple.str() + "' (from " + TripleSource + + ") vs '" + T.str() + "' (from " + + Buffer->getBufferIdentifier() + ")"); } if (L.linkInModule(std::move(*ModOrErr))) @@ -931,14 +929,14 @@ static bool canSkipModuleSplit(IRSplitMode Mode, const Module &M, /// output file. static Error runSYCLLink(ArrayRef<std::unique_ptr<MemoryBuffer>> Inputs, const llvm::Triple &TargetTriple, - StringRef TripleSource, - const ArgList &Args) { + StringRef TripleSource, const ArgList &Args) { llvm::TimeTraceScope TimeScope("SYCL linking"); LLVMContext C; // Link all input bitcode files and library files. - Expected<LinkResult> LinkedOrErr = linkInputs(Inputs, TargetTriple, TripleSource, Args, C); + Expected<LinkResult> LinkedOrErr = + linkInputs(Inputs, TargetTriple, TripleSource, Args, C); if (!LinkedOrErr) return LinkedOrErr.takeError(); LinkResult &Result = *LinkedOrErr; >From af9ddd0abb6f827d4b336383997e00c2b4df43b7 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Wed, 10 Jun 2026 17:29:51 -0700 Subject: [PATCH 3/8] Update clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp Co-authored-by: Yury Plyakhin <[email protected]> --- clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index 49b0de703a87e..904d0ff8ba09e 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -537,7 +537,7 @@ struct LinkResult { /// Link all resolved input bitcode images into one module. All resolved inputs /// are guaranteed to have compatible target triples (incompatible archive /// members are filtered during archive resolution). Triple conflicts between -/// regular (non-archive) inputs are hard errors caught during resolution. +/// regular (non-archive) inputs are hard errors caught before running linkInModule. static Expected<LinkResult> linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> Inputs, const llvm::Triple &TargetTriple, StringRef TripleSource, >From b49c86e13c692469c257f7a4ad55e14366ded684 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Wed, 10 Jun 2026 17:32:51 -0700 Subject: [PATCH 4/8] Fix formatting. --- clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index 904d0ff8ba09e..764815279cbb3 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -537,7 +537,8 @@ struct LinkResult { /// Link all resolved input bitcode images into one module. All resolved inputs /// are guaranteed to have compatible target triples (incompatible archive /// members are filtered during archive resolution). Triple conflicts between -/// regular (non-archive) inputs are hard errors caught before running linkInModule. +/// regular (non-archive) inputs are hard errors caught before running +/// linkInModule. static Expected<LinkResult> linkInputs(ArrayRef<std::unique_ptr<MemoryBuffer>> Inputs, const llvm::Triple &TargetTriple, StringRef TripleSource, >From b109c264f98718ecd8245935fc6ad9d52170b084 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Wed, 10 Jun 2026 18:20:34 -0700 Subject: [PATCH 5/8] Address code review feedback. --- .../OffloadTools/clang-sycl-linker/triple.ll | 30 +++++++++++++++++++ .../clang-sycl-linker/ClangSYCLLinker.cpp | 4 ++- clang/tools/clang-sycl-linker/SYCLLinkOpts.td | 4 ++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/clang/test/OffloadTools/clang-sycl-linker/triple.ll b/clang/test/OffloadTools/clang-sycl-linker/triple.ll index f122194d9d9b2..af347cb352e5e 100644 --- a/clang/test/OffloadTools/clang-sycl-linker/triple.ll +++ b/clang/test/OffloadTools/clang-sycl-linker/triple.ll @@ -32,6 +32,16 @@ ; RUN: not clang-sycl-linker --dry-run %t/no-triple.bc -o a.out 2>&1 \ ; RUN: | FileCheck %s --check-prefix=NO-TRIPLE ; NO-TRIPLE: target triple must be specified or inferable from inputs +; +; Test that archive members with equivalent but textually different triples are +; not filtered: spirv64 from --triple= and spirv64-unknown-unknown from the +; archive member should match and the member should be extracted. +; RUN: llvm-as %t/archive-full-triple.ll -o %t/archive-full-triple.bc +; RUN: llvm-ar rc %t/libfulltriple.a %t/archive-full-triple.bc +; RUN: llvm-as %t/main-undefined.ll -o %t/main-undefined.bc +; RUN: clang-sycl-linker --dry-run -triple=spirv64 %t/main-undefined.bc -l fulltriple -L %t -o %t/fulltriple.out --print-linked-module 2>&1 \ +; RUN: | FileCheck %s --check-prefix=ARCHIVE-FULL-TRIPLE +; ARCHIVE-FULL-TRIPLE: define {{.*}}archiveFunc{{.*}} ;--- input1.ll target datalayout = "e-i64:64-v16:16-v24:32-v32:32-v48:64-v96:128-v192:256-v256:256-v512:512-v1024:1024-n8:16:32:64-G1" @@ -70,3 +80,23 @@ define spir_kernel void @kernel_d() #0 { } attributes #0 = { "sycl-module-id"="TU4.cpp" } + +;--- archive-full-triple.ll +target triple = "spirv64-unknown-unknown" + +define spir_func i32 @archiveFunc(i32 %a) { +entry: + %res = add nsw i32 %a, 42 + ret i32 %res +} + +;--- main-undefined.ll +target triple = "spirv64" + +declare spir_func i32 @archiveFunc(i32) + +define spir_kernel void @main_kernel() { +entry: + %result = call spir_func i32 @archiveFunc(i32 10) + ret void +} diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index 764815279cbb3..41cecf59dd15d 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -451,13 +451,15 @@ static Expected<ResolvedInputs> resolveArchiveMembers( // a real linker treats device libraries built for other architectures. if (In.FromArchive) { StringRef MemberTriple = In.Symtab.TheReader.getTargetTriple(); - if (!MemberTriple.empty() && MemberTriple != TargetTriple.str()) { + if (!MemberTriple.empty() && + llvm::Triple(MemberTriple) != TargetTriple) { if (Verbose) errs() << formatv( "archive resolution: skipping {0}: triple {1} != {2}\n", In.Buffer->getBufferIdentifier(), MemberTriple, TargetTriple.str()); In.Buffer.reset(); + In.Symtab = {}; continue; } } diff --git a/clang/tools/clang-sycl-linker/SYCLLinkOpts.td b/clang/tools/clang-sycl-linker/SYCLLinkOpts.td index 40f758cc7d837..0f981c3c90660 100644 --- a/clang/tools/clang-sycl-linker/SYCLLinkOpts.td +++ b/clang/tools/clang-sycl-linker/SYCLLinkOpts.td @@ -34,11 +34,13 @@ def library_EQ : Joined<["--", "-"], "library=">, Flags<[HelpHidden]>, def whole_archive : Flag<["--", "-"], "whole-archive">, HelpText<"Include all archive members in the link">; def no_whole_archive : Flag<["--", "-"], "no-whole-archive">, - HelpText<"Only include archive members that resolve undefined symbols (default)">; + HelpText<"Disable --whole-archive for the following inputs (default state)">; def u : JoinedOrSeparate<["-"], "u">, MetaVarName<"<symbol>">, HelpText<"Force undefined symbol during linking">; def undefined : JoinedOrSeparate<["--"], "undefined">, Alias<u>; +def undefined_EQ : Joined<["--"], "undefined=">, Flags<[HelpHidden]>, + Alias<u>; def arch_EQ : Joined<["--", "-"], "arch=">, Flags<[LinkerOnlyOption]>, >From 2e7245b53eb8cd9493dcee797eb8c70d0f77c05c Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Wed, 10 Jun 2026 19:04:18 -0700 Subject: [PATCH 6/8] Update scanSymbols parameter names. --- .../clang-sycl-linker/ClangSYCLLinker.cpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index 41cecf59dd15d..057e16654be7f 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -283,21 +283,21 @@ searchLibrary(StringRef Input, ArrayRef<StringRef> SearchPaths) { return findFromSearchPaths(LibName, SearchPaths); } -/// Scan a member's pre-parsed IR symbol table against \p SymTab and return true -/// if the member should be extracted: it is non-lazy, or it defines a symbol -/// that resolves a currently-undefined reference. Mirrors a linker's archive -/// member selection. -static bool scanSymbols(const IRSymtabFile &Symtab, StringMap<Symbol> &SymTab, - bool IsLazy) { +/// Scan a member's pre-parsed IR symbol table against \p LinkerSymtab and +/// return true if the member should be extracted: it is non-lazy, or it defines +/// a symbol that resolves a currently-undefined reference. Mirrors a linker's +/// archive member selection. +static bool scanSymbols(const IRSymtabFile &MemberSymtab, + StringMap<Symbol> &LinkerSymtab, bool IsLazy) { bool Extracted = !IsLazy; StringMap<Symbol> PendingSymbols; - for (unsigned I = 0; I != Symtab.Mods.size(); ++I) { - for (const auto &IRSym : Symtab.TheReader.module_symbols(I)) { + for (unsigned I = 0; I != MemberSymtab.Mods.size(); ++I) { + for (const auto &IRSym : MemberSymtab.TheReader.module_symbols(I)) { if (IRSym.isFormatSpecific() || !IRSym.isGlobal()) continue; - bool IsNewSymbol = IsLazy && !SymTab.count(IRSym.getName()); - StringMap<Symbol> &Target = IsNewSymbol ? PendingSymbols : SymTab; + bool IsNewSymbol = IsLazy && !LinkerSymtab.count(IRSym.getName()); + StringMap<Symbol> &Target = IsNewSymbol ? PendingSymbols : LinkerSymtab; Symbol &OldSym = Target[IRSym.getName()]; Symbol Sym(IRSym); @@ -319,7 +319,7 @@ static bool scanSymbols(const IRSymtabFile &Symtab, StringMap<Symbol> &SymTab, } if (Extracted && IsLazy) for (const auto &[Name, Sym] : PendingSymbols) - SymTab[Name] = Sym; + LinkerSymtab[Name] = Sym; return Extracted; } >From 335ba7d54b16b0a750561e033906120038324eb6 Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Thu, 11 Jun 2026 09:12:32 -0700 Subject: [PATCH 7/8] Apply code review suggestions. --- clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index 057e16654be7f..f9aca263d649d 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -291,8 +291,8 @@ static bool scanSymbols(const IRSymtabFile &MemberSymtab, StringMap<Symbol> &LinkerSymtab, bool IsLazy) { bool Extracted = !IsLazy; StringMap<Symbol> PendingSymbols; - for (unsigned I = 0; I != MemberSymtab.Mods.size(); ++I) { - for (const auto &IRSym : MemberSymtab.TheReader.module_symbols(I)) { + for (unsigned ModIdx = 0; ModIdx != MemberSymtab.Mods.size(); ++ModIdx) { + for (const auto &IRSym : MemberSymtab.TheReader.module_symbols(ModIdx)) { if (IRSym.isFormatSpecific() || !IRSym.isGlobal()) continue; @@ -367,7 +367,7 @@ static Expected<ResolvedInputs> resolveArchiveMembers( if (!sys::fs::exists(Desc.Value)) return createStringError("input file not found: '" + Desc.Value + "'"); if (sys::fs::is_directory(Desc.Value)) - return createStringError("'" + Desc.Value + "': Is a directory"); + return createStringError("'" + Desc.Value + "': is a directory"); Filename = Desc.Value.str(); } @@ -439,9 +439,9 @@ static Expected<ResolvedInputs> resolveArchiveMembers( // Fixed-point loop to extract archive members. Each pass may resolve symbols // that unlock further members; iterate until no new member is extracted. SmallVector<std::unique_ptr<MemoryBuffer>> Resolved; - bool Extracted = true; - while (Extracted) { - Extracted = false; + bool KeepExtracting = true; + while (KeepExtracting) { + KeepExtracting = false; for (PendingInput &In : Inputs) { if (!In.Buffer) continue; @@ -466,7 +466,7 @@ static Expected<ResolvedInputs> resolveArchiveMembers( if (!scanSymbols(In.Symtab, SymTab, In.IsLazy)) continue; - Extracted = true; + KeepExtracting = true; Resolved.push_back(std::move(In.Buffer)); } } >From 48408a99605d8f547fc591cfe56157774c1f7e1f Mon Sep 17 00:00:00 2001 From: Alexey Bader <[email protected]> Date: Thu, 11 Jun 2026 10:51:44 -0700 Subject: [PATCH 8/8] Update clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp Co-authored-by: Yury Plyakhin <[email protected]> --- clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp index f9aca263d649d..aa3e51fe053a3 100644 --- a/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp +++ b/clang/tools/clang-sycl-linker/ClangSYCLLinker.cpp @@ -298,14 +298,12 @@ static bool scanSymbols(const IRSymtabFile &MemberSymtab, bool IsNewSymbol = IsLazy && !LinkerSymtab.count(IRSym.getName()); StringMap<Symbol> &Target = IsNewSymbol ? PendingSymbols : LinkerSymtab; - Symbol &OldSym = Target[IRSym.getName()]; Symbol Sym(IRSym); + auto [It, Inserted] = Target.try_emplace(IRSym.getName(), Sym); + Symbol &OldSym = It->second; - if (OldSym.SymFlags == Symbol::None) { - OldSym = Sym; - if (!IsNewSymbol) + if (Inserted && !IsNewSymbol) continue; - } bool ResolvesReference = !Sym.isUndefined() && _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
