[lld] [llvm] [lld][DebugInfo/BTF] Add BTF section merging and deduplication (PR #183915)

Can H. Tartanoglu via llvm-commits llvm-commits at lists.llvm.org
Mon Mar 2 14:46:23 PST 2026


https://github.com/caniko updated https://github.com/llvm/llvm-project/pull/183915

>From 2bf425e09ad9cf439474040dbd828e43d290465a Mon Sep 17 00:00:00 2001
From: "Can H. Tartanoglu" <gpg at rotas.mozmail.com>
Date: Sat, 28 Feb 2026 13:31:47 +0100
Subject: [PATCH] [lld][DebugInfo/BTF] Add BTF section merging and
 deduplication

Add support for merging and deduplicating .BTF (BPF Type Format) sections
in lld's ELF linker, enabling the LTO + Rust + BTF pipeline in the Linux
kernel.

The implementation consists of:

1. BTFBuilder (llvm/lib/DebugInfo/BTF/BTFBuilder.cpp): A mutable BTF
   container that can parse, merge, and serialize BTF sections. Supports
   merging multiple .BTF sections with automatic type ID and string
   offset remapping.

2. BTFDedup (llvm/lib/DebugInfo/BTF/BTFDedup.cpp): A 5-pass dedup
   algorithm ported from libbpf's btf_dedup:
   - String deduplication via hash-based interning
   - Primitive type dedup (INT, FLOAT, ENUM, ENUM64, FWD)
   - Composite type dedup (STRUCT, UNION) with DFS cycle handling
   - Reference type dedup (PTR, TYPEDEF, CONST, VOLATILE, etc.)
   - Type compaction and ID remapping

3. BtfSection (lld/ELF/SyntheticSections.cpp): A SyntheticSection that
   collects .BTF input sections, merges and deduplicates them, and emits
   a single merged .BTF output section. Enabled via --btf-merge flag.
---
 lld/ELF/CMakeLists.txt                        |    1 +
 lld/ELF/Config.h                              |    2 +
 lld/ELF/Driver.cpp                            |    3 +
 lld/ELF/Options.td                            |    4 +
 lld/ELF/SyntheticSections.cpp                 |   49 +
 lld/ELF/SyntheticSections.h                   |   15 +
 lld/docs/ld.lld.1                             |    7 +
 lld/test/ELF/btf-merge-be.s                   |   63 +
 lld/test/ELF/btf-merge.s                      |  197 ++++
 llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h  |   96 ++
 llvm/include/llvm/DebugInfo/BTF/BTFDedup.h    |   49 +
 llvm/lib/DebugInfo/BTF/BTFBuilder.cpp         |  416 +++++++
 llvm/lib/DebugInfo/BTF/BTFDedup.cpp           |  777 ++++++++++++
 llvm/lib/DebugInfo/BTF/CMakeLists.txt         |    2 +
 .../DebugInfo/BTF/BTFBuilderTest.cpp          |  793 +++++++++++++
 llvm/unittests/DebugInfo/BTF/BTFDedupTest.cpp | 1047 +++++++++++++++++
 llvm/unittests/DebugInfo/BTF/CMakeLists.txt   |    2 +
 17 files changed, 3523 insertions(+)
 create mode 100644 lld/test/ELF/btf-merge-be.s
 create mode 100644 lld/test/ELF/btf-merge.s
 create mode 100644 llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h
 create mode 100644 llvm/include/llvm/DebugInfo/BTF/BTFDedup.h
 create mode 100644 llvm/lib/DebugInfo/BTF/BTFBuilder.cpp
 create mode 100644 llvm/lib/DebugInfo/BTF/BTFDedup.cpp
 create mode 100644 llvm/unittests/DebugInfo/BTF/BTFBuilderTest.cpp
 create mode 100644 llvm/unittests/DebugInfo/BTF/BTFDedupTest.cpp

diff --git a/lld/ELF/CMakeLists.txt b/lld/ELF/CMakeLists.txt
index e22897c2789d8..c2a9265f5281b 100644
--- a/lld/ELF/CMakeLists.txt
+++ b/lld/ELF/CMakeLists.txt
@@ -66,6 +66,7 @@ add_lld_library(lldELF
   BinaryFormat
   BitWriter
   Core
+  DebugInfoBTF
   DebugInfoDWARF
   Demangle
   DTLTO
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index 237df52194210..afdca7432cbbc 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -332,6 +332,7 @@ struct Config {
   bool fixCortexA8;
   bool formatBinary = false;
   bool fortranCommon;
+  bool btfMerge;
   bool gcSections;
   bool gdbIndex;
   bool gnuHash = false;
@@ -569,6 +570,7 @@ struct InStruct {
   std::unique_ptr<SyntheticSection> riscvAttributes;
   std::unique_ptr<BssSection> bss;
   std::unique_ptr<BssSection> bssRelRo;
+  std::unique_ptr<SyntheticSection> btfSection;
   std::unique_ptr<SyntheticSection> gnuProperty;
   std::unique_ptr<SyntheticSection> gnuStack;
   std::unique_ptr<GotSection> got;
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index d7bfa7357d4ed..2c7216dbddf12 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -1436,6 +1436,9 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
       args.hasArg(OPT_fix_cortex_a8) && !args.hasArg(OPT_relocatable);
   ctx.arg.fortranCommon =
       args.hasFlag(OPT_fortran_common, OPT_no_fortran_common, false);
+  ctx.arg.btfMerge =
+      args.hasFlag(OPT_btf_merge, OPT_no_btf_merge, false) &&
+      !args.hasArg(OPT_relocatable);
   ctx.arg.gcSections = args.hasFlag(OPT_gc_sections, OPT_no_gc_sections, false);
   ctx.arg.gnuUnique = args.hasFlag(OPT_gnu_unique, OPT_no_gnu_unique, true);
   ctx.arg.gdbIndex = args.hasFlag(OPT_gdb_index, OPT_no_gdb_index, false);
diff --git a/lld/ELF/Options.td b/lld/ELF/Options.td
index c2111e58c12b9..db4081c0c0635 100644
--- a/lld/ELF/Options.td
+++ b/lld/ELF/Options.td
@@ -63,6 +63,10 @@ defm branch_to_branch: BB<"branch-to-branch",
     "Enable branch-to-branch optimization (default at -O2)",
     "Disable branch-to-branch optimization (default at -O0 and -O1)">;
 
+defm btf_merge: BB<"btf-merge",
+    "Merge and deduplicate .BTF sections",
+    "Do not merge .BTF sections (default)">;
+
 defm check_sections: B<"check-sections",
     "Check section addresses for overlaps (default)",
     "Do not check section addresses for overlaps">;
diff --git a/lld/ELF/SyntheticSections.cpp b/lld/ELF/SyntheticSections.cpp
index 2cfc88d8389b0..88775935d4286 100644
--- a/lld/ELF/SyntheticSections.cpp
+++ b/lld/ELF/SyntheticSections.cpp
@@ -32,6 +32,8 @@
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/BinaryFormat/ELF.h"
+#include "llvm/DebugInfo/BTF/BTFBuilder.h"
+#include "llvm/DebugInfo/BTF/BTFDedup.h"
 #include "llvm/DebugInfo/DWARF/DWARFAcceleratorTable.h"
 #include "llvm/DebugInfo/DWARF/DWARFDebugPubTable.h"
 #include "llvm/Support/DJB.h"
@@ -3676,6 +3678,48 @@ void GdbIndexSection::writeTo(uint8_t *buf) {
 
 bool GdbIndexSection::isNeeded() const { return !chunks.empty(); }
 
+template <class ELFT>
+BtfSection<ELFT>::BtfSection(Ctx &ctx)
+    : SyntheticSection(ctx, ".BTF", SHT_PROGBITS, 0, 1) {
+  llvm::TimeTraceScope timeScope("Merge BTF");
+  const bool isLE = ELFT::Endianness == llvm::endianness::little;
+  llvm::BTFBuilder builder;
+
+  // Collect all .BTF input sections and merge them.
+  // Mark originals dead so they don't appear in the output.
+  for (InputSectionBase *s : ctx.inputSections) {
+    if (s->name != ".BTF" || !s->isLive())
+      continue;
+    s->markDead();
+    ArrayRef<uint8_t> data = s->content();
+    StringRef raw(reinterpret_cast<const char *>(data.data()), data.size());
+    Expected<uint32_t> idBase = builder.merge(raw, isLE);
+    if (!idBase) {
+      Warn(ctx) << s << ": failed to parse .BTF section: "
+                << toString(idBase.takeError());
+      continue;
+    }
+  }
+
+  if (builder.typesCount() == 0)
+    return;
+
+  // Deduplicate merged types.
+  if (Error e = llvm::BTF::dedup(builder)) {
+    Warn(ctx) << "BTF deduplication failed: " << toString(std::move(e));
+    // Fall through and emit un-deduped BTF.
+    builder.write(outputData, isLE);
+    return;
+  }
+
+  builder.write(outputData, isLE);
+}
+
+template <class ELFT>
+void BtfSection<ELFT>::writeTo(uint8_t *buf) {
+  memcpy(buf, outputData.data(), outputData.size());
+}
+
 VersionDefinitionSection::VersionDefinitionSection(Ctx &ctx)
     : SyntheticSection(ctx, ".gnu.version_d", SHT_GNU_verdef, SHF_ALLOC,
                        sizeof(uint32_t)) {}
@@ -4902,6 +4946,11 @@ template <class ELFT> void elf::createSyntheticSections(Ctx &ctx) {
     add(*ctx.in.gnuProperty);
   }
 
+  if (ctx.arg.btfMerge) {
+    ctx.in.btfSection = std::make_unique<BtfSection<ELFT>>(ctx);
+    add(*ctx.in.btfSection);
+  }
+
   if (ctx.arg.debugNames) {
     ctx.in.debugNames = std::make_unique<DebugNamesSection<ELFT>>(ctx);
     add(*ctx.in.debugNames);
diff --git a/lld/ELF/SyntheticSections.h b/lld/ELF/SyntheticSections.h
index 1ae03dc24a2f2..314c13f0718b4 100644
--- a/lld/ELF/SyntheticSections.h
+++ b/lld/ELF/SyntheticSections.h
@@ -1013,6 +1013,21 @@ class GdbIndexSection final : public SyntheticSection {
   size_t size;
 };
 
+// Merges and deduplicates .BTF (BPF Type Format) sections from multiple input
+// object files into a single output .BTF section. Uses the BTFBuilder/BTFDedup
+// library to parse, merge, and deduplicate BTF type information.
+template <class ELFT>
+class BtfSection final : public SyntheticSection {
+public:
+  BtfSection(Ctx &);
+  void writeTo(uint8_t *buf) override;
+  size_t getSize() const override { return outputData.size(); }
+  bool isNeeded() const override { return !outputData.empty(); }
+
+private:
+  SmallVector<uint8_t, 0> outputData;
+};
+
 // For more information about .gnu.version and .gnu.version_r see:
 // https://www.akkadia.org/drepper/symbol-versioning
 
diff --git a/lld/docs/ld.lld.1 b/lld/docs/ld.lld.1
index cfdde0a6c2299..17d5022103109 100644
--- a/lld/docs/ld.lld.1
+++ b/lld/docs/ld.lld.1
@@ -98,6 +98,13 @@ Enable the branch-to-branch optimizations: a branch whose target is
 another branch instruction is rewritten to point to the latter branch
 target (AArch64 and X86_64 only). Enabled by default at
 .Fl O2 Ns .
+.It Fl -btf-merge
+Merge and deduplicate
+.Li .BTF
+sections from input object files into a single output section.
+Disabled by default.
+Ignored for relocatable links
+.Fl ( r Ns ).
 .It Fl -build-id Ns = Ns Ar value
 Generate a build ID note.
 .Ar value
diff --git a/lld/test/ELF/btf-merge-be.s b/lld/test/ELF/btf-merge-be.s
new file mode 100644
index 0000000000000..43a945f758cdd
--- /dev/null
+++ b/lld/test/ELF/btf-merge-be.s
@@ -0,0 +1,63 @@
+# REQUIRES: ppc
+## Test --btf-merge with a big-endian target.
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=powerpc64-unknown-linux a.s -o a.o
+# RUN: llvm-mc -filetype=obj -triple=powerpc64-unknown-linux b.s -o b.o
+# RUN: ld.lld --btf-merge a.o b.o -o merged
+# RUN: llvm-readelf -x .BTF merged | FileCheck %s
+
+## Big-endian BTF magic is 0xeb9f stored as eb9f (not byte-swapped).
+# CHECK: Hex dump of section '.BTF':
+# CHECK: 0x{{[0-9a-f]+}} eb9f0100
+
+#--- a.s
+.text
+.globl _start
+_start:
+  blr
+
+.section .BTF,"", at progbits
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len
+.long 16                # str_off
+.long 5                 # str_len
+## Type 1: INT "int" size=4
+.long 1                 # name_off
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 4                 # size
+.long 0x00000020        # encoding: bits=32
+## String table: "\0int\0"
+.byte 0
+.ascii "int"
+.byte 0
+
+#--- b.s
+.text
+.globl bar
+.type bar, @function
+bar:
+  blr
+
+.section .BTF,"", at progbits
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len
+.long 16                # str_off
+.long 6                 # str_len
+## Type 1: INT "long" size=8
+.long 1                 # name_off
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 8                 # size
+.long 0x00000040        # encoding: bits=64
+## String table: "\0long\0"
+.byte 0
+.ascii "long"
+.byte 0
diff --git a/lld/test/ELF/btf-merge.s b/lld/test/ELF/btf-merge.s
new file mode 100644
index 0000000000000..8a7afb234a17d
--- /dev/null
+++ b/lld/test/ELF/btf-merge.s
@@ -0,0 +1,197 @@
+# REQUIRES: x86
+## Test --btf-merge: merging and deduplication of .BTF sections.
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64 a.s -o a.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64 b.s -o b.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64 empty.s -o empty.o
+
+## Without --btf-merge, input .BTF sections pass through as-is.
+# RUN: ld.lld a.o b.o -o no-merge
+# RUN: llvm-readelf -S no-merge | FileCheck %s --check-prefix=NO-MERGE
+
+## --btf-merge produces a single merged .BTF section.
+# RUN: ld.lld --btf-merge a.o b.o -o merged
+# RUN: llvm-readelf -S merged | FileCheck %s --check-prefix=MERGED
+# RUN: llvm-readelf -x .BTF merged | FileCheck %s --check-prefix=BTF-HEX
+
+## --no-btf-merge disables merging.
+# RUN: ld.lld --btf-merge --no-btf-merge a.o b.o -o disabled
+# RUN: llvm-readelf -S disabled | FileCheck %s --check-prefix=NO-MERGE
+
+## No .BTF input: no .BTF output.
+# RUN: ld.lld --btf-merge empty.o -o no-btf
+# RUN: llvm-readelf -S no-btf | FileCheck %s --check-prefix=NO-BTF
+
+## Single file: passthrough.
+# RUN: ld.lld --btf-merge a.o -o single
+# RUN: llvm-readelf -x .BTF single | FileCheck %s --check-prefix=BTF-HEX
+
+## Dedup: both files have INT "int"; after merge only one should remain.
+# RUN: llvm-mc -filetype=obj -triple=x86_64 dup.s -o dup.o
+# RUN: ld.lld --btf-merge a.o dup.o -o dedup
+# RUN: llvm-readelf -x .BTF dedup | FileCheck %s --check-prefix=BTF-HEX
+
+## -r should not merge .BTF sections even with --btf-merge.
+## The .BTF content is the raw concatenation of both inputs (size 0x5b = 91),
+## not a parsed/deduped blob.
+# RUN: ld.lld -r --btf-merge a.o b.o -o reloc.o
+# RUN: llvm-readelf -S reloc.o | FileCheck %s --check-prefix=RELOC
+
+## --gc-sections: verify --btf-merge works together with --gc-sections.
+# RUN: ld.lld --btf-merge --gc-sections a.o b.o -o gc
+# RUN: llvm-readelf -x .BTF gc | FileCheck %s --check-prefix=BTF-HEX
+
+## isLive: a .BTF section in a discarded COMDAT group should be skipped.
+## Both comdat files define the same group "grp". The second is discarded,
+## so only the first file's .BTF (INT "int") is merged with a.o's.
+# RUN: llvm-mc -filetype=obj -triple=x86_64 comdat1.s -o comdat1.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64 comdat2.s -o comdat2.o
+# RUN: ld.lld --btf-merge a.o comdat1.o comdat2.o -o comdat
+# RUN: llvm-readelf -x .BTF comdat | FileCheck %s --check-prefix=BTF-HEX
+
+# NO-MERGE: .BTF
+# MERGED:     .BTF PROGBITS
+# MERGED-NOT: .BTF PROGBITS
+# NO-BTF-NOT: .BTF
+# BTF-HEX: Hex dump of section '.BTF':
+# BTF-HEX: 0x{{[0-9a-f]+}} 9feb0100
+
+## -r: raw concatenation of both inputs (45 + 46 = 91 = 0x5b bytes).
+# RELOC: .BTF PROGBITS {{.*}} 00005b
+
+#--- a.s
+.text
+.globl _start
+_start:
+  ret
+
+.section .BTF,"", at progbits
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len
+.long 16                # str_off
+.long 5                 # str_len
+## Type 1: INT "int" size=4
+.long 1                 # name_off
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 4                 # size
+.long 0x00000020        # encoding: bits=32
+## String table: "\0int\0"
+.byte 0
+.ascii "int"
+.byte 0
+
+#--- b.s
+.text
+.globl bar
+.type bar, @function
+bar:
+  ret
+
+.section .BTF,"", at progbits
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len
+.long 16                # str_off
+.long 6                 # str_len
+## Type 1: INT "long" size=8
+.long 1                 # name_off
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 8                 # size
+.long 0x00000040        # encoding: bits=64
+## String table: "\0long\0"
+.byte 0
+.ascii "long"
+.byte 0
+
+#--- empty.s
+.text
+.globl _start
+_start:
+  ret
+
+#--- dup.s
+.text
+.globl dup_fn
+.type dup_fn, @function
+dup_fn:
+  ret
+
+.section .BTF,"", at progbits
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len
+.long 16                # str_off
+.long 5                 # str_len
+## Type 1: INT "int" size=4 (identical to a.s)
+.long 1                 # name_off
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 4                 # size
+.long 0x00000020        # encoding: bits=32
+## String table: "\0int\0"
+.byte 0
+.ascii "int"
+.byte 0
+
+#--- comdat1.s
+## COMDAT group "grp" with a .BTF section containing INT "int".
+.section .text.foo,"axG", at progbits,grp,comdat
+.globl foo
+foo:
+  ret
+
+.section .BTF,"G", at progbits,grp,comdat
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len
+.long 16                # str_off
+.long 5                 # str_len
+## Type 1: INT "int" size=4
+.long 1                 # name_off
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 4                 # size
+.long 0x00000020        # encoding: bits=32
+## String table: "\0int\0"
+.byte 0
+.ascii "int"
+.byte 0
+
+#--- comdat2.s
+## Same COMDAT group "grp"; this copy will be discarded during dedup.
+## Its .BTF section (INT "long") should NOT be merged.
+.section .text.foo,"axG", at progbits,grp,comdat
+.globl foo
+foo:
+  ret
+
+.section .BTF,"G", at progbits,grp,comdat
+.short 0xeb9f           # magic
+.byte 1                 # version
+.byte 0                 # flags
+.long 24                # hdr_len
+.long 0                 # type_off
+.long 16                # type_len
+.long 16                # str_off
+.long 6                 # str_len
+## Type 1: INT "long" size=8 (should be discarded)
+.long 1                 # name_off
+.long 0x01000000        # info: kind=INT(1), vlen=0
+.long 8                 # size
+.long 0x00000040        # encoding: bits=64
+## String table: "\0long\0"
+.byte 0
+.ascii "long"
+.byte 0
diff --git a/llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h b/llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h
new file mode 100644
index 0000000000000..a401788ec88f0
--- /dev/null
+++ b/llvm/include/llvm/DebugInfo/BTF/BTFBuilder.h
@@ -0,0 +1,96 @@
+//===- BTFBuilder.h - BTF builder/writer -----------------------*- 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// Mutable in-memory representation of BTF type information.
+///
+/// BTFBuilder provides an interface for constructing and merging .BTF
+/// sections. Types and strings can be added individually or merged from
+/// raw .BTF section data parsed from ELF object files. The result can
+/// be serialized back to binary .BTF format.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_DEBUGINFO_BTF_BTFBUILDER_H
+#define LLVM_DEBUGINFO_BTF_BTFBUILDER_H
+
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/DebugInfo/BTF/BTF.h"
+#include "llvm/Support/Compiler.h"
+#include "llvm/Support/Error.h"
+
+namespace llvm {
+
+/// A mutable container for BTF type information that supports construction,
+/// merging, and serialization.
+///
+/// Types are stored as contiguous raw bytes in native byte order.
+/// Type IDs are 1-based (ID 0 = void, never stored).
+class BTFBuilder {
+  // String table: concatenated NUL-terminated strings.
+  // Offset 0 is always the empty string (single NUL byte).
+  SmallVector<char, 0> Strings;
+
+  // Raw type data in native byte order. Types are stored sequentially,
+  // each as CommonType header followed by kind-specific tail data.
+  SmallVector<uint8_t, 0> TypeData;
+
+  // TypeOffsets[i] is the byte offset in TypeData for type ID (i+1).
+  SmallVector<uint32_t, 0> TypeOffsets;
+
+public:
+  LLVM_ABI BTFBuilder();
+
+  /// Add a string, returning its offset in the string table.
+  LLVM_ABI uint32_t addString(StringRef S);
+
+  /// Add a type header, returning the new 1-based type ID.
+  /// Append kind-specific tail data with addTail() immediately after.
+  LLVM_ABI uint32_t addType(const BTF::CommonType &Header);
+
+  /// Append kind-specific tail data for the most recently added type.
+  template <typename T> void addTail(const T &Data) {
+    const auto *Ptr = reinterpret_cast<const uint8_t *>(&Data);
+    TypeData.append(Ptr, Ptr + sizeof(Data));
+  }
+
+  /// Merge all types and strings from a raw .BTF section, remapping
+  /// type IDs and string offsets. Returns the first new type ID.
+  LLVM_ABI Expected<uint32_t> merge(StringRef RawBTFSection,
+                                    bool IsLittleEndian);
+
+  /// Look up a type by 1-based ID. Returns nullptr for invalid IDs.
+  LLVM_ABI const BTF::CommonType *findType(uint32_t Id) const;
+
+  /// Get raw bytes for a type entry (CommonType + tail data).
+  LLVM_ABI ArrayRef<uint8_t> getTypeBytes(uint32_t Id) const;
+
+  /// Get mutable raw bytes for a type entry.
+  LLVM_ABI MutableArrayRef<uint8_t> getMutableTypeBytes(uint32_t Id);
+
+  /// Look up a string by offset in the string table.
+  LLVM_ABI StringRef findString(uint32_t Offset) const;
+
+  /// Number of types, excluding void (type 0).
+  uint32_t typesCount() const { return TypeOffsets.size(); }
+
+  /// Compute the byte size of a type entry from its CommonType header.
+  LLVM_ABI static size_t typeByteSize(const BTF::CommonType *T);
+
+  /// Returns true if CommonType.Type is a type reference for this kind.
+  LLVM_ABI static bool hasTypeRef(uint32_t Kind);
+
+  /// Serialize to binary .BTF format, appending to Out.
+  LLVM_ABI void write(SmallVectorImpl<uint8_t> &Out,
+                      bool IsLittleEndian) const;
+};
+
+} // namespace llvm
+
+#endif // LLVM_DEBUGINFO_BTF_BTFBUILDER_H
diff --git a/llvm/include/llvm/DebugInfo/BTF/BTFDedup.h b/llvm/include/llvm/DebugInfo/BTF/BTFDedup.h
new file mode 100644
index 0000000000000..0422e516ca0ba
--- /dev/null
+++ b/llvm/include/llvm/DebugInfo/BTF/BTFDedup.h
@@ -0,0 +1,49 @@
+//===- BTFDedup.h - BTF type deduplication ---------------------*- 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
+//
+//===----------------------------------------------------------------------===//
+///
+/// \file
+/// BTF type deduplication algorithm.
+///
+/// Implements the 5-pass deduplication algorithm from libbpf:
+///   1. String deduplication
+///   2. Primitive/struct/enum type dedup with DFS equivalence checking
+///   3. Reference type dedup (PTR, TYPEDEF, etc.)
+///   4. Type compaction (remove duplicates, assign new IDs)
+///   5. Type ID remapping
+///
+/// The algorithm achieves ~137x type reduction on a full Linux kernel
+/// (3.6M types -> 26K types).
+///
+/// Reference: https://nakryiko.com/posts/btf-dedup/
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_DEBUGINFO_BTF_BTFDEDUP_H
+#define LLVM_DEBUGINFO_BTF_BTFDEDUP_H
+
+#include "llvm/Support/Compiler.h"
+#include "llvm/Support/Error.h"
+
+namespace llvm {
+
+class BTFBuilder;
+
+namespace BTF {
+
+/// Deduplicate types in a BTFBuilder in-place.
+///
+/// After deduplication, structurally equivalent types are merged and
+/// all type IDs are remapped to point to canonical representatives.
+/// The resulting BTFBuilder can be serialized to produce a compact
+/// .BTF section.
+LLVM_ABI Error dedup(BTFBuilder &Builder);
+
+} // namespace BTF
+} // namespace llvm
+
+#endif // LLVM_DEBUGINFO_BTF_BTFDEDUP_H
diff --git a/llvm/lib/DebugInfo/BTF/BTFBuilder.cpp b/llvm/lib/DebugInfo/BTF/BTFBuilder.cpp
new file mode 100644
index 0000000000000..d9875133b4dfb
--- /dev/null
+++ b/llvm/lib/DebugInfo/BTF/BTFBuilder.cpp
@@ -0,0 +1,416 @@
+//===- BTFBuilder.cpp - BTF builder/writer implementation -----------------===//
+//
+// 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/DebugInfo/BTF/BTFBuilder.h"
+#include "llvm/Support/Endian.h"
+#include "llvm/Support/SwapByteOrder.h"
+
+using namespace llvm;
+
+BTFBuilder::BTFBuilder() {
+  // String table starts with the empty string at offset 0.
+  Strings.push_back('\0');
+}
+
+uint32_t BTFBuilder::addString(StringRef S) {
+  uint32_t Offset = Strings.size();
+  Strings.append(S.begin(), S.end());
+  Strings.push_back('\0');
+  return Offset;
+}
+
+uint32_t BTFBuilder::addType(const BTF::CommonType &Header) {
+  TypeOffsets.push_back(TypeData.size());
+  const auto *Ptr = reinterpret_cast<const uint8_t *>(&Header);
+  TypeData.append(Ptr, Ptr + sizeof(Header));
+  return TypeOffsets.size(); // 1-based ID
+}
+
+const BTF::CommonType *BTFBuilder::findType(uint32_t Id) const {
+  if (Id == 0 || Id > TypeOffsets.size())
+    return nullptr;
+  return reinterpret_cast<const BTF::CommonType *>(
+      &TypeData[TypeOffsets[Id - 1]]);
+}
+
+// Returns {Start, Size} for a type's byte range, or {0, 0} for invalid IDs.
+static std::pair<uint32_t, uint32_t>
+typeBounds(uint32_t Id, const SmallVectorImpl<uint32_t> &TypeOffsets,
+           size_t TypeDataSize) {
+  if (Id == 0 || Id > TypeOffsets.size())
+    return {0, 0};
+  uint32_t Start = TypeOffsets[Id - 1];
+  uint32_t End =
+      (Id < TypeOffsets.size()) ? TypeOffsets[Id] : TypeDataSize;
+  return {Start, End - Start};
+}
+
+ArrayRef<uint8_t> BTFBuilder::getTypeBytes(uint32_t Id) const {
+  auto [Start, Size] = typeBounds(Id, TypeOffsets, TypeData.size());
+  if (Size == 0)
+    return {};
+  return ArrayRef<uint8_t>(&TypeData[Start], Size);
+}
+
+MutableArrayRef<uint8_t> BTFBuilder::getMutableTypeBytes(uint32_t Id) {
+  auto [Start, Size] = typeBounds(Id, TypeOffsets, TypeData.size());
+  if (Size == 0)
+    return {};
+  return MutableArrayRef<uint8_t>(&TypeData[Start], Size);
+}
+
+StringRef BTFBuilder::findString(uint32_t Offset) const {
+  if (Offset >= Strings.size())
+    return StringRef();
+  return StringRef(&Strings[Offset]);
+}
+
+size_t BTFBuilder::typeByteSize(const BTF::CommonType *T) {
+  size_t Size = sizeof(BTF::CommonType);
+  switch (T->getKind()) {
+  case BTF::BTF_KIND_INT:
+  case BTF::BTF_KIND_VAR:
+  case BTF::BTF_KIND_DECL_TAG:
+    Size += sizeof(uint32_t);
+    break;
+  case BTF::BTF_KIND_ARRAY:
+    Size += sizeof(BTF::BTFArray);
+    break;
+  case BTF::BTF_KIND_STRUCT:
+  case BTF::BTF_KIND_UNION:
+    Size += sizeof(BTF::BTFMember) * T->getVlen();
+    break;
+  case BTF::BTF_KIND_ENUM:
+    Size += sizeof(BTF::BTFEnum) * T->getVlen();
+    break;
+  case BTF::BTF_KIND_ENUM64:
+    Size += sizeof(BTF::BTFEnum64) * T->getVlen();
+    break;
+  case BTF::BTF_KIND_FUNC_PROTO:
+    Size += sizeof(BTF::BTFParam) * T->getVlen();
+    break;
+  case BTF::BTF_KIND_DATASEC:
+    Size += sizeof(BTF::BTFDataSec) * T->getVlen();
+    break;
+  default:
+    break;
+  }
+  return Size;
+}
+
+bool BTFBuilder::hasTypeRef(uint32_t Kind) {
+  switch (Kind) {
+  case BTF::BTF_KIND_PTR:
+  case BTF::BTF_KIND_TYPEDEF:
+  case BTF::BTF_KIND_VOLATILE:
+  case BTF::BTF_KIND_CONST:
+  case BTF::BTF_KIND_RESTRICT:
+  case BTF::BTF_KIND_FUNC:
+  case BTF::BTF_KIND_FUNC_PROTO:
+  case BTF::BTF_KIND_VAR:
+  case BTF::BTF_KIND_DECL_TAG:
+  case BTF::BTF_KIND_TYPE_TAG:
+    return true;
+  default:
+    return false;
+  }
+}
+
+// Byte-swap CommonType header fields in place.
+static void swapCommonType(BTF::CommonType *T) {
+  using llvm::sys::swapByteOrder;
+  swapByteOrder(T->NameOff);
+  swapByteOrder(T->Info);
+  swapByteOrder(T->Size); // Size and Type are a union, same bytes.
+}
+
+// Byte-swap kind-specific tail data in place.
+// CommonType must already be in native byte order.
+static void swapTailData(uint8_t *TailPtr, const BTF::CommonType *T) {
+  using llvm::sys::swapByteOrder;
+  switch (T->getKind()) {
+  case BTF::BTF_KIND_INT:
+  case BTF::BTF_KIND_VAR:
+  case BTF::BTF_KIND_DECL_TAG: {
+    auto *V = reinterpret_cast<uint32_t *>(TailPtr);
+    swapByteOrder(*V);
+    break;
+  }
+  case BTF::BTF_KIND_ARRAY: {
+    auto *A = reinterpret_cast<BTF::BTFArray *>(TailPtr);
+    swapByteOrder(A->ElemType);
+    swapByteOrder(A->IndexType);
+    swapByteOrder(A->Nelems);
+    break;
+  }
+  case BTF::BTF_KIND_STRUCT:
+  case BTF::BTF_KIND_UNION: {
+    auto *M = reinterpret_cast<BTF::BTFMember *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I) {
+      swapByteOrder(M[I].NameOff);
+      swapByteOrder(M[I].Type);
+      swapByteOrder(M[I].Offset);
+    }
+    break;
+  }
+  case BTF::BTF_KIND_ENUM: {
+    auto *E = reinterpret_cast<BTF::BTFEnum *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I) {
+      swapByteOrder(E[I].NameOff);
+      swapByteOrder(E[I].Val);
+    }
+    break;
+  }
+  case BTF::BTF_KIND_ENUM64: {
+    auto *E = reinterpret_cast<BTF::BTFEnum64 *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I) {
+      swapByteOrder(E[I].NameOff);
+      swapByteOrder(E[I].Val_Lo32);
+      swapByteOrder(E[I].Val_Hi32);
+    }
+    break;
+  }
+  case BTF::BTF_KIND_FUNC_PROTO: {
+    auto *P = reinterpret_cast<BTF::BTFParam *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I) {
+      swapByteOrder(P[I].NameOff);
+      swapByteOrder(P[I].Type);
+    }
+    break;
+  }
+  case BTF::BTF_KIND_DATASEC: {
+    auto *D = reinterpret_cast<BTF::BTFDataSec *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I) {
+      swapByteOrder(D[I].Type);
+      swapByteOrder(D[I].Offset);
+      swapByteOrder(D[I].Size);
+    }
+    break;
+  }
+  default:
+    break;
+  }
+}
+
+// Remap type IDs in a type entry. Non-zero IDs are adjusted by IdDelta.
+static void remapTypeIds(uint8_t *Data, uint32_t IdDelta) {
+  if (IdDelta == 0)
+    return;
+
+  auto *T = reinterpret_cast<BTF::CommonType *>(Data);
+  uint8_t *TailPtr = Data + sizeof(BTF::CommonType);
+
+  if (BTFBuilder::hasTypeRef(T->getKind()) && T->Type != 0)
+    T->Type += IdDelta;
+
+  switch (T->getKind()) {
+  case BTF::BTF_KIND_ARRAY: {
+    auto *A = reinterpret_cast<BTF::BTFArray *>(TailPtr);
+    if (A->ElemType != 0)
+      A->ElemType += IdDelta;
+    if (A->IndexType != 0)
+      A->IndexType += IdDelta;
+    break;
+  }
+  case BTF::BTF_KIND_STRUCT:
+  case BTF::BTF_KIND_UNION: {
+    auto *M = reinterpret_cast<BTF::BTFMember *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+      if (M[I].Type != 0)
+        M[I].Type += IdDelta;
+    break;
+  }
+  case BTF::BTF_KIND_FUNC_PROTO: {
+    auto *P = reinterpret_cast<BTF::BTFParam *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+      if (P[I].Type != 0)
+        P[I].Type += IdDelta;
+    break;
+  }
+  case BTF::BTF_KIND_DATASEC: {
+    auto *D = reinterpret_cast<BTF::BTFDataSec *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+      if (D[I].Type != 0)
+        D[I].Type += IdDelta;
+    break;
+  }
+  default:
+    break;
+  }
+}
+
+// Remap string offsets in a type entry.
+static void remapStringOffsets(uint8_t *Data, uint32_t StrDelta) {
+  if (StrDelta == 0)
+    return;
+
+  auto *T = reinterpret_cast<BTF::CommonType *>(Data);
+  T->NameOff += StrDelta;
+
+  uint8_t *TailPtr = Data + sizeof(BTF::CommonType);
+  switch (T->getKind()) {
+  case BTF::BTF_KIND_STRUCT:
+  case BTF::BTF_KIND_UNION: {
+    auto *M = reinterpret_cast<BTF::BTFMember *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+      M[I].NameOff += StrDelta;
+    break;
+  }
+  case BTF::BTF_KIND_ENUM: {
+    auto *E = reinterpret_cast<BTF::BTFEnum *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+      E[I].NameOff += StrDelta;
+    break;
+  }
+  case BTF::BTF_KIND_ENUM64: {
+    auto *E = reinterpret_cast<BTF::BTFEnum64 *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+      E[I].NameOff += StrDelta;
+    break;
+  }
+  case BTF::BTF_KIND_FUNC_PROTO: {
+    auto *P = reinterpret_cast<BTF::BTFParam *>(TailPtr);
+    for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+      P[I].NameOff += StrDelta;
+    break;
+  }
+  default:
+    break;
+  }
+}
+
+Expected<uint32_t> BTFBuilder::merge(StringRef RawBTFSection,
+                                     bool IsLittleEndian) {
+  bool NeedSwap = (IsLittleEndian != sys::IsLittleEndianHost);
+
+  if (RawBTFSection.size() < sizeof(BTF::Header))
+    return createStringError("BTF section too small for header");
+
+  BTF::Header Hdr;
+  memcpy(&Hdr, RawBTFSection.data(), sizeof(Hdr));
+  if (NeedSwap) {
+    sys::swapByteOrder(Hdr.Magic);
+    sys::swapByteOrder(Hdr.HdrLen);
+    sys::swapByteOrder(Hdr.TypeOff);
+    sys::swapByteOrder(Hdr.TypeLen);
+    sys::swapByteOrder(Hdr.StrOff);
+    sys::swapByteOrder(Hdr.StrLen);
+  }
+
+  if (Hdr.Magic != BTF::MAGIC)
+    return createStringError("invalid BTF magic: " +
+                             Twine::utohexstr(Hdr.Magic));
+  if (Hdr.Version != BTF::VERSION)
+    return createStringError("unsupported BTF version: " +
+                             Twine(Hdr.Version));
+
+  uint64_t DataStart = Hdr.HdrLen;
+  if (DataStart + Hdr.StrOff + Hdr.StrLen > RawBTFSection.size())
+    return createStringError("BTF string section exceeds section bounds");
+  if (DataStart + Hdr.TypeOff + Hdr.TypeLen > RawBTFSection.size())
+    return createStringError("BTF type section exceeds section bounds");
+
+  StringRef InputStrings =
+      RawBTFSection.substr(DataStart + Hdr.StrOff, Hdr.StrLen);
+  StringRef InputTypes =
+      RawBTFSection.substr(DataStart + Hdr.TypeOff, Hdr.TypeLen);
+
+  uint32_t StrDelta = Strings.size();
+  uint32_t IdDelta = TypeOffsets.size();
+  uint32_t FirstNewId = IdDelta + 1;
+
+  Strings.append(InputStrings.begin(), InputStrings.end());
+
+  uint32_t TypeDataBase = TypeData.size();
+  TypeData.append(reinterpret_cast<const uint8_t *>(InputTypes.data()),
+                  reinterpret_cast<const uint8_t *>(InputTypes.data()) +
+                      InputTypes.size());
+
+  uint64_t Offset = 0;
+  while (Offset + sizeof(BTF::CommonType) <= InputTypes.size()) {
+    uint32_t AbsOffset = TypeDataBase + Offset;
+    auto *CT = reinterpret_cast<BTF::CommonType *>(&TypeData[AbsOffset]);
+
+    if (NeedSwap)
+      swapCommonType(CT);
+
+    TypeOffsets.push_back(AbsOffset);
+    size_t FullSize = typeByteSize(CT);
+
+    if (Offset + FullSize > InputTypes.size()) {
+      TypeData.resize(TypeDataBase);
+      TypeOffsets.resize(IdDelta);
+      Strings.resize(StrDelta);
+      return createStringError("incomplete type in BTF type section");
+    }
+
+    if (NeedSwap)
+      swapTailData(&TypeData[AbsOffset + sizeof(BTF::CommonType)], CT);
+
+    remapStringOffsets(&TypeData[AbsOffset], StrDelta);
+    remapTypeIds(&TypeData[AbsOffset], IdDelta);
+
+    Offset += FullSize;
+  }
+
+  if (Offset != InputTypes.size()) {
+    TypeData.resize(TypeDataBase);
+    TypeOffsets.resize(IdDelta);
+    Strings.resize(StrDelta);
+    return createStringError("trailing bytes in BTF type section");
+  }
+
+  return FirstNewId;
+}
+
+void BTFBuilder::write(SmallVectorImpl<uint8_t> &Out,
+                       bool IsLittleEndian) const {
+  bool NeedSwap = (IsLittleEndian != sys::IsLittleEndianHost);
+
+  BTF::Header Hdr;
+  Hdr.Magic = BTF::MAGIC;
+  Hdr.Version = BTF::VERSION;
+  Hdr.Flags = 0;
+  Hdr.HdrLen = sizeof(BTF::Header);
+  Hdr.TypeOff = 0;
+  Hdr.TypeLen = TypeData.size();
+  Hdr.StrOff = TypeData.size();
+  Hdr.StrLen = Strings.size();
+
+  size_t TotalSize = sizeof(Hdr) + TypeData.size() + Strings.size();
+  size_t OutStart = Out.size();
+  Out.resize(OutStart + TotalSize);
+  uint8_t *Buf = &Out[OutStart];
+
+  BTF::Header OutHdr = Hdr;
+  if (NeedSwap) {
+    sys::swapByteOrder(OutHdr.Magic);
+    sys::swapByteOrder(OutHdr.HdrLen);
+    sys::swapByteOrder(OutHdr.TypeOff);
+    sys::swapByteOrder(OutHdr.TypeLen);
+    sys::swapByteOrder(OutHdr.StrOff);
+    sys::swapByteOrder(OutHdr.StrLen);
+  }
+  memcpy(Buf, &OutHdr, sizeof(OutHdr));
+  Buf += sizeof(OutHdr);
+
+  memcpy(Buf, TypeData.data(), TypeData.size());
+  if (NeedSwap) {
+    uint64_t Offset = 0;
+    while (Offset + sizeof(BTF::CommonType) <= TypeData.size()) {
+      auto *CT = reinterpret_cast<BTF::CommonType *>(Buf + Offset);
+      size_t FullSize = typeByteSize(CT);
+      swapTailData(Buf + Offset + sizeof(BTF::CommonType), CT);
+      swapCommonType(CT);
+      Offset += FullSize;
+    }
+  }
+  Buf += TypeData.size();
+
+  memcpy(Buf, Strings.data(), Strings.size());
+}
diff --git a/llvm/lib/DebugInfo/BTF/BTFDedup.cpp b/llvm/lib/DebugInfo/BTF/BTFDedup.cpp
new file mode 100644
index 0000000000000..e2d73243fc717
--- /dev/null
+++ b/llvm/lib/DebugInfo/BTF/BTFDedup.cpp
@@ -0,0 +1,777 @@
+//===- BTFDedup.cpp - BTF type deduplication implementation ---------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Implements the BTF type deduplication algorithm, a port of the algorithm
+// from libbpf (btf_dedup.c, BSD-licensed).
+//
+// The algorithm runs in 5 passes:
+//   1. String deduplication
+//   2. Primitive and composite type dedup (INT, ENUM, STRUCT, UNION, FWD)
+//   3. Reference type dedup (PTR, TYPEDEF, VOLATILE, etc.)
+//   4. Type compaction (remove dups, assign sequential IDs)
+//   5. Type ID remapping (fix all references to use new IDs)
+//
+// For struct/union types, a DFS-based type graph equivalence check is used
+// with a "hypothetical map" to handle recursive/cyclic types.
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/DebugInfo/BTF/BTFDedup.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/Hashing.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringMap.h"
+#include "llvm/DebugInfo/BTF/BTF.h"
+#include "llvm/DebugInfo/BTF/BTFBuilder.h"
+
+#define DEBUG_TYPE "btf-dedup"
+
+using namespace llvm;
+
+namespace {
+
+// Sentinel value: type has not been processed yet.
+constexpr uint32_t BTF_UNPROCESSED = UINT32_MAX;
+
+/// State for the BTF deduplication algorithm.
+class BTFDedupState {
+  BTFBuilder &Builder;
+
+  // Equivalence map: Map[i] = canonical type ID for type i.
+  // Initially Map[i] = i (each type is its own canonical).
+  // After dedup, Map[i] = j means type i is equivalent to canonical type j.
+  std::vector<uint32_t> Map;
+
+  // Hypothetical map for recursive type comparison.
+  // HypotMap[i] = j means "we hypothesize type i is equivalent to type j".
+  // Reset between each top-level comparison.
+  std::vector<uint32_t> HypotMap;
+
+  // List of types currently in the hypothetical map (for fast reset).
+  SmallVector<uint32_t, 0> HypotList;
+
+  // String dedup: maps string content to canonical offset.
+  StringMap<uint32_t> StringDedup;
+
+  // New string offsets: NewStrOff[old_offset] = new_offset after dedup.
+  DenseMap<uint32_t, uint32_t> NewStrOff;
+
+  // Hash for each type, used for bucketing candidates.
+  std::vector<uint64_t> TypeHash;
+
+  // Buckets: hash -> list of canonical type IDs with that hash.
+  DenseMap<uint64_t, SmallVector<uint32_t, 4>> HashBuckets;
+
+  // After compaction: OldToNew[old_id] = new_id.
+  std::vector<uint32_t> OldToNew;
+
+  static bool isPrimitiveKind(uint32_t Kind) {
+    switch (Kind) {
+    case BTF::BTF_KIND_INT:
+    case BTF::BTF_KIND_FLOAT:
+    case BTF::BTF_KIND_ENUM:
+    case BTF::BTF_KIND_ENUM64:
+    case BTF::BTF_KIND_FWD:
+      return true;
+    default:
+      return false;
+    }
+  }
+
+  // Returns true if this is a composite kind that needs DFS comparison.
+  static bool isCompositeKind(uint32_t Kind) {
+    return Kind == BTF::BTF_KIND_STRUCT || Kind == BTF::BTF_KIND_UNION;
+  }
+
+  // Get the canonical representative for a type ID.
+  uint32_t resolve(uint32_t Id) const {
+    while (Id < Map.size() && Map[Id] != Id)
+      Id = Map[Id];
+    return Id;
+  }
+
+  // Hash a type for bucketing. Only considers local structure, not
+  // referenced type IDs (those are checked in equivalence comparison).
+  uint64_t hashType(uint32_t Id);
+
+  // Hash helpers for specific kinds.
+  uint64_t hashCommon(const BTF::CommonType *T);
+  uint64_t hashStruct(const BTF::CommonType *T);
+  uint64_t hashEnum(const BTF::CommonType *T);
+  uint64_t hashEnum64(const BTF::CommonType *T);
+  uint64_t hashFuncProto(const BTF::CommonType *T);
+  uint64_t hashArray(const BTF::CommonType *T);
+
+  // Check if two types are structurally equivalent.
+  // Uses the hypothetical map for cycle handling.
+  bool isEquiv(uint32_t CandId, uint32_t CanonId);
+
+  // Deep comparison helpers.
+  bool isEquivCommon(const BTF::CommonType *Cand, const BTF::CommonType *Canon);
+  bool isEquivStruct(uint32_t CandId, uint32_t CanonId);
+  bool isEquivEnum(const BTF::CommonType *Cand, const BTF::CommonType *Canon);
+  bool isEquivEnum64(const BTF::CommonType *Cand, const BTF::CommonType *Canon);
+  bool isEquivFuncProto(uint32_t CandId, uint32_t CanonId);
+  bool isEquivArray(uint32_t CandId, uint32_t CanonId);
+
+  // Reset the hypothetical map.
+  void clearHypot() {
+    for (uint32_t Id : HypotList)
+      HypotMap[Id] = BTF_UNPROCESSED;
+    HypotList.clear();
+  }
+
+  uint32_t dedupStrOff(uint32_t Offset) const {
+    auto It = NewStrOff.find(Offset);
+    return It != NewStrOff.end() ? It->second : Offset;
+  }
+
+  // The five passes.
+  Error dedupStrings();
+  Error dedupPrimitives();
+  Error dedupComposites();
+  Error dedupRefs();
+  Error compact();
+
+public:
+  BTFDedupState(BTFBuilder &B) : Builder(B) {}
+  Error run();
+};
+
+//===----------------------------------------------------------------------===//
+// Hashing
+//===----------------------------------------------------------------------===//
+
+uint64_t BTFDedupState::hashCommon(const BTF::CommonType *T) {
+  return hash_combine(T->getKind(), dedupStrOff(T->NameOff), T->Size);
+}
+
+uint64_t BTFDedupState::hashStruct(const BTF::CommonType *T) {
+  // Hash name + size + member names (NOT member types — those are checked
+  // during equivalence comparison).
+  uint64_t H = hash_combine(T->getKind(), dedupStrOff(T->NameOff), T->Size,
+                             T->getVlen());
+  auto *Members = reinterpret_cast<const BTF::BTFMember *>(
+      reinterpret_cast<const uint8_t *>(T) + sizeof(BTF::CommonType));
+  for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+    H = hash_combine(H, dedupStrOff(Members[I].NameOff), Members[I].Offset);
+  return H;
+}
+
+uint64_t BTFDedupState::hashEnum(const BTF::CommonType *T) {
+  uint64_t H = hashCommon(T);
+  auto *Values = reinterpret_cast<const BTF::BTFEnum *>(
+      reinterpret_cast<const uint8_t *>(T) + sizeof(BTF::CommonType));
+  for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+    H = hash_combine(H, dedupStrOff(Values[I].NameOff), Values[I].Val);
+  return H;
+}
+
+uint64_t BTFDedupState::hashEnum64(const BTF::CommonType *T) {
+  uint64_t H = hashCommon(T);
+  auto *Values = reinterpret_cast<const BTF::BTFEnum64 *>(
+      reinterpret_cast<const uint8_t *>(T) + sizeof(BTF::CommonType));
+  for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+    H = hash_combine(H, dedupStrOff(Values[I].NameOff), Values[I].Val_Lo32,
+                      Values[I].Val_Hi32);
+  return H;
+}
+
+uint64_t BTFDedupState::hashFuncProto(const BTF::CommonType *T) {
+  // Hash return type + param names (NOT param types).
+  uint64_t H = hash_combine(T->getKind(), T->getVlen());
+  auto *Params = reinterpret_cast<const BTF::BTFParam *>(
+      reinterpret_cast<const uint8_t *>(T) + sizeof(BTF::CommonType));
+  for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+    H = hash_combine(H, dedupStrOff(Params[I].NameOff));
+  return H;
+}
+
+uint64_t BTFDedupState::hashArray(const BTF::CommonType *T) {
+  auto *Arr = reinterpret_cast<const BTF::BTFArray *>(
+      reinterpret_cast<const uint8_t *>(T) + sizeof(BTF::CommonType));
+  return hash_combine(T->getKind(), Arr->Nelems);
+}
+
+uint64_t BTFDedupState::hashType(uint32_t Id) {
+  const BTF::CommonType *T = Builder.findType(Id);
+  if (!T)
+    return 0;
+
+  switch (T->getKind()) {
+  case BTF::BTF_KIND_INT:
+  case BTF::BTF_KIND_FLOAT:
+  case BTF::BTF_KIND_FWD:
+    return hashCommon(T);
+  case BTF::BTF_KIND_ENUM:
+    return hashEnum(T);
+  case BTF::BTF_KIND_ENUM64:
+    return hashEnum64(T);
+  case BTF::BTF_KIND_STRUCT:
+  case BTF::BTF_KIND_UNION:
+    return hashStruct(T);
+  case BTF::BTF_KIND_FUNC_PROTO:
+    return hashFuncProto(T);
+  case BTF::BTF_KIND_ARRAY:
+    return hashArray(T);
+  default:
+    // Reference types: hash by kind only (real comparison uses resolved refs).
+    return hash_combine(T->getKind());
+  }
+}
+
+//===----------------------------------------------------------------------===//
+// Equivalence checking
+//===----------------------------------------------------------------------===//
+
+bool BTFDedupState::isEquivCommon(const BTF::CommonType *Cand,
+                                  const BTF::CommonType *Canon) {
+  return Cand->getKind() == Canon->getKind() &&
+         dedupStrOff(Cand->NameOff) == dedupStrOff(Canon->NameOff) &&
+         Cand->Size == Canon->Size && Cand->getVlen() == Canon->getVlen();
+}
+
+bool BTFDedupState::isEquivEnum(const BTF::CommonType *Cand,
+                                const BTF::CommonType *Canon) {
+  if (!isEquivCommon(Cand, Canon))
+    return false;
+
+  auto *CandVals = reinterpret_cast<const BTF::BTFEnum *>(
+      reinterpret_cast<const uint8_t *>(Cand) + sizeof(BTF::CommonType));
+  auto *CanonVals = reinterpret_cast<const BTF::BTFEnum *>(
+      reinterpret_cast<const uint8_t *>(Canon) + sizeof(BTF::CommonType));
+
+  for (unsigned I = 0, N = Cand->getVlen(); I < N; ++I) {
+    if (dedupStrOff(CandVals[I].NameOff) !=
+            dedupStrOff(CanonVals[I].NameOff) ||
+        CandVals[I].Val != CanonVals[I].Val)
+      return false;
+  }
+  return true;
+}
+
+bool BTFDedupState::isEquivEnum64(const BTF::CommonType *Cand,
+                                  const BTF::CommonType *Canon) {
+  if (!isEquivCommon(Cand, Canon))
+    return false;
+
+  auto *CandVals = reinterpret_cast<const BTF::BTFEnum64 *>(
+      reinterpret_cast<const uint8_t *>(Cand) + sizeof(BTF::CommonType));
+  auto *CanonVals = reinterpret_cast<const BTF::BTFEnum64 *>(
+      reinterpret_cast<const uint8_t *>(Canon) + sizeof(BTF::CommonType));
+
+  for (unsigned I = 0, N = Cand->getVlen(); I < N; ++I) {
+    if (dedupStrOff(CandVals[I].NameOff) !=
+            dedupStrOff(CanonVals[I].NameOff) ||
+        CandVals[I].Val_Lo32 != CanonVals[I].Val_Lo32 ||
+        CandVals[I].Val_Hi32 != CanonVals[I].Val_Hi32)
+      return false;
+  }
+  return true;
+}
+
+bool BTFDedupState::isEquivStruct(uint32_t CandId, uint32_t CanonId) {
+  const BTF::CommonType *Cand = Builder.findType(CandId);
+  const BTF::CommonType *Canon = Builder.findType(CanonId);
+  if (!Cand || !Canon || !isEquivCommon(Cand, Canon))
+    return false;
+
+  auto *CandMembers = reinterpret_cast<const BTF::BTFMember *>(
+      reinterpret_cast<const uint8_t *>(Cand) + sizeof(BTF::CommonType));
+  auto *CanonMembers = reinterpret_cast<const BTF::BTFMember *>(
+      reinterpret_cast<const uint8_t *>(Canon) + sizeof(BTF::CommonType));
+
+  for (unsigned I = 0, N = Cand->getVlen(); I < N; ++I) {
+    if (dedupStrOff(CandMembers[I].NameOff) !=
+            dedupStrOff(CanonMembers[I].NameOff) ||
+        CandMembers[I].Offset != CanonMembers[I].Offset)
+      return false;
+    if (!isEquiv(CandMembers[I].Type, CanonMembers[I].Type))
+      return false;
+  }
+  return true;
+}
+
+bool BTFDedupState::isEquivFuncProto(uint32_t CandId, uint32_t CanonId) {
+  const BTF::CommonType *Cand = Builder.findType(CandId);
+  const BTF::CommonType *Canon = Builder.findType(CanonId);
+  if (!Cand || !Canon)
+    return false;
+  if (Cand->getKind() != Canon->getKind() ||
+      Cand->getVlen() != Canon->getVlen())
+    return false;
+
+  if (!isEquiv(Cand->Type, Canon->Type))
+    return false;
+
+  auto *CandParams = reinterpret_cast<const BTF::BTFParam *>(
+      reinterpret_cast<const uint8_t *>(Cand) + sizeof(BTF::CommonType));
+  auto *CanonParams = reinterpret_cast<const BTF::BTFParam *>(
+      reinterpret_cast<const uint8_t *>(Canon) + sizeof(BTF::CommonType));
+
+  for (unsigned I = 0, N = Cand->getVlen(); I < N; ++I) {
+    if (dedupStrOff(CandParams[I].NameOff) !=
+        dedupStrOff(CanonParams[I].NameOff))
+      return false;
+    if (!isEquiv(CandParams[I].Type, CanonParams[I].Type))
+      return false;
+  }
+  return true;
+}
+
+bool BTFDedupState::isEquivArray(uint32_t CandId, uint32_t CanonId) {
+  const BTF::CommonType *Cand = Builder.findType(CandId);
+  const BTF::CommonType *Canon = Builder.findType(CanonId);
+  if (!Cand || !Canon || Cand->getKind() != Canon->getKind())
+    return false;
+
+  auto *CandArr = reinterpret_cast<const BTF::BTFArray *>(
+      reinterpret_cast<const uint8_t *>(Cand) + sizeof(BTF::CommonType));
+  auto *CanonArr = reinterpret_cast<const BTF::BTFArray *>(
+      reinterpret_cast<const uint8_t *>(Canon) + sizeof(BTF::CommonType));
+
+  if (CandArr->Nelems != CanonArr->Nelems)
+    return false;
+  return isEquiv(CandArr->ElemType, CanonArr->ElemType) &&
+         isEquiv(CandArr->IndexType, CanonArr->IndexType);
+}
+
+bool BTFDedupState::isEquiv(uint32_t CandId, uint32_t CanonId) {
+  CandId = resolve(CandId);
+  CanonId = resolve(CanonId);
+
+  if (CandId == 0 && CanonId == 0)
+    return true;
+  if (CandId == 0 || CanonId == 0)
+    return false;
+  if (CandId == CanonId)
+    return true;
+
+  // Cycle detection: check if we already have a hypothesis for CandId.
+  if (HypotMap[CandId] != BTF_UNPROCESSED)
+    return HypotMap[CandId] == CanonId;
+
+  HypotMap[CandId] = CanonId;
+  HypotList.push_back(CandId);
+
+  const BTF::CommonType *Cand = Builder.findType(CandId);
+  const BTF::CommonType *Canon = Builder.findType(CanonId);
+  if (!Cand || !Canon)
+    return false;
+
+  if (Cand->getKind() != Canon->getKind())
+    return false;
+
+  switch (Cand->getKind()) {
+  case BTF::BTF_KIND_INT:
+  case BTF::BTF_KIND_FLOAT:
+  case BTF::BTF_KIND_FWD:
+    return isEquivCommon(Cand, Canon);
+
+  case BTF::BTF_KIND_ENUM:
+    return isEquivEnum(Cand, Canon);
+
+  case BTF::BTF_KIND_ENUM64:
+    return isEquivEnum64(Cand, Canon);
+
+  case BTF::BTF_KIND_STRUCT:
+  case BTF::BTF_KIND_UNION:
+    return isEquivStruct(CandId, CanonId);
+
+  case BTF::BTF_KIND_FUNC_PROTO:
+    return isEquivFuncProto(CandId, CanonId);
+
+  case BTF::BTF_KIND_ARRAY:
+    return isEquivArray(CandId, CanonId);
+
+  case BTF::BTF_KIND_PTR:
+  case BTF::BTF_KIND_TYPEDEF:
+  case BTF::BTF_KIND_VOLATILE:
+  case BTF::BTF_KIND_CONST:
+  case BTF::BTF_KIND_RESTRICT:
+  case BTF::BTF_KIND_TYPE_TAG:
+    if (dedupStrOff(Cand->NameOff) != dedupStrOff(Canon->NameOff))
+      return false;
+    return isEquiv(Cand->Type, Canon->Type);
+
+  case BTF::BTF_KIND_FUNC:
+    if (dedupStrOff(Cand->NameOff) != dedupStrOff(Canon->NameOff))
+      return false;
+    return isEquiv(Cand->Type, Canon->Type);
+
+  case BTF::BTF_KIND_VAR:
+    if (dedupStrOff(Cand->NameOff) != dedupStrOff(Canon->NameOff))
+      return false;
+    if (Cand->Type != 0 && Canon->Type != 0)
+      return isEquiv(Cand->Type, Canon->Type);
+    return Cand->Type == Canon->Type;
+
+  case BTF::BTF_KIND_DATASEC:
+    return isEquivCommon(Cand, Canon);
+
+  case BTF::BTF_KIND_DECL_TAG:
+    if (dedupStrOff(Cand->NameOff) != dedupStrOff(Canon->NameOff))
+      return false;
+    {
+      auto CandBytes = Builder.getTypeBytes(CandId);
+      auto CanonBytes = Builder.getTypeBytes(CanonId);
+      if (CandBytes.size() < sizeof(BTF::CommonType) + 4 ||
+          CanonBytes.size() < sizeof(BTF::CommonType) + 4)
+        return false;
+      uint32_t CandIdx, CanonIdx;
+      memcpy(&CandIdx, CandBytes.data() + sizeof(BTF::CommonType), 4);
+      memcpy(&CanonIdx, CanonBytes.data() + sizeof(BTF::CommonType), 4);
+      if (CandIdx != CanonIdx)
+        return false;
+    }
+    return isEquiv(Cand->Type, Canon->Type);
+
+  default:
+    return false;
+  }
+}
+
+//===----------------------------------------------------------------------===//
+// Pass 1: String deduplication
+//===----------------------------------------------------------------------===//
+
+Error BTFDedupState::dedupStrings() {
+  StringDedup[""] = 0;
+
+  for (uint32_t Id = 1; Id <= Builder.typesCount(); ++Id) {
+    const BTF::CommonType *T = Builder.findType(Id);
+    if (!T)
+      continue;
+
+    auto DedupStr = [&](uint32_t Off) {
+      if (NewStrOff.count(Off))
+        return;
+      StringRef S = Builder.findString(Off);
+      auto It = StringDedup.find(S);
+      if (It != StringDedup.end()) {
+        NewStrOff[Off] = It->second;
+      } else {
+        StringDedup[S] = Off;
+        NewStrOff[Off] = Off;
+      }
+    };
+
+    DedupStr(T->NameOff);
+
+    const uint8_t *TailPtr =
+        reinterpret_cast<const uint8_t *>(T) + sizeof(BTF::CommonType);
+    switch (T->getKind()) {
+    case BTF::BTF_KIND_STRUCT:
+    case BTF::BTF_KIND_UNION: {
+      auto *M = reinterpret_cast<const BTF::BTFMember *>(TailPtr);
+      for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+        DedupStr(M[I].NameOff);
+      break;
+    }
+    case BTF::BTF_KIND_ENUM: {
+      auto *E = reinterpret_cast<const BTF::BTFEnum *>(TailPtr);
+      for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+        DedupStr(E[I].NameOff);
+      break;
+    }
+    case BTF::BTF_KIND_ENUM64: {
+      auto *E = reinterpret_cast<const BTF::BTFEnum64 *>(TailPtr);
+      for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+        DedupStr(E[I].NameOff);
+      break;
+    }
+    case BTF::BTF_KIND_FUNC_PROTO: {
+      auto *P = reinterpret_cast<const BTF::BTFParam *>(TailPtr);
+      for (unsigned I = 0, N = T->getVlen(); I < N; ++I)
+        DedupStr(P[I].NameOff);
+      break;
+    }
+    default:
+      break;
+    }
+  }
+
+  return Error::success();
+}
+
+//===----------------------------------------------------------------------===//
+// Pass 2: Primitive and composite type dedup
+//===----------------------------------------------------------------------===//
+
+Error BTFDedupState::dedupPrimitives() {
+  uint32_t N = Builder.typesCount();
+  for (uint32_t Id = 1; Id <= N; ++Id) {
+    const BTF::CommonType *T = Builder.findType(Id);
+    if (!T || !isPrimitiveKind(T->getKind()))
+      continue;
+
+    uint64_t H = TypeHash[Id];
+    auto &Bucket = HashBuckets[H];
+
+    bool Found = false;
+    for (uint32_t CanonId : Bucket) {
+      clearHypot();
+      if (isEquiv(Id, CanonId)) {
+        Map[Id] = CanonId;
+        Found = true;
+        break;
+      }
+    }
+    if (!Found)
+      Bucket.push_back(Id);
+  }
+
+  return Error::success();
+}
+
+Error BTFDedupState::dedupComposites() {
+  uint32_t N = Builder.typesCount();
+  for (uint32_t Id = 1; Id <= N; ++Id) {
+    const BTF::CommonType *T = Builder.findType(Id);
+    if (!T || !isCompositeKind(T->getKind()))
+      continue;
+
+    uint64_t H = TypeHash[Id];
+    auto &Bucket = HashBuckets[H];
+
+    bool Found = false;
+    for (uint32_t CanonId : Bucket) {
+      clearHypot();
+      if (isEquiv(Id, CanonId)) {
+        // Commit hypothetical mappings.
+        for (uint32_t HId : HypotList)
+          Map[HId] = HypotMap[HId];
+        Found = true;
+        break;
+      }
+    }
+
+    clearHypot();
+    if (!Found)
+      Bucket.push_back(Id);
+  }
+
+  return Error::success();
+}
+
+//===----------------------------------------------------------------------===//
+// Pass 3: Reference type dedup
+//===----------------------------------------------------------------------===//
+
+Error BTFDedupState::dedupRefs() {
+  uint32_t N = Builder.typesCount();
+
+  for (uint32_t Id = 1; Id <= N; ++Id) {
+    if (Map[Id] != Id)
+      continue;
+
+    const BTF::CommonType *T = Builder.findType(Id);
+    if (!T)
+      continue;
+
+    uint32_t Kind = T->getKind();
+    if (isPrimitiveKind(Kind) || isCompositeKind(Kind))
+      continue;
+
+    uint64_t H = hashType(Id);
+    auto &Bucket = HashBuckets[H];
+
+    bool Found = false;
+    for (uint32_t CanonId : Bucket) {
+      clearHypot();
+      if (isEquiv(Id, CanonId)) {
+        Map[Id] = CanonId;
+        Found = true;
+        break;
+      }
+    }
+
+    clearHypot();
+    if (!Found)
+      Bucket.push_back(Id);
+  }
+
+  return Error::success();
+}
+
+//===----------------------------------------------------------------------===//
+// Pass 4-5: Compaction and remapping
+//===----------------------------------------------------------------------===//
+
+Error BTFDedupState::compact() {
+  uint32_t N = Builder.typesCount();
+  BTFBuilder NewBuilder;
+  DenseMap<uint32_t, uint32_t> StrMap;
+  auto MapStr = [&](uint32_t OldOff) -> uint32_t {
+    uint32_t DedupOff = dedupStrOff(OldOff);
+    auto It = StrMap.find(DedupOff);
+    if (It != StrMap.end())
+      return It->second;
+    StringRef S = Builder.findString(DedupOff);
+    uint32_t NewOff = NewBuilder.addString(S);
+    StrMap[DedupOff] = NewOff;
+    return NewOff;
+  };
+
+  StrMap[0] = 0;
+  OldToNew.assign(N + 1, 0);
+  for (uint32_t Id = 1; Id <= N; ++Id) {
+    if (Map[Id] != Id)
+      continue;
+
+    const BTF::CommonType *T = Builder.findType(Id);
+    if (!T)
+      continue;
+
+    BTF::CommonType NewHeader = *T;
+    NewHeader.NameOff = MapStr(T->NameOff);
+    uint32_t NewId = NewBuilder.addType(NewHeader);
+    OldToNew[Id] = NewId;
+
+    ArrayRef<uint8_t> TypeBytes = Builder.getTypeBytes(Id);
+    if (TypeBytes.size() > sizeof(BTF::CommonType)) {
+      ArrayRef<uint8_t> Tail =
+          TypeBytes.slice(sizeof(BTF::CommonType));
+      SmallVector<uint8_t, 64> TailCopy(Tail.begin(), Tail.end());
+      uint8_t *TailPtr = TailCopy.data();
+
+      switch (T->getKind()) {
+      case BTF::BTF_KIND_STRUCT:
+      case BTF::BTF_KIND_UNION: {
+        auto *M = reinterpret_cast<BTF::BTFMember *>(TailPtr);
+        for (unsigned I = 0, VN = T->getVlen(); I < VN; ++I)
+          M[I].NameOff = MapStr(M[I].NameOff);
+        break;
+      }
+      case BTF::BTF_KIND_ENUM: {
+        auto *E = reinterpret_cast<BTF::BTFEnum *>(TailPtr);
+        for (unsigned I = 0, VN = T->getVlen(); I < VN; ++I)
+          E[I].NameOff = MapStr(E[I].NameOff);
+        break;
+      }
+      case BTF::BTF_KIND_ENUM64: {
+        auto *E = reinterpret_cast<BTF::BTFEnum64 *>(TailPtr);
+        for (unsigned I = 0, VN = T->getVlen(); I < VN; ++I)
+          E[I].NameOff = MapStr(E[I].NameOff);
+        break;
+      }
+      case BTF::BTF_KIND_FUNC_PROTO: {
+        auto *P = reinterpret_cast<BTF::BTFParam *>(TailPtr);
+        for (unsigned I = 0, VN = T->getVlen(); I < VN; ++I)
+          P[I].NameOff = MapStr(P[I].NameOff);
+        break;
+      }
+      default:
+        break;
+      }
+
+      for (uint8_t B : TailCopy)
+        NewBuilder.addTail(B);
+    }
+  }
+
+  for (uint32_t Id = 1; Id <= N; ++Id) {
+    if (OldToNew[Id] != 0)
+      continue;
+    uint32_t CanonId = resolve(Id);
+    OldToNew[Id] = OldToNew[CanonId];
+  }
+
+  for (uint32_t NewId = 1; NewId <= NewBuilder.typesCount(); ++NewId) {
+    MutableArrayRef<uint8_t> Bytes = NewBuilder.getMutableTypeBytes(NewId);
+    if (Bytes.empty())
+      continue;
+
+    auto *T = reinterpret_cast<BTF::CommonType *>(Bytes.data());
+    uint8_t *TailPtr = Bytes.data() + sizeof(BTF::CommonType);
+
+    if (BTFBuilder::hasTypeRef(T->getKind()) && T->Type != 0) {
+      if (T->Type < OldToNew.size())
+        T->Type = OldToNew[T->Type];
+    }
+
+    switch (T->getKind()) {
+    case BTF::BTF_KIND_ARRAY: {
+      auto *A = reinterpret_cast<BTF::BTFArray *>(TailPtr);
+      if (A->ElemType != 0 && A->ElemType < OldToNew.size())
+        A->ElemType = OldToNew[A->ElemType];
+      if (A->IndexType != 0 && A->IndexType < OldToNew.size())
+        A->IndexType = OldToNew[A->IndexType];
+      break;
+    }
+    case BTF::BTF_KIND_STRUCT:
+    case BTF::BTF_KIND_UNION: {
+      auto *M = reinterpret_cast<BTF::BTFMember *>(TailPtr);
+      for (unsigned I = 0, VN = T->getVlen(); I < VN; ++I)
+        if (M[I].Type != 0 && M[I].Type < OldToNew.size())
+          M[I].Type = OldToNew[M[I].Type];
+      break;
+    }
+    case BTF::BTF_KIND_FUNC_PROTO: {
+      auto *P = reinterpret_cast<BTF::BTFParam *>(TailPtr);
+      for (unsigned I = 0, VN = T->getVlen(); I < VN; ++I)
+        if (P[I].Type != 0 && P[I].Type < OldToNew.size())
+          P[I].Type = OldToNew[P[I].Type];
+      break;
+    }
+    case BTF::BTF_KIND_DATASEC: {
+      auto *D = reinterpret_cast<BTF::BTFDataSec *>(TailPtr);
+      for (unsigned I = 0, VN = T->getVlen(); I < VN; ++I)
+        if (D[I].Type != 0 && D[I].Type < OldToNew.size())
+          D[I].Type = OldToNew[D[I].Type];
+      break;
+    }
+    default:
+      break;
+    }
+  }
+
+  Builder = std::move(NewBuilder);
+  return Error::success();
+}
+
+//===----------------------------------------------------------------------===//
+// Main entry point
+//===----------------------------------------------------------------------===//
+
+Error BTFDedupState::run() {
+  uint32_t N = Builder.typesCount();
+  if (N == 0)
+    return Error::success();
+
+  Map.resize(N + 1);
+  for (uint32_t I = 0; I <= N; ++I)
+    Map[I] = I;
+  HypotMap.assign(N + 1, BTF_UNPROCESSED);
+
+  if (Error E = dedupStrings())
+    return E;
+
+  TypeHash.resize(N + 1, 0);
+  for (uint32_t Id = 1; Id <= N; ++Id)
+    TypeHash[Id] = hashType(Id);
+
+  if (Error E = dedupPrimitives())
+    return E;
+  if (Error E = dedupComposites())
+    return E;
+  if (Error E = dedupRefs())
+    return E;
+  if (Error E = compact())
+    return E;
+
+  return Error::success();
+}
+
+} // anonymous namespace
+
+Error llvm::BTF::dedup(BTFBuilder &Builder) {
+  BTFDedupState State(Builder);
+  return State.run();
+}
diff --git a/llvm/lib/DebugInfo/BTF/CMakeLists.txt b/llvm/lib/DebugInfo/BTF/CMakeLists.txt
index 689470c8f23e8..20805df16e0b3 100644
--- a/llvm/lib/DebugInfo/BTF/CMakeLists.txt
+++ b/llvm/lib/DebugInfo/BTF/CMakeLists.txt
@@ -1,4 +1,6 @@
 add_llvm_component_library(LLVMDebugInfoBTF
+  BTFBuilder.cpp
+  BTFDedup.cpp
   BTFParser.cpp
   BTFContext.cpp
   ADDITIONAL_HEADER_DIRS
diff --git a/llvm/unittests/DebugInfo/BTF/BTFBuilderTest.cpp b/llvm/unittests/DebugInfo/BTF/BTFBuilderTest.cpp
new file mode 100644
index 0000000000000..315c4ac4ed3a5
--- /dev/null
+++ b/llvm/unittests/DebugInfo/BTF/BTFBuilderTest.cpp
@@ -0,0 +1,793 @@
+//===-- BTFBuilderTest.cpp - BTFBuilder unit tests ------------------------===//
+//
+// 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/DebugInfo/BTF/BTFBuilder.h"
+#include "llvm/DebugInfo/BTF/BTFParser.h"
+#include "llvm/ObjectYAML/YAML.h"
+#include "llvm/ObjectYAML/yaml2obj.h"
+#include "llvm/Support/SwapByteOrder.h"
+#include "llvm/Testing/Support/Error.h"
+
+using namespace llvm;
+using namespace llvm::object;
+
+#define ASSERT_SUCCEEDED(E) ASSERT_THAT_ERROR((E), Succeeded())
+
+static uint32_t mkInfo(uint32_t Kind) { return Kind << 24; }
+
+static raw_ostream &operator<<(raw_ostream &OS, const yaml::BinaryRef &Ref) {
+  Ref.writeAsHex(OS);
+  return OS;
+}
+
+static yaml::BinaryRef makeBinRef(const void *Ptr, size_t Size) {
+  return yaml::BinaryRef(
+      ArrayRef<uint8_t>(static_cast<const uint8_t *>(Ptr), Size));
+}
+
+// Wrap raw BTF bytes in an ELF ObjectFile for BTFParser verification.
+// Includes a minimal empty .BTF.ext section (required by BTFParser).
+static std::unique_ptr<ObjectFile>
+makeELFWithBTF(const SmallVectorImpl<uint8_t> &BTFData,
+               SmallString<0> &Storage) {
+  // Build a minimal .BTF.ext section (just the header, no subsections).
+  BTF::ExtHeader ExtHdr = {};
+  ExtHdr.Magic = BTF::MAGIC;
+  ExtHdr.Version = 1;
+  ExtHdr.HdrLen = sizeof(BTF::ExtHeader);
+
+  std::string YamlBuffer;
+  raw_string_ostream Yaml(YamlBuffer);
+  Yaml << R"(
+!ELF
+FileHeader:
+  Class:    ELFCLASS64)";
+  if (sys::IsBigEndianHost)
+    Yaml << "\n  Data:     ELFDATA2MSB";
+  else
+    Yaml << "\n  Data:     ELFDATA2LSB";
+  Yaml << R"(
+  Type:     ET_REL
+  Machine:  EM_BPF
+Sections:
+  - Name:     .BTF
+    Type:     SHT_PROGBITS
+    Content: )"
+       << makeBinRef(BTFData.data(), BTFData.size());
+  Yaml << R"(
+  - Name:     .BTF.ext
+    Type:     SHT_PROGBITS
+    Content: )"
+       << makeBinRef(&ExtHdr, sizeof(ExtHdr));
+
+  return yaml::yaml2ObjectFile(Storage, YamlBuffer,
+                               [](const Twine &Err) { errs() << Err; });
+}
+
+namespace {
+
+TEST(BTFBuilderTest, emptyBuilder) {
+  BTFBuilder B;
+  EXPECT_EQ(B.typesCount(), 0u);
+  EXPECT_EQ(B.findType(0), nullptr);
+  EXPECT_EQ(B.findType(1), nullptr);
+  EXPECT_EQ(B.findString(0), "");
+}
+
+TEST(BTFBuilderTest, addStringAndType) {
+  BTFBuilder B;
+
+  uint32_t FooOff = B.addString("foo");
+  uint32_t BarOff = B.addString("bar");
+  EXPECT_EQ(B.findString(FooOff), "foo");
+  EXPECT_EQ(B.findString(BarOff), "bar");
+  EXPECT_EQ(B.findString(0), "");
+
+  // Add INT type: int foo, 4 bytes.
+  uint32_t Id = B.addType({FooOff, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0); // INT encoding
+  EXPECT_EQ(Id, 1u);
+  EXPECT_EQ(B.typesCount(), 1u);
+
+  const BTF::CommonType *T = B.findType(1);
+  ASSERT_TRUE(T);
+  EXPECT_EQ(T->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(T->Size, 4u);
+  EXPECT_EQ(B.findString(T->NameOff), "foo");
+
+  // Add PTR type pointing to type 1.
+  uint32_t Id2 = B.addType({BarOff, mkInfo(BTF::BTF_KIND_PTR), {1}});
+  EXPECT_EQ(Id2, 2u);
+  EXPECT_EQ(B.typesCount(), 2u);
+
+  const BTF::CommonType *T2 = B.findType(2);
+  ASSERT_TRUE(T2);
+  EXPECT_EQ(T2->getKind(), BTF::BTF_KIND_PTR);
+  EXPECT_EQ(T2->Type, 1u);
+}
+
+TEST(BTFBuilderTest, typeByteSize) {
+  BTFBuilder B;
+  uint32_t S = B.addString("s");
+
+  // INT: CommonType + uint32_t = 12 + 4 = 16
+  B.addType({S, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  EXPECT_EQ(B.getTypeBytes(1).size(), 16u);
+
+  // PTR: CommonType only = 12
+  B.addType({S, mkInfo(BTF::BTF_KIND_PTR), {1}});
+  EXPECT_EQ(B.getTypeBytes(2).size(), 12u);
+
+  // STRUCT with 2 members: 12 + 2*12 = 36
+  B.addType({S, mkInfo(BTF::BTF_KIND_STRUCT) | 2, {8}});
+  B.addTail(BTF::BTFMember({S, 1, 0}));
+  B.addTail(BTF::BTFMember({S, 1, 32}));
+  EXPECT_EQ(B.getTypeBytes(3).size(), 36u);
+
+  // ARRAY: 12 + 12 = 24
+  B.addType({S, mkInfo(BTF::BTF_KIND_ARRAY), {0}});
+  B.addTail(BTF::BTFArray({1, 1, 10}));
+  EXPECT_EQ(B.getTypeBytes(4).size(), 24u);
+}
+
+TEST(BTFBuilderTest, writeAndParseRoundtrip) {
+  BTFBuilder B;
+
+  // Build a small BTF with various types.
+  uint32_t IntName = B.addString("int");
+  uint32_t FooName = B.addString("foo");
+  uint32_t AName = B.addString("a");
+  uint32_t BName = B.addString("b");
+
+  // Type 1: int, 4 bytes
+  B.addType({IntName, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  // Type 2: struct foo { int a; int b; }
+  B.addType({FooName, mkInfo(BTF::BTF_KIND_STRUCT) | 2, {8}});
+  B.addTail(BTF::BTFMember({AName, 1, 0}));
+  B.addTail(BTF::BTFMember({BName, 1, 32}));
+
+  // Type 3: pointer to struct foo
+  B.addType({0, mkInfo(BTF::BTF_KIND_PTR), {2}});
+
+  // Write to binary.
+  SmallVector<uint8_t, 0> Output;
+  B.write(Output, !sys::IsBigEndianHost);
+
+  // Parse with BTFParser to verify.
+  SmallString<0> Storage;
+  auto Obj = makeELFWithBTF(Output, Storage);
+  ASSERT_TRUE(Obj);
+
+  BTFParser Parser;
+  BTFParser::ParseOptions Opts;
+  Opts.LoadTypes = true;
+  ASSERT_SUCCEEDED(Parser.parse(*Obj, Opts));
+
+  ASSERT_EQ(Parser.typesCount(), 4u); // 3 types + void
+
+  // Verify INT.
+  const BTF::CommonType *IntType = Parser.findType(1);
+  ASSERT_TRUE(IntType);
+  EXPECT_EQ(IntType->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(IntType->Size, 4u);
+  EXPECT_EQ(Parser.findString(IntType->NameOff), "int");
+
+  // Verify STRUCT.
+  const BTF::CommonType *StructType = Parser.findType(2);
+  ASSERT_TRUE(StructType);
+  EXPECT_EQ(StructType->getKind(), BTF::BTF_KIND_STRUCT);
+  EXPECT_EQ(StructType->getVlen(), 2u);
+  EXPECT_EQ(StructType->Size, 8u);
+  EXPECT_EQ(Parser.findString(StructType->NameOff), "foo");
+
+  // Verify struct members via cast.
+  auto *ST = dyn_cast<BTF::StructType>(StructType);
+  ASSERT_TRUE(ST);
+  EXPECT_EQ(Parser.findString(ST->members()[0].NameOff), "a");
+  EXPECT_EQ(ST->members()[0].Type, 1u);
+  EXPECT_EQ(Parser.findString(ST->members()[1].NameOff), "b");
+  EXPECT_EQ(ST->members()[1].Type, 1u);
+
+  // Verify PTR.
+  const BTF::CommonType *PtrType = Parser.findType(3);
+  ASSERT_TRUE(PtrType);
+  EXPECT_EQ(PtrType->getKind(), BTF::BTF_KIND_PTR);
+  EXPECT_EQ(PtrType->Type, 2u);
+}
+
+TEST(BTFBuilderTest, mergeTwo) {
+  // Build first BTF section.
+  BTFBuilder B1;
+  uint32_t IntName1 = B1.addString("int");
+  B1.addType({IntName1, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B1.addTail((uint32_t)0);
+
+  SmallVector<uint8_t, 0> Blob1;
+  B1.write(Blob1, !sys::IsBigEndianHost);
+
+  // Build second BTF section.
+  BTFBuilder B2;
+  uint32_t LongName2 = B2.addString("long");
+  // Type 1 in B2: long, 8 bytes.
+  B2.addType({LongName2, mkInfo(BTF::BTF_KIND_INT), {8}});
+  B2.addTail((uint32_t)0);
+  // Type 2 in B2: ptr to long (type 1).
+  B2.addType({0, mkInfo(BTF::BTF_KIND_PTR), {1}});
+
+  SmallVector<uint8_t, 0> Blob2;
+  B2.write(Blob2, !sys::IsBigEndianHost);
+
+  // Merge both into a new builder.
+  BTFBuilder Merged;
+  auto FirstId1 = Merged.merge(
+      StringRef(reinterpret_cast<const char *>(Blob1.data()), Blob1.size()),
+      !sys::IsBigEndianHost);
+  ASSERT_SUCCEEDED(FirstId1.takeError());
+  EXPECT_EQ(*FirstId1, 1u);
+  EXPECT_EQ(Merged.typesCount(), 1u);
+
+  auto FirstId2 = Merged.merge(
+      StringRef(reinterpret_cast<const char *>(Blob2.data()), Blob2.size()),
+      !sys::IsBigEndianHost);
+  ASSERT_SUCCEEDED(FirstId2.takeError());
+  EXPECT_EQ(*FirstId2, 2u);
+  EXPECT_EQ(Merged.typesCount(), 3u);
+
+  // Type 1: int from first blob.
+  const BTF::CommonType *T1 = Merged.findType(1);
+  ASSERT_TRUE(T1);
+  EXPECT_EQ(T1->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(T1->Size, 4u);
+  EXPECT_EQ(Merged.findString(T1->NameOff), "int");
+
+  // Type 2: long from second blob.
+  const BTF::CommonType *T2 = Merged.findType(2);
+  ASSERT_TRUE(T2);
+  EXPECT_EQ(T2->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(T2->Size, 8u);
+  EXPECT_EQ(Merged.findString(T2->NameOff), "long");
+
+  // Type 3: ptr to type 2 (remapped from type 1 in second blob).
+  const BTF::CommonType *T3 = Merged.findType(3);
+  ASSERT_TRUE(T3);
+  EXPECT_EQ(T3->getKind(), BTF::BTF_KIND_PTR);
+  EXPECT_EQ(T3->Type, 2u); // Remapped: was 1, now 1+1=2
+
+  // Verify roundtrip through BTFParser.
+  SmallVector<uint8_t, 0> MergedOutput;
+  Merged.write(MergedOutput, !sys::IsBigEndianHost);
+
+  SmallString<0> Storage;
+  auto Obj = makeELFWithBTF(MergedOutput, Storage);
+  ASSERT_TRUE(Obj);
+
+  BTFParser Parser;
+  BTFParser::ParseOptions Opts;
+  Opts.LoadTypes = true;
+  ASSERT_SUCCEEDED(Parser.parse(*Obj, Opts));
+  EXPECT_EQ(Parser.typesCount(), 4u); // 3 types + void
+}
+
+TEST(BTFBuilderTest, mergeStructWithMembers) {
+  // Build BTF with struct that references other types.
+  BTFBuilder B1;
+  uint32_t IntName = B1.addString("int");
+  uint32_t FooName = B1.addString("foo");
+  uint32_t XName = B1.addString("x");
+  uint32_t YName = B1.addString("y");
+
+  // Type 1: int
+  B1.addType({IntName, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B1.addTail((uint32_t)0);
+  // Type 2: struct foo { int x; int y; }
+  B1.addType({FooName, mkInfo(BTF::BTF_KIND_STRUCT) | 2, {8}});
+  B1.addTail(BTF::BTFMember({XName, 1, 0}));
+  B1.addTail(BTF::BTFMember({YName, 1, 32}));
+  // Type 3: ptr to void
+  B1.addType({0, mkInfo(BTF::BTF_KIND_PTR), {0}});
+
+  SmallVector<uint8_t, 0> Blob1;
+  B1.write(Blob1, !sys::IsBigEndianHost);
+
+  // Merge into a builder that already has one type.
+  BTFBuilder Merged;
+  uint32_t PreName = Merged.addString("pre");
+  Merged.addType({PreName, mkInfo(BTF::BTF_KIND_FLOAT), {4}});
+  EXPECT_EQ(Merged.typesCount(), 1u);
+
+  auto FirstId = Merged.merge(
+      StringRef(reinterpret_cast<const char *>(Blob1.data()), Blob1.size()),
+      !sys::IsBigEndianHost);
+  ASSERT_SUCCEEDED(FirstId.takeError());
+  EXPECT_EQ(*FirstId, 2u); // First new type from merge
+  EXPECT_EQ(Merged.typesCount(), 4u);
+
+  // Type 1: pre-existing FLOAT
+  EXPECT_EQ(Merged.findType(1)->getKind(), BTF::BTF_KIND_FLOAT);
+
+  // Type 2: int (remapped from id 1)
+  const BTF::CommonType *IntT = Merged.findType(2);
+  ASSERT_TRUE(IntT);
+  EXPECT_EQ(IntT->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(Merged.findString(IntT->NameOff), "int");
+
+  // Type 3: struct foo with member types remapped to 2
+  const BTF::CommonType *StructT = Merged.findType(3);
+  ASSERT_TRUE(StructT);
+  EXPECT_EQ(StructT->getKind(), BTF::BTF_KIND_STRUCT);
+  EXPECT_EQ(Merged.findString(StructT->NameOff), "foo");
+  auto *ST = dyn_cast<BTF::StructType>(StructT);
+  ASSERT_TRUE(ST);
+  EXPECT_EQ(Merged.findString(ST->members()[0].NameOff), "x");
+  EXPECT_EQ(ST->members()[0].Type, 2u); // Was 1, remapped to 2
+  EXPECT_EQ(Merged.findString(ST->members()[1].NameOff), "y");
+  EXPECT_EQ(ST->members()[1].Type, 2u); // Was 1, remapped to 2
+
+  // Type 4: ptr to void (type 0 stays 0, not remapped)
+  const BTF::CommonType *PtrT = Merged.findType(4);
+  ASSERT_TRUE(PtrT);
+  EXPECT_EQ(PtrT->getKind(), BTF::BTF_KIND_PTR);
+  EXPECT_EQ(PtrT->Type, 0u); // void stays 0
+}
+
+TEST(BTFBuilderTest, mergeAllKinds) {
+  BTFBuilder B;
+
+  // Build a BTF with all type kinds.
+  uint32_t S = B.addString("t");
+  uint32_t M = B.addString("m");
+
+  B.addType({S, mkInfo(BTF::BTF_KIND_INT), {4}});       // 1
+  B.addTail((uint32_t)0);
+  B.addType({S, mkInfo(BTF::BTF_KIND_PTR), {1}});        // 2
+  B.addType({S, mkInfo(BTF::BTF_KIND_ARRAY), {0}});      // 3
+  B.addTail(BTF::BTFArray({1, 1, 10}));
+  B.addType({S, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}}); // 4
+  B.addTail(BTF::BTFMember({M, 1, 0}));
+  B.addType({S, mkInfo(BTF::BTF_KIND_UNION) | 1, {4}});  // 5
+  B.addTail(BTF::BTFMember({M, 1, 0}));
+  B.addType({S, mkInfo(BTF::BTF_KIND_ENUM) | 1, {4}});   // 6
+  B.addTail(BTF::BTFEnum({M, 42}));
+  B.addType({S, mkInfo(BTF::BTF_KIND_FWD), {0}});        // 7
+  B.addType({S, mkInfo(BTF::BTF_KIND_TYPEDEF), {1}});    // 8
+  B.addType({S, mkInfo(BTF::BTF_KIND_VOLATILE), {1}});   // 9
+  B.addType({S, mkInfo(BTF::BTF_KIND_CONST), {1}});      // 10
+  B.addType({S, mkInfo(BTF::BTF_KIND_RESTRICT), {1}});   // 11
+  B.addType({S, mkInfo(BTF::BTF_KIND_FUNC_PROTO) | 1, {1}}); // 12
+  B.addTail(BTF::BTFParam({M, 1}));
+  B.addType({S, mkInfo(BTF::BTF_KIND_FUNC), {12}});      // 13
+  B.addType({S, mkInfo(BTF::BTF_KIND_VAR), {1}});        // 14
+  B.addTail((uint32_t)0); // linkage
+  B.addType({S, mkInfo(BTF::BTF_KIND_DATASEC) | 1, {0}}); // 15
+  B.addTail(BTF::BTFDataSec({14, 0, 4}));
+  B.addType({S, mkInfo(BTF::BTF_KIND_FLOAT), {4}});      // 16
+  B.addType({S, mkInfo(BTF::BTF_KIND_DECL_TAG), {1}});   // 17
+  B.addTail((uint32_t)-1); // component_idx
+  B.addType({S, mkInfo(BTF::BTF_KIND_TYPE_TAG), {1}});   // 18
+  B.addType({S, mkInfo(BTF::BTF_KIND_ENUM64) | 1, {8}}); // 19
+  B.addTail(BTF::BTFEnum64({M, 1, 0}));
+
+  EXPECT_EQ(B.typesCount(), 19u);
+
+  // Write and parse back.
+  SmallVector<uint8_t, 0> Output;
+  B.write(Output, !sys::IsBigEndianHost);
+
+  SmallString<0> Storage;
+  auto Obj = makeELFWithBTF(Output, Storage);
+  ASSERT_TRUE(Obj);
+
+  BTFParser Parser;
+  BTFParser::ParseOptions Opts;
+  Opts.LoadTypes = true;
+  ASSERT_SUCCEEDED(Parser.parse(*Obj, Opts));
+  EXPECT_EQ(Parser.typesCount(), 20u); // 19 + void
+
+  // Verify all types were parsed correctly.
+  for (uint32_t Id = 1; Id <= 19; ++Id) {
+    ASSERT_TRUE(Parser.findType(Id))
+        << "Type " << Id << " not found after roundtrip";
+  }
+
+  // Spot-check a few.
+  EXPECT_EQ(Parser.findType(1)->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(Parser.findType(2)->getKind(), BTF::BTF_KIND_PTR);
+  EXPECT_EQ(Parser.findType(3)->getKind(), BTF::BTF_KIND_ARRAY);
+  EXPECT_EQ(Parser.findType(6)->getKind(), BTF::BTF_KIND_ENUM);
+  EXPECT_EQ(Parser.findType(12)->getKind(), BTF::BTF_KIND_FUNC_PROTO);
+  EXPECT_EQ(Parser.findType(19)->getKind(), BTF::BTF_KIND_ENUM64);
+}
+
+TEST(BTFBuilderTest, mergeInvalidBTF) {
+  BTFBuilder B;
+
+  // Too small.
+  EXPECT_THAT_ERROR(B.merge("", !sys::IsBigEndianHost).takeError(),
+                    FailedWithMessage(testing::HasSubstr("too small")));
+
+  // Bad magic.
+  SmallVector<uint8_t, 0> BadMagic(sizeof(BTF::Header), 0);
+  EXPECT_THAT_ERROR(
+      B.merge(StringRef(reinterpret_cast<const char *>(BadMagic.data()),
+                        BadMagic.size()),
+              !sys::IsBigEndianHost)
+          .takeError(),
+      FailedWithMessage(testing::HasSubstr("invalid BTF magic")));
+}
+
+// Helper to build a raw BTF blob from a BTFBuilder.
+static StringRef blobRef(const SmallVectorImpl<uint8_t> &V) {
+  return StringRef(reinterpret_cast<const char *>(V.data()), V.size());
+}
+
+TEST(BTFBuilderTest, invalidTypeIdLookups) {
+  BTFBuilder B;
+  uint32_t S = B.addString("x");
+  B.addType({S, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  // ID 0 (void) returns nullptr / empty.
+  EXPECT_EQ(B.findType(0), nullptr);
+  EXPECT_TRUE(B.getTypeBytes(0).empty());
+  EXPECT_TRUE(B.getMutableTypeBytes(0).empty());
+
+  // ID beyond range returns nullptr / empty.
+  EXPECT_EQ(B.findType(2), nullptr);
+  EXPECT_TRUE(B.getTypeBytes(2).empty());
+  EXPECT_TRUE(B.getMutableTypeBytes(2).empty());
+  EXPECT_EQ(B.findType(UINT32_MAX), nullptr);
+}
+
+TEST(BTFBuilderTest, getMutableTypeBytesIsWritable) {
+  BTFBuilder B;
+  uint32_t S = B.addString("int");
+  B.addType({S, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0x00000020); // bits=32
+
+  // Read the INT encoding via mutable bytes and change it.
+  MutableArrayRef<uint8_t> Bytes = B.getMutableTypeBytes(1);
+  ASSERT_EQ(Bytes.size(), 16u);
+  auto *T = reinterpret_cast<BTF::CommonType *>(Bytes.data());
+  EXPECT_EQ(T->Size, 4u);
+
+  // Mutate: change size to 8.
+  T->Size = 8;
+
+  // Verify mutation is visible through findType.
+  const BTF::CommonType *T2 = B.findType(1);
+  EXPECT_EQ(T2->Size, 8u);
+}
+
+TEST(BTFBuilderTest, findStringOutOfBounds) {
+  BTFBuilder B;
+  B.addString("hello");
+  // Offset 0 is always the empty string.
+  EXPECT_EQ(B.findString(0), "");
+  // Far past the end.
+  EXPECT_TRUE(B.findString(99999).empty());
+}
+
+TEST(BTFBuilderTest, hasTypeRefAllKinds) {
+  // Types that use Type (not Size) in the CommonType union.
+  EXPECT_TRUE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_PTR));
+  EXPECT_TRUE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_TYPEDEF));
+  EXPECT_TRUE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_VOLATILE));
+  EXPECT_TRUE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_CONST));
+  EXPECT_TRUE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_RESTRICT));
+  EXPECT_TRUE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_FUNC));
+  EXPECT_TRUE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_FUNC_PROTO));
+  EXPECT_TRUE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_VAR));
+  EXPECT_TRUE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_DECL_TAG));
+  EXPECT_TRUE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_TYPE_TAG));
+
+  // Types that use Size (not a type reference).
+  EXPECT_FALSE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_INT));
+  EXPECT_FALSE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_ARRAY));
+  EXPECT_FALSE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_STRUCT));
+  EXPECT_FALSE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_UNION));
+  EXPECT_FALSE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_ENUM));
+  EXPECT_FALSE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_ENUM64));
+  EXPECT_FALSE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_FWD));
+  EXPECT_FALSE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_FLOAT));
+  EXPECT_FALSE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_DATASEC));
+  EXPECT_FALSE(BTFBuilder::hasTypeRef(BTF::BTF_KIND_UNKN));
+}
+
+TEST(BTFBuilderTest, typeByteSizeAllKinds) {
+  BTFBuilder B;
+  uint32_t S = B.addString("s");
+  uint32_t M = B.addString("m");
+
+  // ENUM with 2 values: 12 + 2*8 = 28
+  B.addType({S, mkInfo(BTF::BTF_KIND_ENUM) | 2, {4}});  // 1
+  B.addTail(BTF::BTFEnum({M, 0}));
+  B.addTail(BTF::BTFEnum({M, 1}));
+  EXPECT_EQ(B.getTypeBytes(1).size(), 28u);
+
+  // ENUM64 with 1 value: 12 + 1*12 = 24
+  B.addType({S, mkInfo(BTF::BTF_KIND_ENUM64) | 1, {8}});  // 2
+  B.addTail(BTF::BTFEnum64({M, 0, 0}));
+  EXPECT_EQ(B.getTypeBytes(2).size(), 24u);
+
+  // FUNC_PROTO with 2 params: 12 + 2*8 = 28
+  B.addType({0, mkInfo(BTF::BTF_KIND_FUNC_PROTO) | 2, {0}});  // 3
+  B.addTail(BTF::BTFParam({M, 1}));
+  B.addTail(BTF::BTFParam({M, 1}));
+  EXPECT_EQ(B.getTypeBytes(3).size(), 28u);
+
+  // DATASEC with 1 entry: 12 + 1*12 = 24
+  B.addType({S, mkInfo(BTF::BTF_KIND_DATASEC) | 1, {0}});  // 4
+  B.addTail(BTF::BTFDataSec({1, 0, 4}));
+  EXPECT_EQ(B.getTypeBytes(4).size(), 24u);
+
+  // VAR: 12 + 4 = 16
+  B.addType({S, mkInfo(BTF::BTF_KIND_VAR), {1}});  // 5
+  B.addTail((uint32_t)0);
+  EXPECT_EQ(B.getTypeBytes(5).size(), 16u);
+
+  // DECL_TAG: 12 + 4 = 16
+  B.addType({S, mkInfo(BTF::BTF_KIND_DECL_TAG), {1}});  // 6
+  B.addTail((uint32_t)-1);
+  EXPECT_EQ(B.getTypeBytes(6).size(), 16u);
+
+  // FLOAT: CommonType only = 12
+  B.addType({S, mkInfo(BTF::BTF_KIND_FLOAT), {4}});  // 7
+  EXPECT_EQ(B.getTypeBytes(7).size(), 12u);
+
+  // FWD: CommonType only = 12
+  B.addType({S, mkInfo(BTF::BTF_KIND_FWD), {0}});  // 8
+  EXPECT_EQ(B.getTypeBytes(8).size(), 12u);
+
+  // TYPEDEF: CommonType only = 12
+  B.addType({S, mkInfo(BTF::BTF_KIND_TYPEDEF), {1}});  // 9
+  EXPECT_EQ(B.getTypeBytes(9).size(), 12u);
+
+  // VOLATILE: CommonType only = 12
+  B.addType({S, mkInfo(BTF::BTF_KIND_VOLATILE), {1}});  // 10
+  EXPECT_EQ(B.getTypeBytes(10).size(), 12u);
+
+  // CONST: CommonType only = 12
+  B.addType({S, mkInfo(BTF::BTF_KIND_CONST), {1}});  // 11
+  EXPECT_EQ(B.getTypeBytes(11).size(), 12u);
+
+  // RESTRICT: CommonType only = 12
+  B.addType({S, mkInfo(BTF::BTF_KIND_RESTRICT), {1}});  // 12
+  EXPECT_EQ(B.getTypeBytes(12).size(), 12u);
+
+  // FUNC: CommonType only = 12
+  B.addType({S, mkInfo(BTF::BTF_KIND_FUNC), {3}});  // 13
+  EXPECT_EQ(B.getTypeBytes(13).size(), 12u);
+
+  // TYPE_TAG: CommonType only = 12
+  B.addType({S, mkInfo(BTF::BTF_KIND_TYPE_TAG), {1}});  // 14
+  EXPECT_EQ(B.getTypeBytes(14).size(), 12u);
+
+  // UNION with 1 member: 12 + 1*12 = 24
+  B.addType({S, mkInfo(BTF::BTF_KIND_UNION) | 1, {4}});  // 15
+  B.addTail(BTF::BTFMember({M, 1, 0}));
+  EXPECT_EQ(B.getTypeBytes(15).size(), 24u);
+}
+
+TEST(BTFBuilderTest, mergeBadVersion) {
+  // Build a valid BTF section but with version=2.
+  BTFBuilder Tmp;
+  uint32_t S = Tmp.addString("x");
+  Tmp.addType({S, mkInfo(BTF::BTF_KIND_INT), {4}});
+  Tmp.addTail((uint32_t)0);
+  SmallVector<uint8_t, 0> Blob;
+  Tmp.write(Blob, !sys::IsBigEndianHost);
+
+  // Corrupt the version byte (offset 2 in the header).
+  Blob[2] = 99;
+
+  BTFBuilder B;
+  EXPECT_THAT_ERROR(B.merge(blobRef(Blob), !sys::IsBigEndianHost).takeError(),
+                    FailedWithMessage(testing::HasSubstr("unsupported BTF version")));
+  EXPECT_EQ(B.typesCount(), 0u);
+}
+
+TEST(BTFBuilderTest, mergeStringBoundsExceeded) {
+  // Build a valid BTF, then corrupt str_off+str_len to exceed section size.
+  BTFBuilder Tmp;
+  uint32_t S = Tmp.addString("x");
+  Tmp.addType({S, mkInfo(BTF::BTF_KIND_INT), {4}});
+  Tmp.addTail((uint32_t)0);
+  SmallVector<uint8_t, 0> Blob;
+  Tmp.write(Blob, !sys::IsBigEndianHost);
+
+  // Corrupt StrLen: at offset 20 (uint32_t), set it to a huge value.
+  uint32_t HugeLen = 0xFFFFFF;
+  memcpy(&Blob[20], &HugeLen, sizeof(HugeLen));
+
+  BTFBuilder B;
+  EXPECT_THAT_ERROR(B.merge(blobRef(Blob), !sys::IsBigEndianHost).takeError(),
+                    FailedWithMessage(testing::HasSubstr("exceeds section bounds")));
+  EXPECT_EQ(B.typesCount(), 0u);
+}
+
+TEST(BTFBuilderTest, mergeTypeBoundsExceeded) {
+  // Corrupt type_len to exceed section size.
+  BTFBuilder Tmp;
+  uint32_t S = Tmp.addString("x");
+  Tmp.addType({S, mkInfo(BTF::BTF_KIND_INT), {4}});
+  Tmp.addTail((uint32_t)0);
+  SmallVector<uint8_t, 0> Blob;
+  Tmp.write(Blob, !sys::IsBigEndianHost);
+
+  // Corrupt TypeLen: at offset 12 in header.
+  uint32_t HugeLen = 0xFFFFFF;
+  memcpy(&Blob[12], &HugeLen, sizeof(HugeLen));
+
+  BTFBuilder B;
+  EXPECT_THAT_ERROR(B.merge(blobRef(Blob), !sys::IsBigEndianHost).takeError(),
+                    FailedWithMessage(testing::HasSubstr("exceeds section bounds")));
+  EXPECT_EQ(B.typesCount(), 0u);
+}
+
+TEST(BTFBuilderTest, mergeIncompleteTypeRollback) {
+  // Manually build a raw BTF blob where the type section contains a STRUCT
+  // header claiming vlen=2 (needs 36 bytes), but only 28 bytes of type data.
+  BTF::Header Hdr;
+  Hdr.Magic = BTF::MAGIC;
+  Hdr.Version = BTF::VERSION;
+  Hdr.Flags = 0;
+  Hdr.HdrLen = sizeof(BTF::Header);
+  Hdr.TypeOff = 0;
+  Hdr.TypeLen = 28; // Struct with 2 members needs 36, only 28 given
+  Hdr.StrOff = 28;
+  Hdr.StrLen = 3; // "\0x\0"
+
+  SmallVector<uint8_t, 0> Blob(sizeof(Hdr) + 28 + 3, 0);
+  memcpy(Blob.data(), &Hdr, sizeof(Hdr));
+
+  // STRUCT header at start of type section.
+  uint8_t *TypePtr = Blob.data() + sizeof(Hdr);
+  BTF::CommonType CT;
+  CT.NameOff = 1; // "x"
+  CT.Info = mkInfo(BTF::BTF_KIND_STRUCT) | 2; // vlen=2
+  CT.Size = 8;
+  memcpy(TypePtr, &CT, sizeof(CT));
+  // Write 1 member (12 bytes) — struct claims 2 but only space for ~1.
+  BTF::BTFMember Mem = {1, 0, 0};
+  memcpy(TypePtr + sizeof(CT), &Mem, sizeof(Mem));
+
+  // String table: "\0x\0"
+  uint8_t *StrPtr = Blob.data() + sizeof(Hdr) + 28;
+  StrPtr[0] = 0;
+  StrPtr[1] = 'x';
+  StrPtr[2] = 0;
+
+  // Pre-populate the builder, then attempt merge.
+  BTFBuilder B;
+  uint32_t Pre = B.addString("pre");
+  B.addType({Pre, mkInfo(BTF::BTF_KIND_FLOAT), {4}});
+  EXPECT_EQ(B.typesCount(), 1u);
+
+  EXPECT_THAT_ERROR(
+      B.merge(blobRef(Blob), !sys::IsBigEndianHost).takeError(),
+      FailedWithMessage(testing::HasSubstr("incomplete type")));
+  // Builder state should be rolled back — still just 1 type.
+  EXPECT_EQ(B.typesCount(), 1u);
+}
+
+TEST(BTFBuilderTest, mergeArrayRemapsElemAndIndexType) {
+  BTFBuilder Src;
+  uint32_t IntS = Src.addString("int");
+  // Type 1: int
+  Src.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  Src.addTail((uint32_t)0);
+  // Type 2: array of 10 ints, indexed by int
+  Src.addType({0, mkInfo(BTF::BTF_KIND_ARRAY), {0}});
+  Src.addTail(BTF::BTFArray({1, 1, 10}));
+
+  SmallVector<uint8_t, 0> Blob;
+  Src.write(Blob, !sys::IsBigEndianHost);
+
+  // Merge into a builder that already has 1 type.
+  BTFBuilder B;
+  uint32_t Pre = B.addString("pre");
+  B.addType({Pre, mkInfo(BTF::BTF_KIND_FLOAT), {4}});
+
+  ASSERT_SUCCEEDED(B.merge(blobRef(Blob), !sys::IsBigEndianHost).takeError());
+  EXPECT_EQ(B.typesCount(), 3u);
+
+  // Array is type 3, int is type 2. Array's ElemType and IndexType should
+  // be remapped from 1 to 2.
+  const BTF::CommonType *ArrT = B.findType(3);
+  ASSERT_TRUE(ArrT);
+  EXPECT_EQ(ArrT->getKind(), BTF::BTF_KIND_ARRAY);
+  auto *Arr = reinterpret_cast<const BTF::BTFArray *>(
+      reinterpret_cast<const uint8_t *>(ArrT) + sizeof(BTF::CommonType));
+  EXPECT_EQ(Arr->ElemType, 2u);
+  EXPECT_EQ(Arr->IndexType, 2u);
+  EXPECT_EQ(Arr->Nelems, 10u);
+}
+
+TEST(BTFBuilderTest, mergeFuncProtoRemapsParamTypes) {
+  BTFBuilder Src;
+  uint32_t IntS = Src.addString("int");
+  uint32_t XS = Src.addString("x");
+  // Type 1: int
+  Src.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  Src.addTail((uint32_t)0);
+  // Type 2: func_proto(int x) -> int
+  Src.addType({0, mkInfo(BTF::BTF_KIND_FUNC_PROTO) | 1, {1}});
+  Src.addTail(BTF::BTFParam({XS, 1}));
+
+  SmallVector<uint8_t, 0> Blob;
+  Src.write(Blob, !sys::IsBigEndianHost);
+
+  BTFBuilder B;
+  uint32_t Pre = B.addString("pre");
+  B.addType({Pre, mkInfo(BTF::BTF_KIND_FLOAT), {4}});
+
+  ASSERT_SUCCEEDED(B.merge(blobRef(Blob), !sys::IsBigEndianHost).takeError());
+  EXPECT_EQ(B.typesCount(), 3u);
+
+  // func_proto is type 3, returns type 2 (remapped int).
+  const BTF::CommonType *FP = B.findType(3);
+  ASSERT_TRUE(FP);
+  EXPECT_EQ(FP->getKind(), BTF::BTF_KIND_FUNC_PROTO);
+  EXPECT_EQ(FP->Type, 2u); // Return type remapped
+  auto *Params = reinterpret_cast<const BTF::BTFParam *>(
+      reinterpret_cast<const uint8_t *>(FP) + sizeof(BTF::CommonType));
+  EXPECT_EQ(Params[0].Type, 2u); // Param type remapped
+}
+
+TEST(BTFBuilderTest, mergeDataSecRemapsVarTypes) {
+  BTFBuilder Src;
+  uint32_t IntS = Src.addString("int");
+  uint32_t DS = Src.addString(".data");
+  // Type 1: int
+  Src.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  Src.addTail((uint32_t)0);
+  // Type 2: VAR referencing int
+  Src.addType({IntS, mkInfo(BTF::BTF_KIND_VAR), {1}});
+  Src.addTail((uint32_t)1); // global linkage
+  // Type 3: DATASEC with 1 var
+  Src.addType({DS, mkInfo(BTF::BTF_KIND_DATASEC) | 1, {4}});
+  Src.addTail(BTF::BTFDataSec({2, 0, 4}));
+
+  SmallVector<uint8_t, 0> Blob;
+  Src.write(Blob, !sys::IsBigEndianHost);
+
+  BTFBuilder B;
+  uint32_t Pre = B.addString("pre");
+  B.addType({Pre, mkInfo(BTF::BTF_KIND_FLOAT), {4}});
+
+  ASSERT_SUCCEEDED(B.merge(blobRef(Blob), !sys::IsBigEndianHost).takeError());
+  EXPECT_EQ(B.typesCount(), 4u);
+
+  // DATASEC is type 4, its var entry should point to type 3 (remapped VAR).
+  const BTF::CommonType *DSec = B.findType(4);
+  ASSERT_TRUE(DSec);
+  EXPECT_EQ(DSec->getKind(), BTF::BTF_KIND_DATASEC);
+  auto *Vars = reinterpret_cast<const BTF::BTFDataSec *>(
+      reinterpret_cast<const uint8_t *>(DSec) + sizeof(BTF::CommonType));
+  EXPECT_EQ(Vars[0].Type, 3u); // Remapped from 2 to 3
+}
+
+TEST(BTFBuilderTest, writeEmptyBuilder) {
+  BTFBuilder B;
+  SmallVector<uint8_t, 0> Output;
+  B.write(Output, !sys::IsBigEndianHost);
+
+  // Should produce a valid header with empty type and string sections.
+  EXPECT_GE(Output.size(), sizeof(BTF::Header));
+  BTF::Header Hdr;
+  memcpy(&Hdr, Output.data(), sizeof(Hdr));
+  EXPECT_EQ(Hdr.Magic, BTF::MAGIC);
+  EXPECT_EQ(Hdr.TypeLen, 0u);
+  // String table always has at least the empty string (\0).
+  EXPECT_EQ(Hdr.StrLen, 1u);
+}
+
+} // namespace
diff --git a/llvm/unittests/DebugInfo/BTF/BTFDedupTest.cpp b/llvm/unittests/DebugInfo/BTF/BTFDedupTest.cpp
new file mode 100644
index 0000000000000..a761e697d9555
--- /dev/null
+++ b/llvm/unittests/DebugInfo/BTF/BTFDedupTest.cpp
@@ -0,0 +1,1047 @@
+//===-- BTFDedupTest.cpp - BTFDedup unit tests ----------------------------===//
+//
+// 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/DebugInfo/BTF/BTFDedup.h"
+#include "llvm/DebugInfo/BTF/BTFBuilder.h"
+#include "llvm/DebugInfo/BTF/BTFParser.h"
+#include "llvm/ObjectYAML/YAML.h"
+#include "llvm/ObjectYAML/yaml2obj.h"
+#include "llvm/Testing/Support/Error.h"
+
+using namespace llvm;
+using namespace llvm::object;
+
+#define ASSERT_SUCCEEDED(E) ASSERT_THAT_ERROR((E), Succeeded())
+
+static uint32_t mkInfo(uint32_t Kind) { return Kind << 24; }
+
+static raw_ostream &operator<<(raw_ostream &OS, const yaml::BinaryRef &Ref) {
+  Ref.writeAsHex(OS);
+  return OS;
+}
+
+static yaml::BinaryRef makeBinRef(const void *Ptr, size_t Size) {
+  return yaml::BinaryRef(
+      ArrayRef<uint8_t>(static_cast<const uint8_t *>(Ptr), Size));
+}
+
+static std::unique_ptr<ObjectFile>
+makeELFWithBTF(const SmallVectorImpl<uint8_t> &BTFData,
+               SmallString<0> &Storage) {
+  BTF::ExtHeader ExtHdr = {};
+  ExtHdr.Magic = BTF::MAGIC;
+  ExtHdr.Version = 1;
+  ExtHdr.HdrLen = sizeof(BTF::ExtHeader);
+
+  std::string YamlBuffer;
+  raw_string_ostream Yaml(YamlBuffer);
+  Yaml << R"(
+!ELF
+FileHeader:
+  Class:    ELFCLASS64)";
+  if (sys::IsBigEndianHost)
+    Yaml << "\n  Data:     ELFDATA2MSB";
+  else
+    Yaml << "\n  Data:     ELFDATA2LSB";
+  Yaml << R"(
+  Type:     ET_REL
+  Machine:  EM_BPF
+Sections:
+  - Name:     .BTF
+    Type:     SHT_PROGBITS
+    Content: )"
+       << makeBinRef(BTFData.data(), BTFData.size());
+  Yaml << R"(
+  - Name:     .BTF.ext
+    Type:     SHT_PROGBITS
+    Content: )"
+       << makeBinRef(&ExtHdr, sizeof(ExtHdr));
+
+  return yaml::yaml2ObjectFile(Storage, YamlBuffer,
+                               [](const Twine &Err) { errs() << Err; });
+}
+
+namespace {
+
+TEST(BTFDedupTest, emptyDedup) {
+  BTFBuilder B;
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 0u);
+}
+
+TEST(BTFDedupTest, singleType) {
+  BTFBuilder B;
+  uint32_t S = B.addString("int");
+  B.addType({S, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 1u);
+  EXPECT_EQ(B.findType(1)->getKind(), BTF::BTF_KIND_INT);
+}
+
+TEST(BTFDedupTest, duplicateInts) {
+  BTFBuilder B;
+  uint32_t S1 = B.addString("int");
+  uint32_t S2 = B.addString("int"); // Same string, different offset
+
+  // Two identical INT types.
+  B.addType({S1, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  B.addType({S2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  EXPECT_EQ(B.typesCount(), 2u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 1u);
+
+  const BTF::CommonType *T = B.findType(1);
+  ASSERT_TRUE(T);
+  EXPECT_EQ(T->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(T->Size, 4u);
+  EXPECT_EQ(B.findString(T->NameOff), "int");
+}
+
+TEST(BTFDedupTest, differentInts) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t LongS = B.addString("long");
+
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  B.addType({LongS, mkInfo(BTF::BTF_KIND_INT), {8}});
+  B.addTail((uint32_t)0);
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 2u);
+}
+
+TEST(BTFDedupTest, duplicateEnums) {
+  BTFBuilder B;
+  uint32_t EnumS = B.addString("color");
+  uint32_t RedS = B.addString("RED");
+  uint32_t BlueS = B.addString("BLUE");
+
+  // First enum
+  B.addType({EnumS, mkInfo(BTF::BTF_KIND_ENUM) | 2, {4}});
+  B.addTail(BTF::BTFEnum({RedS, 0}));
+  B.addTail(BTF::BTFEnum({BlueS, 1}));
+
+  // Duplicate enum (same content, different string offsets)
+  uint32_t EnumS2 = B.addString("color");
+  uint32_t RedS2 = B.addString("RED");
+  uint32_t BlueS2 = B.addString("BLUE");
+  B.addType({EnumS2, mkInfo(BTF::BTF_KIND_ENUM) | 2, {4}});
+  B.addTail(BTF::BTFEnum({RedS2, 0}));
+  B.addTail(BTF::BTFEnum({BlueS2, 1}));
+
+  EXPECT_EQ(B.typesCount(), 2u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 1u);
+}
+
+TEST(BTFDedupTest, duplicatePtrTypes) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 2: duplicate int
+  uint32_t IntS2 = B.addString("int");
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 3: ptr to type 1
+  B.addType({0, mkInfo(BTF::BTF_KIND_PTR), {1}});
+  // Type 4: ptr to type 2
+  B.addType({0, mkInfo(BTF::BTF_KIND_PTR), {2}});
+
+  EXPECT_EQ(B.typesCount(), 4u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  // Should dedup to: int + ptr
+  EXPECT_EQ(B.typesCount(), 2u);
+
+  EXPECT_EQ(B.findType(1)->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(B.findType(2)->getKind(), BTF::BTF_KIND_PTR);
+  EXPECT_EQ(B.findType(2)->Type, 1u);
+}
+
+TEST(BTFDedupTest, duplicateStructs) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t FooS = B.addString("foo");
+  uint32_t AS = B.addString("a");
+  uint32_t BS = B.addString("b");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  // Type 2: struct foo { int a; int b; }
+  B.addType({FooS, mkInfo(BTF::BTF_KIND_STRUCT) | 2, {8}});
+  B.addTail(BTF::BTFMember({AS, 1, 0}));
+  B.addTail(BTF::BTFMember({BS, 1, 32}));
+
+  // Duplicate types (from another compilation unit):
+  // Type 3: int (duplicate)
+  uint32_t IntS2 = B.addString("int");
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  // Type 4: struct foo { int a; int b; } (duplicate, refs type 3)
+  uint32_t FooS2 = B.addString("foo");
+  uint32_t AS2 = B.addString("a");
+  uint32_t BS2 = B.addString("b");
+  B.addType({FooS2, mkInfo(BTF::BTF_KIND_STRUCT) | 2, {8}});
+  B.addTail(BTF::BTFMember({AS2, 3, 0}));   // refs type 3 (dup int)
+  B.addTail(BTF::BTFMember({BS2, 3, 32}));  // refs type 3 (dup int)
+
+  EXPECT_EQ(B.typesCount(), 4u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  // Should dedup to: int + struct foo
+  EXPECT_EQ(B.typesCount(), 2u);
+
+  // Verify struct members reference the deduped int.
+  const BTF::CommonType *ST = B.findType(2);
+  ASSERT_TRUE(ST);
+  EXPECT_EQ(ST->getKind(), BTF::BTF_KIND_STRUCT);
+  auto *Members = reinterpret_cast<const BTF::BTFMember *>(
+      reinterpret_cast<const uint8_t *>(ST) + sizeof(BTF::CommonType));
+  EXPECT_EQ(Members[0].Type, 1u); // Remapped to new int ID
+  EXPECT_EQ(Members[1].Type, 1u);
+}
+
+TEST(BTFDedupTest, selfReferentialStruct) {
+  BTFBuilder B;
+  uint32_t NodeS = B.addString("node");
+  uint32_t DataS = B.addString("data");
+  uint32_t NextS = B.addString("next");
+  uint32_t IntS = B.addString("int");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 2: ptr to type 3 (struct node)
+  B.addType({0, mkInfo(BTF::BTF_KIND_PTR), {3}});
+  // Type 3: struct node { int data; struct node *next; }
+  B.addType({NodeS, mkInfo(BTF::BTF_KIND_STRUCT) | 2, {16}});
+  B.addTail(BTF::BTFMember({DataS, 1, 0}));
+  B.addTail(BTF::BTFMember({NextS, 2, 64}));
+
+  // Duplicate self-referential struct:
+  // Type 4: int (dup)
+  uint32_t IntS2 = B.addString("int");
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 5: ptr to type 6
+  B.addType({0, mkInfo(BTF::BTF_KIND_PTR), {6}});
+  // Type 6: struct node (dup, refs types 4 and 5)
+  uint32_t NodeS2 = B.addString("node");
+  uint32_t DataS2 = B.addString("data");
+  uint32_t NextS2 = B.addString("next");
+  B.addType({NodeS2, mkInfo(BTF::BTF_KIND_STRUCT) | 2, {16}});
+  B.addTail(BTF::BTFMember({DataS2, 4, 0}));
+  B.addTail(BTF::BTFMember({NextS2, 5, 64}));
+
+  EXPECT_EQ(B.typesCount(), 6u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  // Should dedup to: int + ptr + struct node = 3 types
+  EXPECT_EQ(B.typesCount(), 3u);
+}
+
+TEST(BTFDedupTest, fwdDeclResolution) {
+  BTFBuilder B;
+  uint32_t FooS = B.addString("foo");
+
+  // Type 1: forward declaration of struct foo
+  B.addType({FooS, mkInfo(BTF::BTF_KIND_FWD), {0}});
+
+  // Type 2: full struct foo {}
+  uint32_t FooS2 = B.addString("foo");
+  B.addType({FooS2, mkInfo(BTF::BTF_KIND_STRUCT), {0}});
+
+  // FWD and STRUCT are different kinds, so they shouldn't be deduped
+  // by the basic algorithm (FWD resolution is a separate concern).
+  // Both should survive dedup.
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 2u);
+}
+
+TEST(BTFDedupTest, funcProtoDedup) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t XS = B.addString("x");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  // Type 2: func_proto(int) -> int
+  B.addType({0, mkInfo(BTF::BTF_KIND_FUNC_PROTO) | 1, {1}});
+  B.addTail(BTF::BTFParam({XS, 1}));
+
+  // Duplicate func_proto:
+  // Type 3: int (dup)
+  uint32_t IntS2 = B.addString("int");
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  // Type 4: func_proto(int) -> int (dup, refs type 3)
+  uint32_t XS2 = B.addString("x");
+  B.addType({0, mkInfo(BTF::BTF_KIND_FUNC_PROTO) | 1, {3}});
+  B.addTail(BTF::BTFParam({XS2, 3}));
+
+  EXPECT_EQ(B.typesCount(), 4u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  // Should dedup to: int + func_proto
+  EXPECT_EQ(B.typesCount(), 2u);
+}
+
+TEST(BTFDedupTest, roundtripAfterDedup) {
+  BTFBuilder B;
+
+  // Build types with duplicates.
+  uint32_t IntS = B.addString("int");
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  uint32_t IntS2 = B.addString("int");
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  B.addType({0, mkInfo(BTF::BTF_KIND_PTR), {1}});
+  B.addType({0, mkInfo(BTF::BTF_KIND_PTR), {2}});
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 2u);
+
+  // Write to binary and verify with BTFParser.
+  SmallVector<uint8_t, 0> Output;
+  B.write(Output, !sys::IsBigEndianHost);
+
+  SmallString<0> Storage;
+  auto Obj = makeELFWithBTF(Output, Storage);
+  ASSERT_TRUE(Obj);
+
+  BTFParser Parser;
+  BTFParser::ParseOptions Opts;
+  Opts.LoadTypes = true;
+  ASSERT_SUCCEEDED(Parser.parse(*Obj, Opts));
+  EXPECT_EQ(Parser.typesCount(), 3u); // 2 types + void
+
+  EXPECT_EQ(Parser.findType(1)->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(Parser.findType(2)->getKind(), BTF::BTF_KIND_PTR);
+  EXPECT_EQ(Parser.findType(2)->Type, 1u);
+}
+
+TEST(BTFDedupTest, mergedBlobDedup) {
+  // Simulate what lld would do: merge two .BTF sections, then dedup.
+
+  // First "object file" BTF.
+  BTFBuilder B1;
+  uint32_t IntS1 = B1.addString("int");
+  B1.addType({IntS1, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B1.addTail((uint32_t)0);
+  uint32_t FooS1 = B1.addString("foo");
+  uint32_t XS1 = B1.addString("x");
+  B1.addType({FooS1, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}});
+  B1.addTail(BTF::BTFMember({XS1, 1, 0}));
+
+  SmallVector<uint8_t, 0> Blob1;
+  B1.write(Blob1, !sys::IsBigEndianHost);
+
+  // Second "object file" BTF (same types, different IDs).
+  BTFBuilder B2;
+  uint32_t IntS2 = B2.addString("int");
+  B2.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B2.addTail((uint32_t)0);
+  uint32_t FooS2 = B2.addString("foo");
+  uint32_t XS2 = B2.addString("x");
+  B2.addType({FooS2, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}});
+  B2.addTail(BTF::BTFMember({XS2, 1, 0}));
+
+  SmallVector<uint8_t, 0> Blob2;
+  B2.write(Blob2, !sys::IsBigEndianHost);
+
+  // Merge.
+  BTFBuilder Merged;
+  ASSERT_SUCCEEDED(
+      Merged.merge(StringRef(reinterpret_cast<const char *>(Blob1.data()),
+                             Blob1.size()),
+                   !sys::IsBigEndianHost)
+          .takeError());
+  ASSERT_SUCCEEDED(
+      Merged.merge(StringRef(reinterpret_cast<const char *>(Blob2.data()),
+                             Blob2.size()),
+                   !sys::IsBigEndianHost)
+          .takeError());
+  EXPECT_EQ(Merged.typesCount(), 4u); // 2 from each blob
+
+  // Dedup.
+  ASSERT_SUCCEEDED(BTF::dedup(Merged));
+  EXPECT_EQ(Merged.typesCount(), 2u); // int + struct foo
+
+  // Verify correctness.
+  EXPECT_EQ(Merged.findType(1)->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(Merged.findType(2)->getKind(), BTF::BTF_KIND_STRUCT);
+
+  auto *ST = Merged.findType(2);
+  auto *Members = reinterpret_cast<const BTF::BTFMember *>(
+      reinterpret_cast<const uint8_t *>(ST) + sizeof(BTF::CommonType));
+  EXPECT_EQ(Members[0].Type, 1u); // Points to deduped int
+}
+
+TEST(BTFDedupTest, duplicateFloats) {
+  BTFBuilder B;
+  uint32_t S1 = B.addString("float");
+  uint32_t S2 = B.addString("float");
+
+  B.addType({S1, mkInfo(BTF::BTF_KIND_FLOAT), {4}});
+  B.addType({S2, mkInfo(BTF::BTF_KIND_FLOAT), {4}});
+
+  EXPECT_EQ(B.typesCount(), 2u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 1u);
+  EXPECT_EQ(B.findType(1)->getKind(), BTF::BTF_KIND_FLOAT);
+  EXPECT_EQ(B.findType(1)->Size, 4u);
+}
+
+TEST(BTFDedupTest, differentFloats) {
+  BTFBuilder B;
+  uint32_t S1 = B.addString("float");
+  uint32_t S2 = B.addString("double");
+
+  B.addType({S1, mkInfo(BTF::BTF_KIND_FLOAT), {4}});
+  B.addType({S2, mkInfo(BTF::BTF_KIND_FLOAT), {8}});
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 2u);
+}
+
+TEST(BTFDedupTest, duplicateEnum64) {
+  BTFBuilder B;
+  uint32_t ES = B.addString("big_enum");
+  uint32_t VS = B.addString("VAL");
+
+  B.addType({ES, mkInfo(BTF::BTF_KIND_ENUM64) | 1, {8}});
+  B.addTail(BTF::BTFEnum64({VS, 0xDEADBEEF, 0x12345678}));
+
+  uint32_t ES2 = B.addString("big_enum");
+  uint32_t VS2 = B.addString("VAL");
+  B.addType({ES2, mkInfo(BTF::BTF_KIND_ENUM64) | 1, {8}});
+  B.addTail(BTF::BTFEnum64({VS2, 0xDEADBEEF, 0x12345678}));
+
+  EXPECT_EQ(B.typesCount(), 2u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 1u);
+}
+
+TEST(BTFDedupTest, differentEnum64Values) {
+  BTFBuilder B;
+  uint32_t ES = B.addString("big_enum");
+  uint32_t VS = B.addString("VAL");
+
+  B.addType({ES, mkInfo(BTF::BTF_KIND_ENUM64) | 1, {8}});
+  B.addTail(BTF::BTFEnum64({VS, 1, 0}));
+
+  uint32_t ES2 = B.addString("big_enum");
+  uint32_t VS2 = B.addString("VAL");
+  B.addType({ES2, mkInfo(BTF::BTF_KIND_ENUM64) | 1, {8}});
+  B.addTail(BTF::BTFEnum64({VS2, 2, 0})); // Different value
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 2u);
+}
+
+TEST(BTFDedupTest, duplicateTypedefChain) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t MyIntS = B.addString("myint");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 2: typedef myint -> int
+  B.addType({MyIntS, mkInfo(BTF::BTF_KIND_TYPEDEF), {1}});
+
+  // Duplicate set:
+  uint32_t IntS2 = B.addString("int");
+  uint32_t MyIntS2 = B.addString("myint");
+  // Type 3: int (dup)
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 4: typedef myint -> int (dup, refs type 3)
+  B.addType({MyIntS2, mkInfo(BTF::BTF_KIND_TYPEDEF), {3}});
+
+  EXPECT_EQ(B.typesCount(), 4u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 2u);
+
+  EXPECT_EQ(B.findType(1)->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(B.findType(2)->getKind(), BTF::BTF_KIND_TYPEDEF);
+  EXPECT_EQ(B.findType(2)->Type, 1u);
+}
+
+TEST(BTFDedupTest, duplicateVolatileConstRestrict) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 2: volatile int
+  B.addType({0, mkInfo(BTF::BTF_KIND_VOLATILE), {1}});
+  // Type 3: const int
+  B.addType({0, mkInfo(BTF::BTF_KIND_CONST), {1}});
+  // Type 4: restrict int
+  B.addType({0, mkInfo(BTF::BTF_KIND_RESTRICT), {1}});
+
+  // Duplicate set:
+  uint32_t IntS2 = B.addString("int");
+  // Type 5: int (dup)
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 6: volatile int (dup, refs 5)
+  B.addType({0, mkInfo(BTF::BTF_KIND_VOLATILE), {5}});
+  // Type 7: const int (dup, refs 5)
+  B.addType({0, mkInfo(BTF::BTF_KIND_CONST), {5}});
+  // Type 8: restrict int (dup, refs 5)
+  B.addType({0, mkInfo(BTF::BTF_KIND_RESTRICT), {5}});
+
+  EXPECT_EQ(B.typesCount(), 8u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  // int + volatile + const + restrict = 4 types
+  EXPECT_EQ(B.typesCount(), 4u);
+}
+
+TEST(BTFDedupTest, duplicateArrays) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 2: int[10]
+  B.addType({0, mkInfo(BTF::BTF_KIND_ARRAY), {0}});
+  B.addTail(BTF::BTFArray({1, 1, 10}));
+
+  // Duplicate set:
+  uint32_t IntS2 = B.addString("int");
+  // Type 3: int (dup)
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 4: int[10] (dup, refs type 3)
+  B.addType({0, mkInfo(BTF::BTF_KIND_ARRAY), {0}});
+  B.addTail(BTF::BTFArray({3, 3, 10}));
+
+  EXPECT_EQ(B.typesCount(), 4u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 2u);
+
+  // Verify array references are remapped.
+  const BTF::CommonType *ArrT = B.findType(2);
+  ASSERT_TRUE(ArrT);
+  EXPECT_EQ(ArrT->getKind(), BTF::BTF_KIND_ARRAY);
+  auto *Arr = reinterpret_cast<const BTF::BTFArray *>(
+      reinterpret_cast<const uint8_t *>(ArrT) + sizeof(BTF::CommonType));
+  EXPECT_EQ(Arr->ElemType, 1u);
+  EXPECT_EQ(Arr->IndexType, 1u);
+  EXPECT_EQ(Arr->Nelems, 10u);
+}
+
+TEST(BTFDedupTest, differentArrayNelems) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // int[10]
+  B.addType({0, mkInfo(BTF::BTF_KIND_ARRAY), {0}});
+  B.addTail(BTF::BTFArray({1, 1, 10}));
+  // int[20] — different nelems, should NOT dedup.
+  B.addType({0, mkInfo(BTF::BTF_KIND_ARRAY), {0}});
+  B.addTail(BTF::BTFArray({1, 1, 20}));
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 3u);
+}
+
+TEST(BTFDedupTest, duplicateFuncAndFuncProto) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t XS = B.addString("x");
+  uint32_t FnS = B.addString("myfunc");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 2: func_proto(int x) -> int
+  B.addType({0, mkInfo(BTF::BTF_KIND_FUNC_PROTO) | 1, {1}});
+  B.addTail(BTF::BTFParam({XS, 1}));
+  // Type 3: func "myfunc" -> func_proto
+  B.addType({FnS, mkInfo(BTF::BTF_KIND_FUNC), {2}});
+
+  // Duplicate:
+  uint32_t IntS2 = B.addString("int");
+  uint32_t XS2 = B.addString("x");
+  uint32_t FnS2 = B.addString("myfunc");
+  // Type 4: int (dup)
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 5: func_proto (dup, refs type 4)
+  B.addType({0, mkInfo(BTF::BTF_KIND_FUNC_PROTO) | 1, {4}});
+  B.addTail(BTF::BTFParam({XS2, 4}));
+  // Type 6: func (dup, refs type 5)
+  B.addType({FnS2, mkInfo(BTF::BTF_KIND_FUNC), {5}});
+
+  EXPECT_EQ(B.typesCount(), 6u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 3u);
+}
+
+TEST(BTFDedupTest, duplicateVar) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t VS = B.addString("myvar");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 2: VAR myvar -> int
+  B.addType({VS, mkInfo(BTF::BTF_KIND_VAR), {1}});
+  B.addTail((uint32_t)1); // global linkage
+
+  // Duplicate:
+  uint32_t IntS2 = B.addString("int");
+  uint32_t VS2 = B.addString("myvar");
+  // Type 3: int (dup)
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 4: VAR myvar (dup, refs type 3)
+  B.addType({VS2, mkInfo(BTF::BTF_KIND_VAR), {3}});
+  B.addTail((uint32_t)1);
+
+  EXPECT_EQ(B.typesCount(), 4u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 2u);
+}
+
+TEST(BTFDedupTest, duplicateTypeTag) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t TagS = B.addString("user");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 2: TYPE_TAG "user" -> int
+  B.addType({TagS, mkInfo(BTF::BTF_KIND_TYPE_TAG), {1}});
+
+  // Duplicate:
+  uint32_t IntS2 = B.addString("int");
+  uint32_t TagS2 = B.addString("user");
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  B.addType({TagS2, mkInfo(BTF::BTF_KIND_TYPE_TAG), {3}});
+
+  EXPECT_EQ(B.typesCount(), 4u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 2u);
+}
+
+TEST(BTFDedupTest, duplicateDeclTag) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t TagS = B.addString("mytag");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 2: DECL_TAG "mytag" on type 1, component_idx=-1
+  B.addType({TagS, mkInfo(BTF::BTF_KIND_DECL_TAG), {1}});
+  B.addTail((uint32_t)-1);
+
+  // Duplicate:
+  uint32_t IntS2 = B.addString("int");
+  uint32_t TagS2 = B.addString("mytag");
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  B.addType({TagS2, mkInfo(BTF::BTF_KIND_DECL_TAG), {3}});
+  B.addTail((uint32_t)-1);
+
+  EXPECT_EQ(B.typesCount(), 4u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 2u);
+}
+
+TEST(BTFDedupTest, differentDeclTagComponentIdx) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t TagS = B.addString("mytag");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 2: DECL_TAG on type 1, component_idx=0
+  B.addType({TagS, mkInfo(BTF::BTF_KIND_DECL_TAG), {1}});
+  B.addTail((uint32_t)0);
+  // Type 3: DECL_TAG on type 1, component_idx=1 — different, should NOT dedup
+  uint32_t TagS2 = B.addString("mytag");
+  B.addType({TagS2, mkInfo(BTF::BTF_KIND_DECL_TAG), {1}});
+  B.addTail((uint32_t)1);
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 3u);
+}
+
+TEST(BTFDedupTest, structDifferentMemberOffsets) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t FooS = B.addString("foo");
+  uint32_t AS = B.addString("a");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  // Type 2: struct foo { int a; } at offset 0
+  B.addType({FooS, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}});
+  B.addTail(BTF::BTFMember({AS, 1, 0}));
+
+  // Type 3: struct foo { int a; } at offset 32 — different offset
+  uint32_t FooS2 = B.addString("foo");
+  uint32_t AS2 = B.addString("a");
+  B.addType({FooS2, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}});
+  B.addTail(BTF::BTFMember({AS2, 1, 32})); // Different offset
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 3u); // int + 2 different structs
+}
+
+TEST(BTFDedupTest, structDifferentMemberNames) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t FooS = B.addString("foo");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  // Type 2: struct foo { int a; }
+  uint32_t AS = B.addString("a");
+  B.addType({FooS, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}});
+  B.addTail(BTF::BTFMember({AS, 1, 0}));
+
+  // Type 3: struct foo { int b; } — different member name
+  uint32_t FooS2 = B.addString("foo");
+  uint32_t BAS = B.addString("b");
+  B.addType({FooS2, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}});
+  B.addTail(BTF::BTFMember({BAS, 1, 0}));
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 3u);
+}
+
+TEST(BTFDedupTest, structDifferentMemberCount) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t FooS = B.addString("foo");
+  uint32_t AS = B.addString("a");
+  uint32_t BS = B.addString("b");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+
+  // Type 2: struct foo { int a; } — 1 member
+  B.addType({FooS, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}});
+  B.addTail(BTF::BTFMember({AS, 1, 0}));
+
+  // Type 3: struct foo { int a; int b; } — 2 members
+  uint32_t FooS2 = B.addString("foo");
+  uint32_t AS2 = B.addString("a");
+  uint32_t BS2 = B.addString("b");
+  B.addType({FooS2, mkInfo(BTF::BTF_KIND_STRUCT) | 2, {8}});
+  B.addTail(BTF::BTFMember({AS2, 1, 0}));
+  B.addTail(BTF::BTFMember({BS2, 1, 32}));
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 3u);
+}
+
+TEST(BTFDedupTest, mutualRecursion) {
+  // struct A { struct B *b; };
+  // struct B { struct A *a; };
+  BTFBuilder B;
+  uint32_t AS = B.addString("A");
+  uint32_t BS = B.addString("B");
+  uint32_t MemA = B.addString("a");
+  uint32_t MemB = B.addString("b");
+
+  // Type 1: ptr to struct B (type 4)
+  B.addType({0, mkInfo(BTF::BTF_KIND_PTR), {4}});
+  // Type 2: ptr to struct A (type 3)
+  B.addType({0, mkInfo(BTF::BTF_KIND_PTR), {3}});
+  // Type 3: struct A { struct B *b; }
+  B.addType({AS, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {8}});
+  B.addTail(BTF::BTFMember({MemB, 1, 0}));
+  // Type 4: struct B { struct A *a; }
+  B.addType({BS, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {8}});
+  B.addTail(BTF::BTFMember({MemA, 2, 0}));
+
+  // Duplicate set:
+  uint32_t AS2 = B.addString("A");
+  uint32_t BS2 = B.addString("B");
+  uint32_t MemA2 = B.addString("a");
+  uint32_t MemB2 = B.addString("b");
+
+  // Type 5: ptr to struct B (type 8)
+  B.addType({0, mkInfo(BTF::BTF_KIND_PTR), {8}});
+  // Type 6: ptr to struct A (type 7)
+  B.addType({0, mkInfo(BTF::BTF_KIND_PTR), {7}});
+  // Type 7: struct A (dup)
+  B.addType({AS2, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {8}});
+  B.addTail(BTF::BTFMember({MemB2, 5, 0}));
+  // Type 8: struct B (dup)
+  B.addType({BS2, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {8}});
+  B.addTail(BTF::BTFMember({MemA2, 6, 0}));
+
+  EXPECT_EQ(B.typesCount(), 8u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  // Should dedup to: ptr(B), ptr(A), struct A, struct B = 4 types
+  EXPECT_EQ(B.typesCount(), 4u);
+}
+
+TEST(BTFDedupTest, diamondDependency) {
+  // Two structs that share a base INT type.
+  // struct S1 { int x; }; struct S2 { int y; };
+  // Both reference the same int. After dedup from two CUs, we should get
+  // 1 int + 2 structs (they're different) = 3 types.
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t S1S = B.addString("S1");
+  uint32_t S2S = B.addString("S2");
+  uint32_t XS = B.addString("x");
+  uint32_t YS = B.addString("y");
+
+  // CU 1:
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});  // 1
+  B.addTail((uint32_t)0);
+  B.addType({S1S, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}});  // 2
+  B.addTail(BTF::BTFMember({XS, 1, 0}));
+  B.addType({S2S, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}});  // 3
+  B.addTail(BTF::BTFMember({YS, 1, 0}));
+
+  // CU 2 (same types, different IDs):
+  uint32_t IntS2 = B.addString("int");
+  uint32_t S1S2 = B.addString("S1");
+  uint32_t S2S2 = B.addString("S2");
+  uint32_t XS2 = B.addString("x");
+  uint32_t YS2 = B.addString("y");
+
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});  // 4
+  B.addTail((uint32_t)0);
+  B.addType({S1S2, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}});  // 5
+  B.addTail(BTF::BTFMember({XS2, 4, 0}));
+  B.addType({S2S2, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}});  // 6
+  B.addTail(BTF::BTFMember({YS2, 4, 0}));
+
+  EXPECT_EQ(B.typesCount(), 6u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  // 1 int + 2 different structs = 3
+  EXPECT_EQ(B.typesCount(), 3u);
+}
+
+TEST(BTFDedupTest, manyDuplicateInts) {
+  BTFBuilder B;
+  const unsigned N = 100;
+  for (unsigned I = 0; I < N; ++I) {
+    uint32_t S = B.addString("int");
+    B.addType({S, mkInfo(BTF::BTF_KIND_INT), {4}});
+    B.addTail((uint32_t)0);
+  }
+
+  EXPECT_EQ(B.typesCount(), N);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 1u);
+}
+
+TEST(BTFDedupTest, enumMembersPreservedAfterDedup) {
+  BTFBuilder B;
+  uint32_t ES = B.addString("color");
+  uint32_t RS = B.addString("RED");
+  uint32_t GS = B.addString("GREEN");
+  uint32_t BS2 = B.addString("BLUE");
+
+  B.addType({ES, mkInfo(BTF::BTF_KIND_ENUM) | 3, {4}});
+  B.addTail(BTF::BTFEnum({RS, 0}));
+  B.addTail(BTF::BTFEnum({GS, 1}));
+  B.addTail(BTF::BTFEnum({BS2, 2}));
+
+  // Duplicate:
+  uint32_t ES2 = B.addString("color");
+  uint32_t RS2 = B.addString("RED");
+  uint32_t GS2 = B.addString("GREEN");
+  uint32_t BS3 = B.addString("BLUE");
+  B.addType({ES2, mkInfo(BTF::BTF_KIND_ENUM) | 3, {4}});
+  B.addTail(BTF::BTFEnum({RS2, 0}));
+  B.addTail(BTF::BTFEnum({GS2, 1}));
+  B.addTail(BTF::BTFEnum({BS3, 2}));
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 1u);
+
+  // Verify enum members are intact.
+  const BTF::CommonType *T = B.findType(1);
+  ASSERT_TRUE(T);
+  EXPECT_EQ(T->getKind(), BTF::BTF_KIND_ENUM);
+  EXPECT_EQ(T->getVlen(), 3u);
+  auto *Vals = reinterpret_cast<const BTF::BTFEnum *>(
+      reinterpret_cast<const uint8_t *>(T) + sizeof(BTF::CommonType));
+  EXPECT_EQ(B.findString(Vals[0].NameOff), "RED");
+  EXPECT_EQ(Vals[0].Val, 0);
+  EXPECT_EQ(B.findString(Vals[1].NameOff), "GREEN");
+  EXPECT_EQ(Vals[1].Val, 1);
+  EXPECT_EQ(B.findString(Vals[2].NameOff), "BLUE");
+  EXPECT_EQ(Vals[2].Val, 2);
+}
+
+TEST(BTFDedupTest, allKindsDedup) {
+  // Build a BTF with one of each type kind, then duplicate all of them.
+  // After dedup, should end up with exactly 19 types.
+  auto BuildAllKinds = [](BTFBuilder &B, uint32_t BaseId) {
+    uint32_t S = B.addString("t");
+    uint32_t M = B.addString("m");
+    uint32_t IntId = BaseId + 1;
+    uint32_t FuncProtoId = BaseId + 12;
+    uint32_t VarId = BaseId + 14;
+
+    B.addType({S, mkInfo(BTF::BTF_KIND_INT), {4}});        // +1
+    B.addTail((uint32_t)0);
+    B.addType({S, mkInfo(BTF::BTF_KIND_PTR), {IntId}});    // +2
+    B.addType({S, mkInfo(BTF::BTF_KIND_ARRAY), {0}});      // +3
+    B.addTail(BTF::BTFArray({IntId, IntId, 10}));
+    B.addType({S, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}}); // +4
+    B.addTail(BTF::BTFMember({M, IntId, 0}));
+    B.addType({S, mkInfo(BTF::BTF_KIND_UNION) | 1, {4}});  // +5
+    B.addTail(BTF::BTFMember({M, IntId, 0}));
+    B.addType({S, mkInfo(BTF::BTF_KIND_ENUM) | 1, {4}});   // +6
+    B.addTail(BTF::BTFEnum({M, 42}));
+    B.addType({S, mkInfo(BTF::BTF_KIND_FWD), {0}});        // +7
+    B.addType({S, mkInfo(BTF::BTF_KIND_TYPEDEF), {IntId}}); // +8
+    B.addType({S, mkInfo(BTF::BTF_KIND_VOLATILE), {IntId}}); // +9
+    B.addType({S, mkInfo(BTF::BTF_KIND_CONST), {IntId}});   // +10
+    B.addType({S, mkInfo(BTF::BTF_KIND_RESTRICT), {IntId}}); // +11
+    B.addType({S, mkInfo(BTF::BTF_KIND_FUNC_PROTO) | 1, {IntId}}); // +12
+    B.addTail(BTF::BTFParam({M, IntId}));
+    B.addType({S, mkInfo(BTF::BTF_KIND_FUNC), {FuncProtoId}}); // +13
+    B.addType({S, mkInfo(BTF::BTF_KIND_VAR), {IntId}});     // +14
+    B.addTail((uint32_t)0);
+    B.addType({S, mkInfo(BTF::BTF_KIND_DATASEC) | 1, {0}}); // +15
+    B.addTail(BTF::BTFDataSec({VarId, 0, 4}));
+    B.addType({S, mkInfo(BTF::BTF_KIND_FLOAT), {4}});       // +16
+    B.addType({S, mkInfo(BTF::BTF_KIND_DECL_TAG), {IntId}}); // +17
+    B.addTail((uint32_t)-1);
+    B.addType({S, mkInfo(BTF::BTF_KIND_TYPE_TAG), {IntId}}); // +18
+    B.addType({S, mkInfo(BTF::BTF_KIND_ENUM64) | 1, {8}});  // +19
+    B.addTail(BTF::BTFEnum64({M, 1, 0}));
+  };
+
+  BTFBuilder B;
+  BuildAllKinds(B, 0);    // Types 1-19
+  BuildAllKinds(B, 19);   // Types 20-38 (duplicates)
+
+  EXPECT_EQ(B.typesCount(), 38u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 19u);
+
+  // Verify all 19 type kinds survive.
+  for (uint32_t Id = 1; Id <= 19; ++Id) {
+    ASSERT_TRUE(B.findType(Id))
+        << "Type " << Id << " missing after dedup";
+  }
+}
+
+TEST(BTFDedupTest, unionDedup) {
+  BTFBuilder B;
+  uint32_t IntS = B.addString("int");
+  uint32_t US = B.addString("myunion");
+  uint32_t XS = B.addString("x");
+  uint32_t YS = B.addString("y");
+
+  // Type 1: int
+  B.addType({IntS, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  // Type 2: union myunion { int x; int y; }
+  B.addType({US, mkInfo(BTF::BTF_KIND_UNION) | 2, {4}});
+  B.addTail(BTF::BTFMember({XS, 1, 0}));
+  B.addTail(BTF::BTFMember({YS, 1, 0}));
+
+  // Duplicate:
+  uint32_t IntS2 = B.addString("int");
+  uint32_t US2 = B.addString("myunion");
+  uint32_t XS2 = B.addString("x");
+  uint32_t YS2 = B.addString("y");
+  B.addType({IntS2, mkInfo(BTF::BTF_KIND_INT), {4}});
+  B.addTail((uint32_t)0);
+  B.addType({US2, mkInfo(BTF::BTF_KIND_UNION) | 2, {4}});
+  B.addTail(BTF::BTFMember({XS2, 3, 0}));
+  B.addTail(BTF::BTFMember({YS2, 3, 0}));
+
+  EXPECT_EQ(B.typesCount(), 4u);
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 2u);
+}
+
+TEST(BTFDedupTest, dedupRoundtripAllKinds) {
+  // Build, dedup, write, parse — verify the output is valid BTF.
+  BTFBuilder B;
+  uint32_t S = B.addString("t");
+  uint32_t M = B.addString("m");
+
+  B.addType({S, mkInfo(BTF::BTF_KIND_INT), {4}});        // 1
+  B.addTail((uint32_t)0);
+  B.addType({S, mkInfo(BTF::BTF_KIND_PTR), {1}});        // 2
+  B.addType({S, mkInfo(BTF::BTF_KIND_STRUCT) | 1, {4}}); // 3
+  B.addTail(BTF::BTFMember({M, 1, 0}));
+  B.addType({S, mkInfo(BTF::BTF_KIND_ENUM) | 1, {4}});   // 4
+  B.addTail(BTF::BTFEnum({M, 99}));
+  B.addType({0, mkInfo(BTF::BTF_KIND_CONST), {1}});      // 5
+
+  // Duplicate int + ptr:
+  uint32_t S2 = B.addString("t");
+  B.addType({S2, mkInfo(BTF::BTF_KIND_INT), {4}});       // 6
+  B.addTail((uint32_t)0);
+  B.addType({S2, mkInfo(BTF::BTF_KIND_PTR), {6}});       // 7
+
+  ASSERT_SUCCEEDED(BTF::dedup(B));
+  EXPECT_EQ(B.typesCount(), 5u);
+
+  SmallVector<uint8_t, 0> Output;
+  B.write(Output, !sys::IsBigEndianHost);
+
+  SmallString<0> Storage;
+  auto Obj = makeELFWithBTF(Output, Storage);
+  ASSERT_TRUE(Obj);
+
+  BTFParser Parser;
+  BTFParser::ParseOptions Opts;
+  Opts.LoadTypes = true;
+  ASSERT_SUCCEEDED(Parser.parse(*Obj, Opts));
+  EXPECT_EQ(Parser.typesCount(), 6u); // 5 types + void
+
+  EXPECT_EQ(Parser.findType(1)->getKind(), BTF::BTF_KIND_INT);
+  EXPECT_EQ(Parser.findType(2)->getKind(), BTF::BTF_KIND_PTR);
+  EXPECT_EQ(Parser.findType(2)->Type, 1u);
+  EXPECT_EQ(Parser.findType(3)->getKind(), BTF::BTF_KIND_STRUCT);
+  EXPECT_EQ(Parser.findType(4)->getKind(), BTF::BTF_KIND_ENUM);
+  EXPECT_EQ(Parser.findType(5)->getKind(), BTF::BTF_KIND_CONST);
+  EXPECT_EQ(Parser.findType(5)->Type, 1u);
+}
+
+} // namespace
diff --git a/llvm/unittests/DebugInfo/BTF/CMakeLists.txt b/llvm/unittests/DebugInfo/BTF/CMakeLists.txt
index 6f7f684c58bed..6f745f87166d5 100644
--- a/llvm/unittests/DebugInfo/BTF/CMakeLists.txt
+++ b/llvm/unittests/DebugInfo/BTF/CMakeLists.txt
@@ -5,6 +5,8 @@ set(LLVM_LINK_COMPONENTS
   )
 
 add_llvm_unittest(DebugInfoBTFTests
+  BTFBuilderTest.cpp
+  BTFDedupTest.cpp
   BTFParserTest.cpp
   )
 



More information about the llvm-commits mailing list