[clang] [llvm] [clang][ssaf] Add entity metadata for linking (PR #214369)

Aviral Goel via cfe-commits cfe-commits at lists.llvm.org
Sun Aug 9 18:20:19 PDT 2026


https://github.com/aviralg updated https://github.com/llvm/llvm-project/pull/214369

>From e105187ed37e710cd0b7a00f03c306b6a1c93cc1 Mon Sep 17 00:00:00 2001
From: Aviral Goel <goel.aviral at gmail.com>
Date: Sun, 9 Aug 2026 18:17:46 -0700
Subject: [PATCH] Add linkage fields with EntityLinker support

---
 .../Core/EntityLinker/EntityLinker.h          | 307 ++++++-
 .../Core/EntityLinker/EntitySummaryEncoding.h |  34 +-
 .../Core/EntityLinker/LinkageRules.h          | 124 +++
 .../Core/Model/EntityLinkage.h                |  89 +-
 .../Core/Model/PrivateFieldNames.def          |   4 +
 .../Core/TUSummary/TUSummaryBuilder.h         |   2 +-
 .../Core/CMakeLists.txt                       |   1 +
 .../Core/EntityLinker/EntityLinker.cpp        | 591 ++++++++++--
 .../Core/EntityLinker/LinkageRules.cpp        | 450 +++++++++
 .../Core/Model/EntityLinkage.cpp              |  28 +-
 .../Core/ModelStringConversions.h             | 119 +++
 .../JSONFormat/JSONEntitySummaryEncoding.cpp  |  46 +-
 .../JSONFormat/JSONEntitySummaryEncoding.h    |  19 +-
 .../JSONFormat/JSONFormatImpl.cpp             | 140 ++-
 .../Serialization/JSONFormat/JSONFormatImpl.h |  29 +
 .../Core/TUSummary/TUSummaryBuilder.cpp       |  14 +-
 .../Core/TUSummary/TUSummaryExtractor.cpp     | 160 +++-
 .../Inputs/tu-summary-bad-array.json          |  18 +-
 .../Inputs/tu-summary-bad-summary.json        |  18 +-
 .../PointerFlow/Inputs/tu-summary-no-key.json |  18 +-
 .../PointerFlow/Inputs/tu-summary.json        |  18 +-
 .../Inputs/tu-summary-bad-id.json             |   6 +-
 .../Inputs/tu-summary-no-key.json             |   6 +-
 .../Inputs/tu-summary.json                    |  18 +-
 .../Inputs/tu-summary-bad-element.json        |  12 +-
 .../Inputs/tu-summary-bad-ptr-level.json      |  12 +-
 .../Inputs/tu-summary-no-key.json             |  12 +-
 .../UnsafeBufferUsage/Inputs/tu-summary.json  |  18 +-
 .../Analysis/Scalable/linkage-extraction.cpp  |  73 ++
 .../ssaf-analyzer/Inputs/lu-tags-only.json    |   6 +-
 .../Scalable/ssaf-analyzer/Inputs/lu.json     |  18 +-
 ...rt-multi-arch-shared-library-nonempty.json |  12 +-
 .../invalid-direct-callee-element.json        |   6 +-
 .../CallGraph/invalid-direct-callee-id.json   |   6 +-
 .../Inputs/CallGraph/missing-def-col.json     |   6 +-
 .../Inputs/CallGraph/missing-def-file.json    |   6 +-
 .../Inputs/CallGraph/missing-def-line.json    |   6 +-
 .../Inputs/CallGraph/missing-def.json         |   6 +-
 .../CallGraph/missing-direct-callees.json     |   6 +-
 .../Inputs/CallGraph/missing-pretty-name.json |   6 +-
 .../CallGraph/missing-virtual-callees.json    |   6 +-
 .../duplicate-entity-id-in-data-map.json      |   6 +-
 .../LUSummary/Inputs/duplicate-entity.json    |  12 +-
 .../Inputs/entity-name-missing-namespace.json |   6 +-
 .../Inputs/entity-name-missing-suffix.json    |   6 +-
 .../Inputs/entity-name-missing-usr.json       |   6 +-
 .../Inputs/linkage-table-duplicate-id.json    |  12 +-
 .../linkage-table-entry-id-not-uint64.json    |   6 +-
 ...kage-table-entry-linkage-invalid-type.json |   6 +-
 .../linkage-table-entry-missing-id.json       |   6 +-
 .../Inputs/linkage-table-extra-id.json        |   6 +-
 .../namespace-element-invalid-kind.json       |   6 +-
 .../namespace-element-missing-kind.json       |   6 +-
 .../namespace-element-missing-name.json       |   6 +-
 .../Inputs/namespace-element-not-object.json  |   6 +-
 .../LUSummary/Inputs/rt-linkage-external.json |   6 +-
 .../LUSummary/Inputs/rt-linkage-internal.json |   6 +-
 .../LUSummary/Inputs/rt-linkage-multiple.json |  18 +-
 .../LUSummary/Inputs/rt-linkage-none.json     |   6 +-
 .../Inputs/rt-two-summary-types.json          |  30 +-
 .../Inputs/rt-nonempty-members.json           |  12 +-
 .../duplicate-entity-id-in-data-map.json      |   6 +-
 .../TUSummary/Inputs/duplicate-entity.json    |   4 +-
 .../Inputs/entity-name-missing-suffix.json    |   2 +-
 .../Inputs/entity-name-missing-usr.json       |   2 +-
 .../Inputs/id-table-entry-missing-name.json   |   2 +-
 .../Inputs/linkage-table-duplicate-id.json    |   4 +-
 .../linkage-table-entry-id-not-uint64.json    |   2 +-
 ...able-entry-linkage-invalid-coalescing.json |  16 +
 ...able-entry-linkage-missing-coalescing.json |  16 +
 .../linkage-table-entry-missing-id.json       |   2 +-
 .../Inputs/linkage-table-extra-id.json        |   2 +-
 .../TUSummary/Inputs/rt-linkage-external.json |   6 +-
 .../TUSummary/Inputs/rt-linkage-internal.json |   6 +-
 .../TUSummary/Inputs/rt-linkage-multiple.json |  18 +-
 .../TUSummary/Inputs/rt-linkage-none.json     |   6 +-
 .../Inputs/rt-two-summary-types.json          |  30 +-
 .../ssaf-format/TUSummary/linkage.test        |  22 +
 .../ssaf-linker/Inputs/carry-coff.json        | 115 +++
 .../ssaf-linker/Inputs/carry-elf.json         | 115 +++
 .../ssaf-linker/Inputs/carry-macho.json       | 115 +++
 .../ssaf-linker/Inputs/join-lhs-coff.json     | 139 +++
 .../ssaf-linker/Inputs/join-lhs-elf.json      | 139 +++
 .../ssaf-linker/Inputs/join-lhs-macho.json    | 139 +++
 .../ssaf-linker/Inputs/join-rhs-coff.json     | 139 +++
 .../ssaf-linker/Inputs/join-rhs-elf.json      | 139 +++
 .../ssaf-linker/Inputs/join-rhs-macho.json    | 139 +++
 .../Scalable/ssaf-linker/Inputs/tu-1.json     |  36 +-
 .../Scalable/ssaf-linker/Inputs/tu-2.json     |  36 +-
 .../Inputs/tu-invalid-entity-id-multikey.json |   6 +-
 .../Inputs/tu-invalid-entity-id-ref.json      |   6 +-
 .../Inputs/tu-invalid-entity-id-value.json    |   6 +-
 .../Inputs/tu-redefines-shared-ext.json       |  43 +
 .../ssaf-linker/Inputs/vis-lhs-elf.json       |  64 ++
 .../ssaf-linker/Inputs/vis-lhs-macho.json     |  64 ++
 .../ssaf-linker/Inputs/vis-rhs-elf.json       |  64 ++
 .../ssaf-linker/Inputs/vis-rhs-macho.json     |  64 ++
 .../Scalable/ssaf-linker/Outputs/libtwo.json  |  72 +-
 .../Scalable/ssaf-linker/Outputs/lu-1+2.json  |  66 +-
 .../Scalable/ssaf-linker/Outputs/lu-1.json    |  36 +-
 .../Scalable/ssaf-linker/Outputs/lu-2.json    |  36 +-
 .../Analysis/Scalable/ssaf-linker/help.test   |   9 +
 .../ssaf-linker/linkage-carry-through.test    |  70 ++
 .../ssaf-linker/linkage-join-visibility.test  |  62 ++
 .../ssaf-linker/linkage-join-winner.test      |  74 ++
 .../Scalable/ssaf-linker/linking-errors.test  |   5 +
 .../Scalable/ssaf-linker/linking.test         |  10 +
 .../Scalable/ssaf-linker/verbose.test         |   7 +-
 clang/tools/clang-ssaf-linker/SSAFLinker.cpp  | 103 ++-
 .../PointerFlow/PointerFlowWPATest.cpp        |   7 +-
 .../UnsafeBufferUsageWPATest.cpp              |   7 +-
 .../EntityLinkageTest.cpp                     |  96 +-
 .../EntityLinkerTest.cpp                      | 869 +++++++++++++++++-
 .../SharedLexicalRepresentationFormatTest.cpp |   7 +-
 .../SummaryData/SummaryDataTest.cpp           |   7 +-
 .../ScalableStaticAnalysis/TestFixture.h      |  30 +
 .../AnalysisDriverTest.cpp                    |   7 +-
 .../UnsafeBufferReachableAnalysisTest.cpp     |   7 +-
 docs/cmake_install.cmake                      |  45 +
 docs/ssaf-linker-coff-behavior.md             | 505 ++++++++++
 docs/ssaf-linker-elf-behavior.md              | 742 +++++++++++++++
 docs/ssaf-linker-implementation-plan.md       | 607 ++++++++++++
 docs/ssaf-linker-macho-behavior.md            | 662 +++++++++++++
 123 files changed, 8102 insertions(+), 410 deletions(-)
 create mode 100644 clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/LinkageRules.h
 create mode 100644 clang/lib/ScalableStaticAnalysis/Core/EntityLinker/LinkageRules.cpp
 create mode 100644 clang/test/Analysis/Scalable/linkage-extraction.cpp
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-linkage-invalid-coalescing.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-linkage-missing-coalescing.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-coff.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-elf.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-macho.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-coff.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-elf.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-macho.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-coff.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-elf.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-macho.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-redefines-shared-ext.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-lhs-elf.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-lhs-macho.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-rhs-elf.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-rhs-macho.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/linkage-carry-through.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/linkage-join-visibility.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/linkage-join-winner.test
 create mode 100644 docs/cmake_install.cmake
 create mode 100644 docs/ssaf-linker-coff-behavior.md
 create mode 100644 docs/ssaf-linker-elf-behavior.md
 create mode 100644 docs/ssaf-linker-implementation-plan.md
 create mode 100644 docs/ssaf-linker-macho-behavior.md

diff --git a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h
index f07def1a9b344..365d293dcbec6 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h
@@ -15,29 +15,99 @@
 #define LLVM_CLANG_SCALABLESTATICANALYSIS_CORE_ENTITYLINKER_ENTITYLINKER_H
 
 #include "clang/ScalableStaticAnalysis/Core/EntityLinker/LUSummaryEncoding.h"
+#include "clang/ScalableStaticAnalysis/Core/EntityLinker/LinkageRules.h"
 #include "llvm/Support/Error.h"
 #include "llvm/TargetParser/Triple.h"
 #include <map>
 #include <memory>
 #include <set>
+#include <utility>
 #include <vector>
 
 namespace clang::ssaf {
 
 class TUSummaryEncoding;
 
+/// How the linker reports entities that no linked TU defines.
+enum class UnresolvedPolicy : uint8_t {
+  Ignore, ///< do not check
+  Warn,   ///< report on stderr and keep linking
+  Error   ///< fail the link
+};
+
+/// How the linker reports summary data that disagrees between two definitions
+/// required to be identical.
+enum class ODRMismatchPolicy : uint8_t {
+  Ignore, ///< do not check
+  Warn,   ///< report on stderr and keep linking
+  Error   ///< fail the link
+};
+
 class EntityLinker {
+  friend class TestFixture;
+
+  /// Records, for each LU EntityId a TU resolved to, whether that TU's summary
+  /// data for the entity is the copy to keep. It is false when an earlier TU's
+  /// occurrence won the linkage reconciliation, in which case the data the TU
+  /// carries for that entity is dropped.
+  using DataSelectionMap = std::map<EntityId, bool>;
+
   LUSummaryEncoding Output;
   std::set<BuildNamespace> ProcessedTUNamespaces;
 
+  /// The resolution rules for the link unit's object format. Owned statically
+  /// by LinkageRules::forTarget().
+  const LinkageRules *Rules;
+
+  /// Whether a multiple definition is reported as a warning rather than
+  /// failing the link.
+  bool WarnOnMultipleDefinitions;
+
+  /// How entities that no linked TU defines are reported.
+  UnresolvedPolicy UnresolvedSymbols;
+
+  /// How summary data that disagrees between ODR definitions is reported.
+  ODRMismatchPolicy ODRMismatch;
+
+  /// Summary data displaced by a duplicate ODR definition, kept only when
+  /// \c ODRMismatch asks for the comparison. Per-link scratch, compared once
+  /// the whole TU has been patched into the LU EntityId space.
+  struct DisplacedData {
+    SummaryName Name;                            ///< the summary it belongs to
+    EntityId Id;                                 ///< the LU entity it describes
+    std::unique_ptr<EntitySummaryEncoding> Data; ///< the displaced encoding
+  };
+  std::vector<DisplacedData> Displaced;
+
+  /// Names of the LU entities, kept so that finalization can name them in
+  /// diagnostics. EntityIdTable offers no reverse lookup.
+  std::map<EntityId, EntityName> EntityNames;
+
 public:
   /// Constructs an EntityLinker to link TU summaries into a LU summary.
   ///
-  /// \param TargetTriple The target triple of the link unit. Every linked TU
-  ///        must report the same triple.
+  /// \param TargetTriple The target triple of the link unit. It selects the
+  ///        resolution rules to emulate, and every linked TU must report a
+  ///        compatible triple.
   /// \param LUNamespace The namespace identifying this link unit.
-  EntityLinker(llvm::Triple TargetTriple, NestedBuildNamespace LUNamespace)
-      : Output(std::move(TargetTriple), std::move(LUNamespace)) {}
+  /// \param WarnOnMultipleDefinitions Report an external entity defined by more
+  ///        than one TU as a warning on stderr and keep linking. By default
+  ///        such an entity fails the link, matching a linker's
+  ///        multiple-definition rule.
+  /// \param UnresolvedSymbols How to report entities that no linked TU defines.
+  ///        Defaults to \c Ignore because a link unit is usually an
+  ///        intermediate artifact, where unresolved references are expected.
+  /// \param ODRMismatch How to report two definitions that were required to be
+  ///        identical but whose summary data differs. Defaults to \c Ignore
+  ///        because the check costs an extra patch of every displaced encoding.
+  EntityLinker(llvm::Triple TargetTriple, NestedBuildNamespace LUNamespace,
+               bool WarnOnMultipleDefinitions = false,
+               UnresolvedPolicy UnresolvedSymbols = UnresolvedPolicy::Ignore,
+               ODRMismatchPolicy ODRMismatch = ODRMismatchPolicy::Ignore)
+      : Output(std::move(TargetTriple), std::move(LUNamespace)),
+        Rules(&LinkageRules::forTarget(Output.TargetTriple)),
+        WarnOnMultipleDefinitions(WarnOnMultipleDefinitions),
+        UnresolvedSymbols(UnresolvedSymbols), ODRMismatch(ODRMismatch) {}
 
   /// Links a TU summary into a LU summary.
   ///
@@ -47,47 +117,250 @@ class EntityLinker {
   /// \param Summary The TU summary to link. Ownership is transferred.
   /// \returns Error if the TU namespace has already been linked or if patching
   ///          fails, success otherwise. Corrupted summary data (missing linkage
-  ///          information, duplicate entity IDs, etc.) triggers a fatal error.
+  ///          information, duplicate entity IDs, etc.) triggers a fatal error,
+  ///          as does an external entity defined by more than one TU unless
+  ///          warnings were requested.
   llvm::Error link(std::unique_ptr<TUSummaryEncoding> Summary);
 
   /// Returns the accumulated LU summary.
   ///
+  /// Runs finalization first: rules that need the complete link unit, such as
+  /// reporting entities no TU defined and demoting hidden entities, cannot run
+  /// per-TU because a later TU can change the answer.
+  ///
   /// \returns LU summary containing all the deduplicated and patched entity
   /// summaries.
-  LUSummaryEncoding takeOutput() && { return std::move(Output); }
+  LUSummaryEncoding takeOutput() && {
+    finalize();
+    return std::move(Output);
+  }
 
 private:
+  /// Applies the rules that need the whole link unit. See takeOutput().
+  void finalize();
+
+  /// Reports entities that no linked TU defined, under \c UnresolvedSymbols.
+  ///
+  /// An undefined weak reference is legal by design and is never reported:
+  ///
+  ///   __attribute__((weak)) int maybe_missing(void);
+  ///   $ ld.lld -o out undef_weak_main.o     -> links
+  ///
+  /// whereas a strong one fails when linking an executable. We default to
+  /// Ignore rather than emulating a specific platform because a link unit is
+  /// typically intermediate; see the plan's §3.1.
+  void reportUnresolvedEntities() const;
+
+  /// Demotes entities the target treats as hidden from `External` to
+  /// `Internal`, so a later link stage cannot resolve against them.
+  ///
+  /// This is the link-unit analogue of a hidden ELF symbol becoming LOCAL and
+  /// dropping out of .dynsym:
+  ///
+  ///   // tu1.c
+  ///   int v(void) { return 1; }
+  ///   // tu2.c
+  ///   __attribute__((visibility("hidden"))) int v(void);
+  ///   $ ld.lld -shared -o out.so tu1.o tu2.o
+  ///     v: FUNC LOCAL HIDDEN, absent from .dynsym
+  ///
+  /// Only the linkage type changes: the entity keeps its name, because every
+  /// EntityId already patched into the summary data refers to it, and external
+  /// names are LU-qualified so they cannot collide across link units.
+  /// (ELF doc §4 V1.)
+  void demoteHiddenEntities();
+
+  /// The binding the target emits for \p Linkage, per the active rules.
+  EntityBinding effectiveBinding(const EntityLinkage &Linkage) const;
+
+  /// Compares each displaced encoding against the one kept in its place.
+  ///
+  /// Only meaningful for entities whose definitions were required to be
+  /// identical: for a plain weak definition, differing bodies are legal and
+  /// the same check would be noise. Runs after patch(), because the two
+  /// encodings are only comparable once both are in the LU EntityId space.
+  ///
+  /// \param TUNamespace The namespace of the TU whose data was displaced.
+  void reportODRMismatches(const NestedBuildNamespace &TUNamespace);
+
+  /// Returns true if \p Current and \p Incoming are two definitions of the
+  /// same external entity that the target cannot both accept.
+  ///
+  /// \param Current The linkage already recorded for the entity.
+  /// \param Incoming The linkage of the occurrence being linked.
+  bool isConflictingDefinition(const EntityLinkage &Current,
+                               const EntityLinkage &Incoming) const;
+
+  /// Decides whether the summary data of the occurrence being linked replaces
+  /// the data already linked for the same external entity.
+  ///
+  /// A definition beats a declaration, and among definitions the stronger
+  /// binding wins. Every other case keeps the data already linked, so ties
+  /// resolve in favour of whichever occurrence was linked first. This is the
+  /// one reconciliation rule that is deliberately order-sensitive, and every
+  /// platform agrees on it:
+  ///
+  ///   __attribute__((weak)) int f(void) { return 1; }   // weak_a.c
+  ///   __attribute__((weak)) int f(void) { return 2; }   // weak_b.c
+  ///   $ ld.lld -o out main.o weak_a.o weak_b.o
+  ///     f -> returns 1, the first one linked
+  ///
+  /// (ELF doc §1 P4, Mach-O doc §1 M4.)
+  ///
+  /// \param Current The linkage already recorded for the entity.
+  /// \param Incoming The linkage of the occurrence being linked.
+  bool incomingDataWins(const EntityLinkage &Current,
+                        const EntityLinkage &Incoming) const;
+
+  /// Reconciles two occurrences of the same external entity into the linkage
+  /// recorded for it.
+  ///
+  /// Binding and coalescing describe definitions, so a declaration contributes
+  /// neither when the other occurrence defines the entity — every platform
+  /// agrees that a weak declaration leaves no trace on a common definition
+  /// (ELF doc §6.1, Mach-O doc §6.1, COFF doc §6.2). Visibility merges across
+  /// all occurrences, including ones that lose resolution.
+  ///
+  /// The result is commutative except where the target itself is
+  /// order-dependent; see LinkageRules::isOrderDependentMerge().
+  ///
+  /// \param Current The linkage already recorded for the entity.
+  /// \param Incoming The linkage of the occurrence being linked.
+  /// \pre Both occurrences have the same linkage type. Namespace resolution
+  ///      guarantees this for every collision the linker reconciles.
+  EntityLinkage mergeLinkage(const EntityLinkage &Current,
+                             const EntityLinkage &Incoming) const;
+
+  /// Records \p TUNamespace as linked into this link unit.
+  ///
+  /// \param TUNamespace The namespace of the TU being linked.
+  /// \returns Error if a TU with this namespace has already been linked,
+  ///          success otherwise.
+  llvm::Error checkTUNotAlreadyLinked(const BuildNamespace &TUNamespace);
+
+  /// Verifies that \p TUTriple selects the same resolution rules as the link
+  /// unit's own triple.
+  ///
+  /// Rules differ enough between formats that applying the wrong ones is
+  /// silently incorrect: Mach-O ranks a weak definition above a common while
+  /// ELF and COFF do the reverse, and COFF rejects two weak definitions the
+  /// others accept. The OS version is ignored, since it does not affect
+  /// resolution.
+  ///
+  /// \returns Error if the TU targets a different platform, success otherwise.
+  llvm::Error checkTUTargetMatches(const llvm::Triple &TUTriple,
+                                   const BuildNamespace &TUNamespace) const;
+
+  /// Fails the link if \p Linkage holds a value the target cannot express.
+  ///
+  /// \param Name The entity the linkage belongs to, for diagnostics.
+  void reportIfNotRepresentable(const EntityLinkage &Linkage,
+                                const EntityName &Name) const;
+
+  /// Warns when reconciling these two occurrences gives an order-dependent
+  /// result, which means the program itself is ambiguous.
+  void reportIfOrderDependent(const EntityLinkage &Current,
+                              const EntityLinkage &Incoming,
+                              const EntityName &Name) const;
+
+  /// Fails the link if a collision happened on a non-`External` entity.
+  ///
+  /// `None` and `Internal` entities are qualified with their TU namespace, so
+  /// they can never collide across TUs. A collision means the TU summary is
+  /// corrupted or namespace resolution is buggy.
+  ///
+  /// \param NewId The LU EntityId that collided.
+  /// \param Linkage The linkage of the colliding occurrence.
+  static void reportIfLinkageIsNotExternal(EntityId NewId,
+                                           const EntityLinkage &Linkage);
+
+  /// Reports an external entity that more than one TU defines.
+  ///
+  /// Does nothing unless \p Current and \p Incoming both strongly define the
+  /// entity. Otherwise it terminates with a fatal error, or emits a warning on
+  /// stderr and returns if \c WarnOnMultipleDefinitions is set.
+  ///
+  /// \param Current The linkage already recorded for the entity.
+  /// \param Incoming The linkage of the occurrence being linked.
+  /// \param Name The LU entity name the two occurrences resolved to.
+  /// \param TUNamespace The namespace of the TU carrying the extra definition.
+  /// \returns True if the two occurrences conflict, in which case the caller
+  ///          keeps the definition already linked and ignores \p Incoming
+  ///          entirely rather than reconciling the two.
+  bool reportIfDefinitionsConflict(
+      const EntityLinkage &Current, const EntityLinkage &Incoming,
+      const EntityName &Name, const NestedBuildNamespace &TUNamespace) const;
+
   /// Resolves a TU entity name to an LU entity name and ID.
   ///
   /// \param OldName The entity name in the TU namespace.
   /// \param Linkage The linkage determining namespace resolution strategy.
-  /// \returns The resolved LU EntityId.
-  EntityId resolveEntity(const EntityName &OldName,
-                         const EntityLinkage &Linkage,
-                         const NestedBuildNamespace &TUNamespace);
+  /// \param TUNamespace The namespace of the TU being linked.
+  /// \returns The resolved LU EntityId, paired with whether this TU's summary
+  ///          data for it is the copy to keep.
+  std::pair<EntityId, bool>
+  resolveEntity(const EntityName &OldName, const EntityLinkage &Linkage,
+                const NestedBuildNamespace &TUNamespace);
 
   /// Resolves each TU EntityId to its corresponding LU EntityId.
   ///
   /// \param Summary The TU summary whose entities are being resolved.
-  /// \returns A map from TU EntityIds to their corresponding LU EntityIds.
-  std::map<EntityId, EntityId> resolve(const TUSummaryEncoding &Summary);
+  /// \returns A map from TU EntityIds to their corresponding LU EntityIds,
+  ///          paired with the data selection for each LU EntityId resolved.
+  std::pair<EntityResolutionMap, DataSelectionMap>
+  resolve(const TUSummaryEncoding &Summary);
 
   /// Merges all summary data from a TU summary into the LU Summary.
   ///
   /// \param Summary The TU summary whose data is being merged.
-  /// \param EntityResolutionTable Map from TU EntityIds to LU EntityIds.
+  /// \param Resolution Map from TU EntityIds to LU EntityIds.
+  /// \param DataSelection Which LU EntityIds this TU owns the data for.
   /// \returns Pointers to each EntitySummaryEncoding successfully merged.
   std::vector<EntitySummaryEncoding *>
-  merge(TUSummaryEncoding &Summary,
-        const std::map<EntityId, EntityId> &EntityResolutionTable);
+  merge(TUSummaryEncoding &Summary, const EntityResolutionMap &Resolution,
+        const DataSelectionMap &DataSelection);
+
+  /// Merges one summary's data from a TU summary into the LU summary.
+  ///
+  /// \param SN The summary the data belongs to.
+  /// \param DataMap The TU's encodings for \p SN, keyed by TU EntityId.
+  /// \param Summary The TU summary being merged.
+  /// \param Resolution Map from TU EntityIds to LU EntityIds.
+  /// \param DataSelection Which LU EntityIds this TU owns the data for.
+  /// \returns Pointers to each EntitySummaryEncoding successfully merged.
+  std::vector<EntitySummaryEncoding *>
+  mergeSummaryData(const SummaryName &SN, EntityDataMap &DataMap,
+                   const TUSummaryEncoding &Summary,
+                   const EntityResolutionMap &Resolution,
+                   const DataSelectionMap &DataSelection);
+
+  /// Merges one entity's encoding into the LU summary data for a summary.
+  ///
+  /// The encoding is dropped when an earlier TU already contributed the copy
+  /// to keep for \p NewId.
+  ///
+  /// \param OutputData The LU summary data to merge into.
+  /// \param SN The summary the data belongs to, for diagnostics.
+  /// \param NewId The LU EntityId the data describes.
+  /// \param Encoding The TU's encoding for \p NewId. Ownership is transferred
+  ///        only when the encoding is kept.
+  /// \param Linkage The TU's linkage for the entity.
+  /// \param DataFromIncoming Whether \p Encoding is the copy to keep.
+  /// \returns The merged encoding to patch, or nullptr if it was dropped
+  ///          without being kept for comparison.
+  EntitySummaryEncoding *
+  mergeEntityData(EntityDataMap &OutputData, const SummaryName &SN,
+                  EntityId NewId,
+                  std::unique_ptr<EntitySummaryEncoding> &Encoding,
+                  const EntityLinkage &Linkage, bool DataFromIncoming);
 
   /// Patches EntityId references in merged summary data.
   ///
   /// \param PatchTargets Vector of summary encodings that need patching.
-  /// \param EntityResolutionTable Map from TU EntityIds to LU EntityIds.
+  /// \param Resolution Map from TU EntityIds to LU EntityIds.
   /// \returns Error if patching any encoding fails, success otherwise.
   llvm::Error patch(const std::vector<EntitySummaryEncoding *> &PatchTargets,
-                    const std::map<EntityId, EntityId> &EntityResolutionTable);
+                    const EntityResolutionMap &Resolution);
 };
 
 } // namespace clang::ssaf
diff --git a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntitySummaryEncoding.h b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntitySummaryEncoding.h
index 9442ecbbe24cc..76cf0de5fc462 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntitySummaryEncoding.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntitySummaryEncoding.h
@@ -17,9 +17,20 @@
 #include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h"
 #include "llvm/Support/Error.h"
 #include <map>
+#include <memory>
 
 namespace clang::ssaf {
 
+class EntitySummaryEncoding;
+
+/// Maps each EntityId in a TU summary to the EntityId it resolved to in the
+/// link unit.
+using EntityResolutionMap = std::map<EntityId, EntityId>;
+
+/// Maps each entity of one summary to its encoded summary data.
+using EntityDataMap =
+    std::map<EntityId, std::unique_ptr<EntitySummaryEncoding>>;
+
 /// Represents EntitySummary data in its serialized, format-specific encoding.
 ///
 /// This abstract base class allows the entity linker to manipulate serialized
@@ -32,9 +43,26 @@ class EntitySummaryEncoding {
 
   /// Updates EntityId references in the encoded data.
   ///
-  /// \param EntityResolutionTable Mapping from old EntityIds to new EntityIds.
-  virtual llvm::Error
-  patch(const std::map<EntityId, EntityId> &EntityResolutionTable) = 0;
+  /// \param Resolution Mapping from old EntityIds to new EntityIds.
+  virtual llvm::Error patch(const EntityResolutionMap &Resolution) = 0;
+
+  /// Returns an identifier unique to this encoding's concrete class.
+  ///
+  /// SSAF is built without RTTI, so \c equals implementations use this to
+  /// establish that another encoding has the same type before casting it.
+  /// Implementations return the address of a static object scoped to the
+  /// implementing class.
+  virtual const void *getEncodingKind() const = 0;
+
+  /// Returns true if \p Other encodes equivalent data.
+  ///
+  /// Only encodings of the same concrete class can compare equal;
+  /// implementations return false for any other \c getEncodingKind().
+  ///
+  /// Used to detect definitions that were required to be identical but are
+  /// not. \pre Both operands have been patched into the same EntityId space;
+  /// comparing encodings that reference different spaces is meaningless.
+  virtual bool equals(const EntitySummaryEncoding &Other) const = 0;
 };
 
 } // namespace clang::ssaf
diff --git a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/LinkageRules.h b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/LinkageRules.h
new file mode 100644
index 0000000000000..37c9c1e9fb689
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/LinkageRules.h
@@ -0,0 +1,124 @@
+//===- LinkageRules.h - Per-platform symbol resolution ----------*- 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
+//
+//===----------------------------------------------------------------------===//
+//
+//  This file defines LinkageRules, which supplies the symbol resolution rules
+//  the entity linker emulates for a given object format. Every rule here is
+//  grounded in an observed linker behaviour; see
+//  docs/ssaf-linker-{elf,macho,coff}-behavior.md.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSIS_CORE_ENTITYLINKER_LINKAGERULES_H
+#define LLVM_CLANG_SCALABLESTATICANALYSIS_CORE_ENTITYLINKER_LINKAGERULES_H
+
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityLinkage.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/TargetParser/Triple.h"
+
+namespace clang::ssaf {
+
+/// The platform-specific symbol resolution rules the linker emulates.
+///
+/// The EntityLinkage enums carry no ordering of their own, because precedence
+/// is platform-specific: Mach-O ranks a weak definition above a common one
+/// while ELF and COFF do the reverse, and Mach-O merges visibility to the
+/// least restrictive while ELF merges to the most. The ranks below answer
+/// "which value wins the join", not "which value is semantically greater", and
+/// are deliberately inverted for some fields on some targets.
+class LinkageRules {
+public:
+  virtual ~LinkageRules() = default;
+
+  /// Returns the rules for \p TargetTriple, dispatching on its object format.
+  ///
+  /// The returned reference has static storage duration. An unsupported object
+  /// format is a fatal error naming the triple.
+  static const LinkageRules &forTarget(const llvm::Triple &TargetTriple);
+
+  /// The object format these rules emulate, for diagnostics.
+  virtual llvm::StringRef getName() const = 0;
+
+  /// Lowers a source-level linkage to the binding this platform actually
+  /// emits.
+  ///
+  /// A summary records what the source declared, so an inline function is
+  /// Strong + ODR regardless of target. The object formats disagree about how
+  /// to express that, and the disagreement changes resolution, so every
+  /// comparison below is made on the lowered value rather than the raw one.
+  virtual EntityBinding
+  effectiveBinding(const EntityLinkage &Linkage) const = 0;
+
+  /// Precedence among bindings: the greater rank prevails when two definitions
+  /// of the same entity collide.
+  ///
+  /// \param B A binding already lowered by effectiveBinding().
+  virtual unsigned strengthRank(EntityBinding B) const = 0;
+
+  /// Precedence among visibilities: the greater rank prevails when two
+  /// occurrences of the same entity disagree.
+  virtual unsigned visibilityRank(EntityVisibility V) const = 0;
+
+  /// Returns true if two definitions of the same external entity cannot
+  /// coexist, which is the multiple-definition error a real linker reports.
+  ///
+  /// This is not derivable from the ranks: COFF licenses duplicate definitions
+  /// by COMDAT rather than by weakness, and rejects two weak definitions that
+  /// ELF and Mach-O both accept.
+  ///
+  /// \pre Both occurrences are definitions.
+  virtual bool isConflictingDefinition(const EntityLinkage &Current,
+                                       const EntityLinkage &Incoming) const = 0;
+
+  /// Merges the visibility of two occurrences of the same entity.
+  ///
+  /// Defaults to the higher visibilityRank(). Mach-O overrides this because
+  /// its rule for common symbols depends on the bindings, not just the
+  /// visibilities.
+  virtual EntityVisibility mergeVisibility(const EntityLinkage &Current,
+                                           const EntityLinkage &Incoming) const;
+
+  /// Returns true if merging these two occurrences gives a result that depends
+  /// on which was linked first, so the caller can warn that the program is
+  /// ambiguous.
+  virtual bool isOrderDependentMerge(const EntityLinkage &Current,
+                                     const EntityLinkage &Incoming) const;
+
+  /// Normalizes \p Linkage to what this platform can represent.
+  ///
+  /// Values the platform's toolchain silently drops are coerced to their
+  /// platform equivalent. Values the toolchain could never have produced are
+  /// left for isRepresentable() to reject, since they indicate a corrupted or
+  /// hand-edited summary rather than ordinary portable source.
+  virtual EntityLinkage normalize(const EntityLinkage &Linkage) const;
+
+  /// Returns false if \p Linkage holds a value this platform's toolchain could
+  /// not have produced, which the caller reports as a fatal error.
+  ///
+  /// Distinct from normalize(): a value that is merely dropped at emission is
+  /// coerced, whereas a value clang refuses to emit at all is rejected.
+  virtual bool isRepresentable(const EntityLinkage &Linkage) const;
+
+protected:
+  // Friendship with EntityLinkage does not extend to subclasses, so the base
+  // reads its private fields on their behalf.
+  static EntityLinkageType linkageOf(const EntityLinkage &L);
+  static EntityBinding bindingOf(const EntityLinkage &L);
+  static EntityCoalescing coalescingOf(const EntityLinkage &L);
+  static EntityVisibility visibilityOf(const EntityLinkage &L);
+
+  /// True when \p Linkage defines the entity rather than declaring it.
+  static bool defines(const EntityLinkage &Linkage);
+
+  /// Builds a copy of \p Linkage with its visibility replaced.
+  static EntityLinkage withVisibility(const EntityLinkage &Linkage,
+                                      EntityVisibility V);
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSIS_CORE_ENTITYLINKER_LINKAGERULES_H
diff --git a/clang/include/clang/ScalableStaticAnalysis/Core/Model/EntityLinkage.h b/clang/include/clang/ScalableStaticAnalysis/Core/Model/EntityLinkage.h
index f90571c672452..d4df0510ce63d 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Core/Model/EntityLinkage.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Core/Model/EntityLinkage.h
@@ -11,27 +11,93 @@
 
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/raw_ostream.h"
+#include <cstdint>
 
 namespace clang::ssaf {
 
-enum class EntityLinkageType {
+/// Symbol scope.
+///
+/// Enumerator order is insignificant: the linker never compares these values,
+/// it switches on them by name.
+enum class EntityLinkageType : uint8_t {
   None,     ///< local variables
   Internal, ///< static functions/variables, anonymous namespace
   External  ///< globally visible across translation units (including parameters
             ///< of functions with external linkage)
 };
 
-/// Represents the linkage properties of an entity in the program model.
+/// Symbol strength: which definition prevails when two of them collide.
+///
+/// Enumerator order is insignificant. Precedence is platform-specific — on
+/// Mach-O a weak definition displaces a common, while on ELF and COFF the
+/// common wins — so comparisons must go through LinkageRules::strengthRank()
+/// rather than comparing these values directly.
+enum class EntityBinding : uint8_t {
+  Undefined, ///< a reference with no definition in this unit
+  Weak,      ///< __attribute__((weak)); overridden by a strong definition
+  Common,    ///< C tentative definition; merged, size = max
+  Strong     ///< ordinary definition
+};
+
+/// How far a symbol escapes its link unit.
+///
+/// Enumerator order is insignificant. ELF merges to the most restrictive
+/// visibility, Mach-O to the least, and COFF has no visibility at all; see
+/// LinkageRules::mergeVisibility().
+enum class EntityVisibility : uint8_t {
+  Default,  ///< participates in cross-LU resolution; exported/interposable
+  Hidden,   ///< visible within the LU, not exported past its boundary
+  Protected ///< exported but bound to the defining LU (not interposable)
+};
+
+/// Whether every definition of the entity is required to be identical, as a
+/// COMDAT group (ELF and COFF) or a .weak_definition (Mach-O) guarantees.
+///
+/// This is independent of EntityBinding, because the object formats encode the
+/// two properties separately: an inline function is emitted weak on ELF and
+/// Mach-O but strong on COFF, and in every case the ODR guarantee is carried by
+/// a mechanism other than the binding. See LinkageRules::effectiveBinding().
+///
+/// Modelled as an enum rather than a bool so that COFF's other
+/// IMAGE_COMDAT_SELECT_* kinds can be added without a schema change.
+enum class EntityCoalescing : uint8_t {
+  None, ///< definitions are unrelated; at most one may exist
+  ODR   ///< all definitions are required to be identical and are coalesced
+};
+
+/// Whether this occurrence defines the entity or merely declares/references it.
+///
+/// Enumerator order is insignificant; see
+/// LinkageRules::definitionKindRank().
+enum class EntityDefinitionKind : uint8_t {
+  Declaration, ///< declares or references the entity
+  Definition   ///< defines the entity
+};
+
+/// Represents the linker-relevant properties of an entity in the program model.
 ///
-/// EntityLinkage captures whether an entity has no linkage, internal linkage,
-/// or external linkage, which determines its visibility and accessibility
-/// across translation units.
+/// EntityLinkage captures the scope, strength, coalescing, visibility, and
+/// definition state of an entity, which together determine how it is resolved
+/// and exported across translation units and link units.
+///
+/// The values are recorded as the source declares them; translating them into
+/// what a particular object format would emit is the linker's job, via
+/// LinkageRules.
 class EntityLinkage {
+  friend class EntityLinker;
+  friend class LinkageRules;
   friend class SerializationFormat;
   friend class TestFixture;
+  friend llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
+                                       const EntityLinkage &Linkage);
 
 public:
-  constexpr explicit EntityLinkage(EntityLinkageType L) : Linkage(L) {}
+  constexpr EntityLinkage(EntityLinkageType Linkage, EntityBinding Binding,
+                          EntityCoalescing Coalescing,
+                          EntityVisibility Visibility,
+                          EntityDefinitionKind DefinitionKind)
+      : Linkage(Linkage), Binding(Binding), Coalescing(Coalescing),
+        Visibility(Visibility), DefinitionKind(DefinitionKind) {}
 
   EntityLinkageType getLinkage() const { return Linkage; }
 
@@ -40,10 +106,21 @@ class EntityLinkage {
 
 private:
   EntityLinkageType Linkage;
+  EntityBinding Binding;
+  EntityCoalescing Coalescing;
+  EntityVisibility Visibility;
+  EntityDefinitionKind DefinitionKind;
 };
 
 /// Writes a string representation of the linkage type to the stream.
 llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, EntityLinkageType Linkage);
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, EntityBinding Binding);
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
+                              EntityCoalescing Coalescing);
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
+                              EntityVisibility Visibility);
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
+                              EntityDefinitionKind DefinitionKind);
 
 llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
                               const EntityLinkage &Linkage);
diff --git a/clang/include/clang/ScalableStaticAnalysis/Core/Model/PrivateFieldNames.def b/clang/include/clang/ScalableStaticAnalysis/Core/Model/PrivateFieldNames.def
index 33f47f8f9151c..1ffda11ed3634 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Core/Model/PrivateFieldNames.def
+++ b/clang/include/clang/ScalableStaticAnalysis/Core/Model/PrivateFieldNames.def
@@ -22,6 +22,10 @@ FIELD(BuildNamespace, Name)
 FIELD(EntityId, Index)
 FIELD(EntityIdTable, Entities)
 FIELD(EntityLinkage, Linkage)
+FIELD(EntityLinkage, Binding)
+FIELD(EntityLinkage, Coalescing)
+FIELD(EntityLinkage, Visibility)
+FIELD(EntityLinkage, DefinitionKind)
 FIELD(EntityName, Namespace)
 FIELD(EntityName, Suffix)
 FIELD(EntityName, USR)
diff --git a/clang/include/clang/ScalableStaticAnalysis/Core/TUSummary/TUSummaryBuilder.h b/clang/include/clang/ScalableStaticAnalysis/Core/TUSummary/TUSummaryBuilder.h
index 5132761ed6bb5..3f78754978056 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Core/TUSummary/TUSummaryBuilder.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Core/TUSummary/TUSummaryBuilder.h
@@ -26,7 +26,7 @@ class TUSummaryBuilder {
   TUSummaryBuilder(TUSummary &Summary, const SSAFOptions &Options)
       : Summary(Summary), Options(Options) {}
 
-  EntityId addEntity(const EntityName &EN, EntityLinkageType Linkage);
+  EntityId addEntity(const EntityName &EN, EntityLinkage Linkage);
 
   /// Associate the \p Data \c EntitySummary with the \p Entity.
   /// This consumes the \p Data only if \p Entity wasn't associated yet with the
diff --git a/clang/lib/ScalableStaticAnalysis/Core/CMakeLists.txt b/clang/lib/ScalableStaticAnalysis/Core/CMakeLists.txt
index 28c4ac9f6911b..3143cc230db8a 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysis/Core/CMakeLists.txt
@@ -6,6 +6,7 @@ set(LLVM_LINK_COMPONENTS
 add_clang_library(clangScalableStaticAnalysisCore
   ASTEntityMapping.cpp
   EntityLinker/EntityLinker.cpp
+  EntityLinker/LinkageRules.cpp
   Model/BuildNamespace.cpp
   Model/EntityId.cpp
   Model/EntityIdTable.cpp
diff --git a/clang/lib/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.cpp b/clang/lib/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.cpp
index 462978932a53d..60ccacb4a9875 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.cpp
@@ -13,7 +13,13 @@
 #include "clang/ScalableStaticAnalysis/Core/Model/EntityName.h"
 #include "clang/ScalableStaticAnalysis/Core/Support/ErrorBuilder.h"
 #include "clang/ScalableStaticAnalysis/Core/Support/FormatProviders.h"
+#include "llvm/ADT/StringExtras.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/WithColor.h"
+#include <algorithm>
 #include <cassert>
+#include <string>
+#include <utility>
 
 using namespace clang::ssaf;
 
@@ -38,14 +44,91 @@ static constexpr const char *DuplicateEntityIdInTUSummary =
 static constexpr const char *EntityNotFoundInResolutionTable =
     "{0} - {1} not found in EntityResolutionTable";
 
+static constexpr const char *MultipleEntitiesResolveToSameId =
+    "{0} - Multiple entities in the TU summary resolve to {1}";
+
 static constexpr const char *FailedToInsertEntityIntoOutputSummary =
     "{0} - Failed to insert data for {1} with {2} against {3} to LUSummary";
 
 static constexpr const char *DuplicateTUNamespace =
     "failed to link TU summary: duplicate {0}";
 
+static constexpr const char *MultipleDefinition =
+    "multiple definition of {0} in {1}";
+
+static constexpr const char *TargetTripleMismatch =
+    "failed to link TU summary: {0} targets '{1}', which resolves symbols as "
+    "{2}, but the link unit targets '{3}', which resolves them as {4}";
+
+static constexpr const char *UnrepresentableLinkage =
+    "{0} has {1}, which a {2} target cannot represent";
+
+static constexpr const char *OrderDependentMerge =
+    "visibility of common symbol {0} differs between translation units; the "
+    "linked result depends on link order";
+
+static constexpr const char *UnresolvedEntity = "undefined symbol: {0}";
+
+static constexpr const char *ODRMismatch =
+    "{0} summary data for {1} differs between {2} and an earlier translation "
+    "unit, but its definitions are required to be identical";
+
 } // namespace ErrorMessages
 
+//===----------------------------------------------------------------------===//
+// Fatal Error Helpers
+//===----------------------------------------------------------------------===//
+
+namespace {
+
+/// Reports a corrupted TU summary or logic bug and terminates execution.
+///
+/// Supplies \c EntityLinkerFatalErrorPrefix as the leading `{0}` of every
+/// message in \c ErrorMessages, so callers pass only the remaining arguments.
+template <typename... Args>
+[[noreturn]] void fatal(const char *Fmt, Args &&...ArgVals) {
+  ErrorBuilder::fatal(Fmt, ErrorMessages::EntityLinkerFatalErrorPrefix,
+                      std::forward<Args>(ArgVals)...);
+}
+
+/// Looks \p Key up in \p Map, reporting a fatal error if it is absent.
+///
+/// \returns A reference to the mapped value, which is owned by \p Map.
+template <typename MapT, typename... Args>
+const typename MapT::mapped_type &
+lookupOrFatal(const MapT &Map, const typename MapT::key_type &Key,
+              const char *Fmt, Args &&...ArgVals) {
+  auto Iter = Map.find(Key);
+  if (Iter == Map.end()) {
+    fatal(Fmt, std::forward<Args>(ArgVals)...);
+  }
+  return Iter->second;
+}
+
+/// Inserts \p Key into \p Map, reporting a fatal error if it is already
+/// present. Used for the write-once records built while resolving a TU.
+template <typename MapT, typename... Args>
+void insertOrFatal(MapT &Map, const typename MapT::key_type &Key,
+                   typename MapT::mapped_type Value, const char *Fmt,
+                   Args &&...ArgVals) {
+  auto [Iter, Inserted] = Map.insert({Key, std::move(Value)});
+  if (!Inserted) {
+    fatal(Fmt, std::forward<Args>(ArgVals)...);
+  }
+}
+
+/// Returns true if \p Linkage makes the entity visible across translation
+/// units, and so able to legitimately collide with an occurrence in another TU.
+bool isExternal(const EntityLinkage &Linkage) {
+  return Linkage.getLinkage() == EntityLinkageType::External;
+}
+
+} // namespace
+
+//===----------------------------------------------------------------------===//
+// Namespace Resolution
+//===----------------------------------------------------------------------===//
+
 static NestedBuildNamespace
 resolveNamespace(const NestedBuildNamespace &LUNamespace,
                  const NestedBuildNamespace &TUNamespace,
@@ -65,133 +148,487 @@ resolveNamespace(const NestedBuildNamespace &LUNamespace,
   llvm_unreachable("Unhandled EntityLinkageType variant");
 }
 
-EntityId EntityLinker::resolveEntity(const EntityName &OldName,
-                                     const EntityLinkage &Linkage,
-                                     const NestedBuildNamespace &TUNamespace) {
-  NestedBuildNamespace NewNamespace = resolveNamespace(
-      Output.LUNamespace, TUNamespace, OldName.Namespace, Linkage.getLinkage());
+//===----------------------------------------------------------------------===//
+// Linkage Reconciliation
+//===----------------------------------------------------------------------===//
+
+EntityBinding
+EntityLinker::effectiveBinding(const EntityLinkage &Linkage) const {
+  return Rules->effectiveBinding(Linkage);
+}
+
+bool EntityLinker::isConflictingDefinition(
+    const EntityLinkage &Current, const EntityLinkage &Incoming) const {
+  // Only two definitions can conflict; what makes them incompatible is the
+  // target's business.
+  if (Current.DefinitionKind != EntityDefinitionKind::Definition ||
+      Incoming.DefinitionKind != EntityDefinitionKind::Definition) {
+    return false;
+  }
+  return Rules->isConflictingDefinition(Current, Incoming);
+}
+
+bool EntityLinker::incomingDataWins(const EntityLinkage &Current,
+                                    const EntityLinkage &Incoming) const {
+  const bool CurrentDefines =
+      Current.DefinitionKind == EntityDefinitionKind::Definition;
+  const bool IncomingDefines =
+      Incoming.DefinitionKind == EntityDefinitionKind::Definition;
+
+  // A definition beats a declaration.
+  if (CurrentDefines != IncomingDefines) {
+    return IncomingDefines;
+  }
+
+  // Among definitions the stronger binding wins. Two declarations carry no
+  // data worth preferring, and equally strong definitions keep the data
+  // already linked.
+  return IncomingDefines && Rules->strengthRank(effectiveBinding(Incoming)) >
+                                Rules->strengthRank(effectiveBinding(Current));
+}
+
+EntityLinkage EntityLinker::mergeLinkage(const EntityLinkage &Current,
+                                         const EntityLinkage &Incoming) const {
+  // Entities collide only when their names, namespaces included, are identical,
+  // and namespace resolution qualifies `None` and `Internal` entities with
+  // their TU namespace. Occurrences of different linkage therefore never
+  // reconcile, and the linkage type carries through unchanged rather than
+  // joining.
+  assert(Current.Linkage == Incoming.Linkage &&
+         "EntityLinker::mergeLinkage: only same-linkage occurrences of an "
+         "entity can be reconciled");
+
+  const bool CurrentDefines =
+      Current.DefinitionKind == EntityDefinitionKind::Definition;
+  const bool IncomingDefines =
+      Incoming.DefinitionKind == EntityDefinitionKind::Definition;
+
+  // The binding is the stronger of the two, and the coalescing guarantee holds
+  // only if every copy carries it. Both describe definitions, so a declaration
+  // contributes neither when the other occurrence defines the entity: on
+  // Mach-O, where a weak binding outranks a common one, joining a weak
+  // declaration with a common definition would otherwise describe the common
+  // definition as weak.
+  EntityBinding MergedBinding = Current.Binding;
+  EntityCoalescing MergedCoalescing = Current.Coalescing;
+  if (CurrentDefines == IncomingDefines) {
+    const bool IncomingStronger =
+        Rules->strengthRank(effectiveBinding(Incoming)) >
+        Rules->strengthRank(effectiveBinding(Current));
+    const EntityLinkage &Winner = IncomingStronger ? Incoming : Current;
+
+    MergedCoalescing =
+        (CurrentDefines && Current.Coalescing == EntityCoalescing::ODR &&
+         Incoming.Coalescing == EntityCoalescing::ODR)
+            ? EntityCoalescing::ODR
+            : EntityCoalescing::None;
+
+    // A strong binding that only behaved weakly because of its ODR guarantee
+    // must degrade along with it. Recording Strong once the guarantee is gone
+    // would claim a strong definition the program does not have, and a later
+    // genuinely strong definition would then be reported as a duplicate.
+    // Doing this also makes the merge commutative when an ODR definition meets
+    // an explicitly weak one, since the two tie on effective binding.
+    MergedBinding = MergedCoalescing == Winner.Coalescing
+                        ? Winner.Binding
+                        : effectiveBinding(Winner);
+  } else if (IncomingDefines) {
+    MergedBinding = Incoming.Binding;
+    MergedCoalescing = Incoming.Coalescing;
+  }
+
+  return EntityLinkage(Current.Linkage, MergedBinding, MergedCoalescing,
+                       Rules->mergeVisibility(Current, Incoming),
+                       (CurrentDefines || IncomingDefines)
+                           ? EntityDefinitionKind::Definition
+                           : EntityDefinitionKind::Declaration);
+}
+
+void EntityLinker::reportIfLinkageIsNotExternal(EntityId NewId,
+                                                const EntityLinkage &Linkage) {
+  if (!isExternal(Linkage)) {
+    fatal(ErrorMessages::EntityAlreadyExistsInLinkageTable, NewId, Linkage);
+  }
+}
+
+bool EntityLinker::reportIfDefinitionsConflict(
+    const EntityLinkage &Current, const EntityLinkage &Incoming,
+    const EntityName &Name, const NestedBuildNamespace &TUNamespace) const {
+  if (!isConflictingDefinition(Current, Incoming)) {
+    return false;
+  }
+
+  if (!WarnOnMultipleDefinitions) {
+    ErrorBuilder::fatal(ErrorMessages::MultipleDefinition, Name, TUNamespace);
+  }
+
+  llvm::WithColor::warning()
+      << llvm::formatv(ErrorMessages::MultipleDefinition, Name, TUNamespace)
+      << "\n";
+
+  return true;
+}
+
+void EntityLinker::reportIfNotRepresentable(const EntityLinkage &Linkage,
+                                            const EntityName &Name) const {
+  if (!Rules->isRepresentable(Linkage)) {
+    ErrorBuilder::fatal(ErrorMessages::UnrepresentableLinkage, Name, Linkage,
+                        Rules->getName());
+  }
+}
+
+void EntityLinker::reportIfOrderDependent(const EntityLinkage &Current,
+                                          const EntityLinkage &Incoming,
+                                          const EntityName &Name) const {
+  if (Rules->isOrderDependentMerge(Current, Incoming)) {
+    llvm::WithColor::warning()
+        << llvm::formatv(ErrorMessages::OrderDependentMerge, Name) << "\n";
+  }
+}
+
+//===----------------------------------------------------------------------===//
+// Resolve
+//===----------------------------------------------------------------------===//
+
+std::pair<EntityId, bool>
+EntityLinker::resolveEntity(const EntityName &OldName,
+                            const EntityLinkage &RawLinkage,
+                            const NestedBuildNamespace &TUNamespace) {
+  NestedBuildNamespace NewNamespace =
+      resolveNamespace(Output.LUNamespace, TUNamespace, OldName.Namespace,
+                       RawLinkage.getLinkage());
 
   EntityName NewName(OldName.USR, OldName.Suffix, NewNamespace);
 
+  // The summary records what the source declared; coerce it to what this
+  // target can express before any rule looks at it, and reject values the
+  // target's toolchain could never have produced.
+  reportIfNotRepresentable(RawLinkage, NewName);
+  const EntityLinkage Linkage = Rules->normalize(RawLinkage);
+
   // NewId construction will always return a fresh id for `None` and `Internal`
   // linkage entities since their namespaces will be different even if their
   // names clash. For `External` linkage entities with identical names this
   // function will return the id assigned at the first insertion.
   EntityId NewId = Output.IdTable.getId(NewName);
 
-  auto [_, Inserted] = Output.LinkageTable.try_emplace(NewId, Linkage);
-  if (!Inserted) {
-    // Insertion failure for `None` and `Internal` linkage is a fatal error
-    // because these entities have unique namespaces and should never collide.
-    // `External` linkage entities may collide.
-    if (Linkage.getLinkage() == EntityLinkageType::None ||
-        Linkage.getLinkage() == EntityLinkageType::Internal) {
-      ErrorBuilder::fatal(ErrorMessages::EntityAlreadyExistsInLinkageTable,
-                          ErrorMessages::EntityLinkerFatalErrorPrefix, NewId,
-                          Linkage);
-    }
+  auto [It, Inserted] = Output.LinkageTable.try_emplace(NewId, Linkage);
+  if (Inserted) {
+    // Keep the name so finalization can name the entity in diagnostics.
+    EntityNames.insert({NewId, NewName});
+    // First occurrence of this entity: its summary data is the copy to keep.
+    return {NewId, true};
+  }
+
+  reportIfLinkageIsNotExternal(NewId, Linkage);
+
+  // `External` entities collide legitimately. A conflicting duplicate
+  // definition is ignored outright, leaving the definition already linked in
+  // place; anything else is reconciled, and the data selection tells merge()
+  // whose encoding to keep.
+  if (reportIfDefinitionsConflict(It->second, Linkage, NewName, TUNamespace)) {
+    return {NewId, false};
   }
 
-  return NewId;
+  reportIfOrderDependent(It->second, Linkage, NewName);
+
+  const bool DataFromIncoming = incomingDataWins(It->second, Linkage);
+  It->second = mergeLinkage(It->second, Linkage);
+
+  return {NewId, DataFromIncoming};
 }
 
-std::map<EntityId, EntityId>
+std::pair<EntityResolutionMap, EntityLinker::DataSelectionMap>
 EntityLinker::resolve(const TUSummaryEncoding &Summary) {
-  std::map<EntityId, EntityId> EntityResolutionTable;
+  EntityResolutionMap Resolution;
+  DataSelectionMap DataSelection;
 
   Summary.IdTable.forEach([&](const EntityName &OldName, const EntityId OldId) {
-    auto Iter = Summary.LinkageTable.find(OldId);
-    if (Iter == Summary.LinkageTable.end()) {
-      ErrorBuilder::fatal(ErrorMessages::MissingLinkageInformation,
-                          ErrorMessages::EntityLinkerFatalErrorPrefix, OldId);
-    }
+    const EntityLinkage &Linkage =
+        lookupOrFatal(Summary.LinkageTable, OldId,
+                      ErrorMessages::MissingLinkageInformation, OldId);
+
+    auto [NewId, DataFromIncoming] = resolveEntity(
+        OldName, Linkage, NestedBuildNamespace(Summary.TUNamespace));
+
+    insertOrFatal(Resolution, OldId, NewId,
+                  ErrorMessages::DuplicateEntityIdInTUSummary, OldId);
+    insertOrFatal(DataSelection, NewId, DataFromIncoming,
+                  ErrorMessages::MultipleEntitiesResolveToSameId, NewId);
+  });
+
+  return {std::move(Resolution), std::move(DataSelection)};
+}
+
+//===----------------------------------------------------------------------===//
+// Merge
+//===----------------------------------------------------------------------===//
+
+EntitySummaryEncoding *EntityLinker::mergeEntityData(
+    EntityDataMap &OutputData, const SummaryName &SN, EntityId NewId,
+    std::unique_ptr<EntitySummaryEncoding> &Encoding,
+    const EntityLinkage &Linkage, bool DataFromIncoming) {
+  auto [It, Inserted] = OutputData.try_emplace(NewId, std::move(Encoding));
 
-    const EntityLinkage &Linkage = Iter->second;
+  // No earlier TU contributed data for this entity, so this encoding is it.
+  if (Inserted) {
+    return It->second.get();
+  }
 
-    EntityId NewId = resolveEntity(OldName, Linkage,
-                                   NestedBuildNamespace(Summary.TUNamespace));
+  // Insertion should never fail for `None` and `Internal` linkage entities
+  // because these entities will have different namespaces across TUs even if
+  // their names match.
+  if (!isExternal(Linkage)) {
+    fatal(ErrorMessages::FailedToInsertEntityIntoOutputSummary, NewId, Linkage,
+          SN);
+  }
 
-    auto [_, Inserted] = EntityResolutionTable.insert({OldId, NewId});
-    if (!Inserted) {
-      ErrorBuilder::fatal(ErrorMessages::DuplicateEntityIdInTUSummary,
-                          ErrorMessages::EntityLinkerFatalErrorPrefix, OldId);
+  // Two definitions required to be identical should carry identical summaries.
+  // Keep the loser so reportODRMismatches() can compare them once both are in
+  // the LU EntityId space; a displaced encoding from this TU still needs
+  // patching, so it is returned as a patch target either way.
+  const bool CompareDisplaced = ODRMismatch != ODRMismatchPolicy::Ignore &&
+                                Linkage.Coalescing == EntityCoalescing::ODR;
+
+  // `External` collision: the reconciliation in resolve() decided which
+  // occurrence's data is the copy to keep. If this TU won, its encoding
+  // replaces the incumbent; otherwise the incumbent is kept and this encoding
+  // is dropped.
+  if (!DataFromIncoming) {
+    if (!CompareDisplaced) {
+      return nullptr;
     }
-  });
+    Displaced.push_back({SN, NewId, std::move(Encoding)});
+    return Displaced.back().Data.get();
+  }
 
-  return EntityResolutionTable;
+  if (CompareDisplaced) {
+    // The incumbent is already patched, so it needs no further work.
+    Displaced.push_back({SN, NewId, std::move(It->second)});
+  }
+  It->second = std::move(Encoding);
+  return It->second.get();
+}
+
+std::vector<EntitySummaryEncoding *>
+EntityLinker::mergeSummaryData(const SummaryName &SN, EntityDataMap &DataMap,
+                               const TUSummaryEncoding &Summary,
+                               const EntityResolutionMap &Resolution,
+                               const DataSelectionMap &DataSelection) {
+  std::vector<EntitySummaryEncoding *> PatchTargets;
+  auto &OutputSummaryData = Output.Data[SN];
+
+  for (auto &[OldId, ES] : DataMap) {
+    const EntityId NewId =
+        lookupOrFatal(Resolution, OldId,
+                      ErrorMessages::EntityNotFoundInResolutionTable, OldId);
+
+    // Safe to retrieve linkage and data selection using .at since the resolve
+    // step records both for every OldId in the TU summary.
+    assert(Summary.LinkageTable.count(OldId) &&
+           "EntityLinker::mergeSummaryData: resolve() records a linkage for "
+           "every entity");
+    assert(DataSelection.count(NewId) &&
+           "EntityLinker::mergeSummaryData: resolve() records a data selection "
+           "for every resolved entity");
+
+    if (auto *PatchTarget = mergeEntityData(OutputSummaryData, SN, NewId, ES,
+                                            Summary.LinkageTable.at(OldId),
+                                            DataSelection.at(NewId))) {
+      PatchTargets.push_back(PatchTarget);
+    }
+  }
+
+  return PatchTargets;
 }
 
 std::vector<EntitySummaryEncoding *>
 EntityLinker::merge(TUSummaryEncoding &Summary,
-                    const std::map<EntityId, EntityId> &EntityResolutionTable) {
+                    const EntityResolutionMap &Resolution,
+                    const DataSelectionMap &DataSelection) {
   std::vector<EntitySummaryEncoding *> PatchTargets;
 
   for (auto &[SN, DataMap] : Summary.Data) {
-    auto &OutputSummaryData = Output.Data[SN];
-
-    for (auto &[OldId, ES] : DataMap) {
-      auto Iter = EntityResolutionTable.find(OldId);
-      if (Iter == EntityResolutionTable.end()) {
-        ErrorBuilder::fatal(ErrorMessages::EntityNotFoundInResolutionTable,
-                            ErrorMessages::EntityLinkerFatalErrorPrefix, OldId);
-      }
-
-      const auto NewId = Iter->second;
-
-      auto [It, Inserted] = OutputSummaryData.try_emplace(NewId, std::move(ES));
-
-      if (Inserted) {
-        PatchTargets.push_back(It->second.get());
-      } else {
-        // Safe to retrieve linkage using .at since the resolve step ensures
-        // linkage information is always present for every OldId.
-        auto Linkage = Summary.LinkageTable.at(OldId);
-
-        // Insertion should never fail for `None` and `Internal` linkage
-        // entities because these entities will have different namespaces across
-        // TUs even if their names match.
-        if (Linkage.getLinkage() == EntityLinkageType::None ||
-            Linkage.getLinkage() == EntityLinkageType::Internal) {
-          ErrorBuilder::fatal(
-              ErrorMessages::FailedToInsertEntityIntoOutputSummary,
-              ErrorMessages::EntityLinkerFatalErrorPrefix, NewId, Linkage, SN);
-        }
-
-        // TODO: Insertion is expected to fail for duplicate occurrences of
-        // `External` linkage entities. Report these cases in a "debug" mode to
-        // help debug potential ODR violations.
-      }
-    }
+    std::vector<EntitySummaryEncoding *> SummaryTargets =
+        mergeSummaryData(SN, DataMap, Summary, Resolution, DataSelection);
+    PatchTargets.insert(PatchTargets.end(), SummaryTargets.begin(),
+                        SummaryTargets.end());
   }
 
   return PatchTargets;
 }
 
+//===----------------------------------------------------------------------===//
+// Patch
+//===----------------------------------------------------------------------===//
+
 llvm::Error
 EntityLinker::patch(const std::vector<EntitySummaryEncoding *> &PatchTargets,
-                    const std::map<EntityId, EntityId> &EntityResolutionTable) {
+                    const EntityResolutionMap &Resolution) {
   for (auto *PatchTarget : PatchTargets) {
     assert(PatchTarget && "EntityLinker::patch: Patch target cannot be null");
 
-    if (auto Err = PatchTarget->patch(EntityResolutionTable)) {
+    if (auto Err = PatchTarget->patch(Resolution)) {
       return Err;
     }
   }
   return llvm::Error::success();
 }
 
-llvm::Error EntityLinker::link(std::unique_ptr<TUSummaryEncoding> Summary) {
-  auto [_, Inserted] = ProcessedTUNamespaces.insert(Summary->TUNamespace);
+//===----------------------------------------------------------------------===//
+// Link
+//===----------------------------------------------------------------------===//
+
+llvm::Error
+EntityLinker::checkTUNotAlreadyLinked(const BuildNamespace &TUNamespace) {
+  auto [_, Inserted] = ProcessedTUNamespaces.insert(TUNamespace);
   if (!Inserted) {
     return ErrorBuilder::create(std::errc::invalid_argument,
                                 ErrorMessages::DuplicateTUNamespace,
-                                Summary->TUNamespace)
+                                TUNamespace)
         .build();
   }
+  return llvm::Error::success();
+}
+
+llvm::Error
+EntityLinker::checkTUTargetMatches(const llvm::Triple &TUTriple,
+                                   const BuildNamespace &TUNamespace) const {
+  // Compare only what selects resolution rules and affects the outcome. The OS
+  // version does not, so macosx14.0 and macosx15.0 are interchangeable here,
+  // matching the key MultiArch* uses to order its members.
+  const llvm::Triple &LUTriple = Output.TargetTriple;
+  const bool Matches = TUTriple.getArch() == LUTriple.getArch() &&
+                       TUTriple.getSubArch() == LUTriple.getSubArch() &&
+                       TUTriple.getVendor() == LUTriple.getVendor() &&
+                       TUTriple.getOS() == LUTriple.getOS() &&
+                       TUTriple.getEnvironment() == LUTriple.getEnvironment() &&
+                       TUTriple.getObjectFormat() == LUTriple.getObjectFormat();
+  if (Matches) {
+    return llvm::Error::success();
+  }
+
+  return ErrorBuilder::create(std::errc::invalid_argument,
+                              ErrorMessages::TargetTripleMismatch, TUNamespace,
+                              TUTriple.str(),
+                              LinkageRules::forTarget(TUTriple).getName(),
+                              LUTriple.str(), Rules->getName())
+      .build();
+}
+
+llvm::Error EntityLinker::link(std::unique_ptr<TUSummaryEncoding> Summary) {
+  if (auto Err = checkTUNotAlreadyLinked(Summary->TUNamespace)) {
+    return Err;
+  }
+
+  if (auto Err = checkTUTargetMatches(Summary->getTargetTriple(),
+                                      Summary->TUNamespace)) {
+    return Err;
+  }
 
   TUSummaryEncoding &SummaryRef = *Summary;
+  Displaced.clear();
+
+  auto [Resolution, DataSelection] = resolve(SummaryRef);
+  auto PatchTargets = merge(SummaryRef, Resolution, DataSelection);
+  if (auto Err = patch(PatchTargets, Resolution)) {
+    return Err;
+  }
+
+  reportODRMismatches(NestedBuildNamespace(SummaryRef.TUNamespace));
+  return llvm::Error::success();
+}
+
+void EntityLinker::reportODRMismatches(
+    const NestedBuildNamespace &TUNamespace) {
+  std::vector<std::string> Messages;
+  for (const DisplacedData &D : Displaced) {
+    const auto &Kept = Output.Data.at(D.Name).at(D.Id);
+    if (Kept->equals(*D.Data)) {
+      continue;
+    }
+    auto NameIt = EntityNames.find(D.Id);
+    assert(NameIt != EntityNames.end() &&
+           "EntityLinker: every resolved entity records its name");
+    Messages.push_back(llvm::formatv(ErrorMessages::ODRMismatch, D.Name,
+                                     NameIt->second, TUNamespace)
+                           .str());
+  }
+  Displaced.clear();
+
+  if (Messages.empty()) {
+    return;
+  }
+
+  if (ODRMismatch == ODRMismatchPolicy::Warn) {
+    for (const std::string &Message : Messages) {
+      llvm::WithColor::warning() << Message << "\n";
+    }
+    return;
+  }
+
+  ErrorBuilder::fatal("{0}", llvm::join(Messages, "\n"));
+}
+
+//===----------------------------------------------------------------------===//
+// Finalization
+//===----------------------------------------------------------------------===//
+
+void EntityLinker::reportUnresolvedEntities() const {
+  if (UnresolvedSymbols == UnresolvedPolicy::Ignore) {
+    return;
+  }
+
+  std::vector<std::string> Messages;
+  for (const auto &[Id, Linkage] : Output.LinkageTable) {
+    // Only an external entity can be satisfied by another translation unit.
+    if (Linkage.Linkage != EntityLinkageType::External ||
+        Linkage.DefinitionKind == EntityDefinitionKind::Definition) {
+      continue;
+    }
+    // An undefined weak reference resolves to nothing by design.
+    if (Rules->effectiveBinding(Linkage) == EntityBinding::Weak) {
+      continue;
+    }
+    auto NameIt = EntityNames.find(Id);
+    assert(NameIt != EntityNames.end() &&
+           "EntityLinker: every resolved entity records its name");
+    Messages.push_back(
+        llvm::formatv(ErrorMessages::UnresolvedEntity, NameIt->second).str());
+  }
+
+  if (Messages.empty()) {
+    return;
+  }
+
+  if (UnresolvedSymbols == UnresolvedPolicy::Warn) {
+    for (const std::string &Message : Messages) {
+      llvm::WithColor::warning() << Message << "\n";
+    }
+    return;
+  }
+
+  ErrorBuilder::fatal("{0}", llvm::join(Messages, "\n"));
+}
+
+void EntityLinker::demoteHiddenEntities() {
+  for (auto &[Id, Linkage] : Output.LinkageTable) {
+    if (Linkage.Linkage != EntityLinkageType::External) {
+      continue;
+    }
+    // "Hidden" is whatever the target treats as most restrictive; on COFF,
+    // where visibility is meaningless, normalize() has already flattened
+    // everything to Default so nothing is demoted.
+    if (Linkage.Visibility != EntityVisibility::Hidden) {
+      continue;
+    }
+    Linkage = EntityLinkage(EntityLinkageType::Internal, Linkage.Binding,
+                            Linkage.Coalescing, Linkage.Visibility,
+                            Linkage.DefinitionKind);
+  }
+}
 
-  auto EntityResolutionTable = resolve(SummaryRef);
-  auto PatchTargets = merge(SummaryRef, EntityResolutionTable);
-  return patch(PatchTargets, EntityResolutionTable);
+void EntityLinker::finalize() {
+  reportUnresolvedEntities();
+  demoteHiddenEntities();
 }
diff --git a/clang/lib/ScalableStaticAnalysis/Core/EntityLinker/LinkageRules.cpp b/clang/lib/ScalableStaticAnalysis/Core/EntityLinker/LinkageRules.cpp
new file mode 100644
index 0000000000000..c71904d7266c3
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysis/Core/EntityLinker/LinkageRules.cpp
@@ -0,0 +1,450 @@
+//===- LinkageRules.cpp ---------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Each rule below cites the probe that establishes it. The probes, including
+// the full programs and linker output, are in
+// docs/ssaf-linker-{elf,macho,coff}-behavior.md.
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/ScalableStaticAnalysis/Core/EntityLinker/LinkageRules.h"
+#include "clang/ScalableStaticAnalysis/Core/Support/ErrorBuilder.h"
+#include "llvm/Support/ErrorHandling.h"
+
+namespace clang::ssaf {
+
+namespace {
+
+constexpr const char *UnsupportedObjectFormat =
+    "unsupported object format for target triple '{0}': the entity linker "
+    "emulates ELF, Mach-O and COFF";
+
+} // namespace
+
+EntityLinkageType LinkageRules::linkageOf(const EntityLinkage &L) {
+  return L.Linkage;
+}
+
+EntityBinding LinkageRules::bindingOf(const EntityLinkage &L) {
+  return L.Binding;
+}
+
+EntityCoalescing LinkageRules::coalescingOf(const EntityLinkage &L) {
+  return L.Coalescing;
+}
+
+EntityVisibility LinkageRules::visibilityOf(const EntityLinkage &L) {
+  return L.Visibility;
+}
+
+bool LinkageRules::defines(const EntityLinkage &Linkage) {
+  return Linkage.DefinitionKind == EntityDefinitionKind::Definition;
+}
+
+EntityLinkage LinkageRules::withVisibility(const EntityLinkage &Linkage,
+                                           EntityVisibility V) {
+  return EntityLinkage(Linkage.Linkage, Linkage.Binding, Linkage.Coalescing, V,
+                       Linkage.DefinitionKind);
+}
+
+EntityVisibility
+LinkageRules::mergeVisibility(const EntityLinkage &Current,
+                              const EntityLinkage &Incoming) const {
+  return visibilityRank(visibilityOf(Incoming)) >
+                 visibilityRank(visibilityOf(Current))
+             ? visibilityOf(Incoming)
+             : visibilityOf(Current);
+}
+
+bool LinkageRules::isOrderDependentMerge(const EntityLinkage &,
+                                         const EntityLinkage &) const {
+  return false;
+}
+
+EntityLinkage LinkageRules::normalize(const EntityLinkage &Linkage) const {
+  return Linkage;
+}
+
+bool LinkageRules::isRepresentable(const EntityLinkage &) const { return true; }
+
+//===----------------------------------------------------------------------===//
+// ELF
+//===----------------------------------------------------------------------===//
+
+namespace {
+
+class ELFLinkageRules : public LinkageRules {
+public:
+  llvm::StringRef getName() const override { return "ELF"; }
+
+  /// ELF has no general coalescing mechanism, so clang lowers an ODR
+  /// definition to a weak symbol plus a COMDAT group:
+  ///
+  ///   inline int inl(void) { return 42; }
+  ///   $ llvm-nm inl_a.o  ->  W _Z3inlv
+  ///   $ llvm-readelf --section-groups inl_a.o
+  ///     COMDAT group section [4] `.group' [_Z3inlv]
+  ///
+  /// The weak binding is what lets two copies coexist, and what makes an
+  /// ordinary definition of the same symbol win rather than conflict.
+  /// (ELF doc §3, §8.)
+  EntityBinding effectiveBinding(const EntityLinkage &Linkage) const override {
+    if (bindingOf(Linkage) == EntityBinding::Strong &&
+        coalescingOf(Linkage) == EntityCoalescing::ODR) {
+      return EntityBinding::Weak;
+    }
+    return bindingOf(Linkage);
+  }
+
+  /// Undefined < Weak < Common < Strong.
+  ///
+  ///   int g;                             // common, 4 bytes
+  ///   __attribute__((weak)) int g = 9;   // weak definition
+  ///   $ ld.lld -o out main.o weakdef_g.o common_big.o
+  ///     g -> the common (size 32 survives)   => Common > Weak
+  ///
+  ///   int g = 7;                         // strong definition
+  ///   $ ld.lld -o out main.o common_big.o def_g.o
+  ///     g -> def_g.o (size 4)                => Strong > Common
+  ///
+  /// (ELF doc §2, probes C2b and C3.)
+  unsigned strengthRank(EntityBinding B) const override {
+    switch (B) {
+    case EntityBinding::Undefined:
+      return 0;
+    case EntityBinding::Weak:
+      return 1;
+    case EntityBinding::Common:
+      return 2;
+    case EntityBinding::Strong:
+      return 3;
+    }
+    llvm_unreachable("Unhandled EntityBinding variant");
+  }
+
+  /// Most restrictive wins: Default < Protected < Hidden.
+  ///
+  ///   // tu1.c
+  ///   int v(void) { return 1; }                                 // default
+  ///   // tu2.c
+  ///   __attribute__((visibility("hidden"))) int v(void);        // hidden decl
+  ///   $ ld.lld -shared -o out.so tu1.o tu2.o
+  ///     v: FUNC LOCAL HIDDEN, absent from .dynsym
+  ///
+  /// Note a hidden *declaration* demotes a default-visibility definition, and
+  /// that hidden beats protected in either order. (ELF doc §4 V1, §6.2.)
+  unsigned visibilityRank(EntityVisibility V) const override {
+    switch (V) {
+    case EntityVisibility::Default:
+      return 0;
+    case EntityVisibility::Protected:
+      return 1;
+    case EntityVisibility::Hidden:
+      return 2;
+    }
+    llvm_unreachable("Unhandled EntityVisibility variant");
+  }
+
+  /// Two non-weak definitions collide; anything involving a weak one does not.
+  ///
+  ///   int f(void) { return 1; }   // strong_a.c
+  ///   int f(void) { return 2; }   // strong_b.c
+  ///   $ ld.lld -o out main.o strong_a.o strong_b.o
+  ///     ld.lld: error: duplicate symbol: f
+  ///
+  /// Replacing either with __attribute__((weak)) links cleanly, as does an
+  /// inline definition, which lowers to weak. Commons never conflict.
+  /// (ELF doc §1, probes P1-P4; §2 C1.)
+  bool isConflictingDefinition(const EntityLinkage &Current,
+                               const EntityLinkage &Incoming) const override {
+    return effectiveBinding(Current) != EntityBinding::Weak &&
+           effectiveBinding(Incoming) != EntityBinding::Weak &&
+           effectiveBinding(Current) != EntityBinding::Common &&
+           effectiveBinding(Incoming) != EntityBinding::Common;
+  }
+};
+
+//===----------------------------------------------------------------------===//
+// Mach-O
+//===----------------------------------------------------------------------===//
+
+class MachOLinkageRules : public LinkageRules {
+public:
+  llvm::StringRef getName() const override { return "Mach-O"; }
+
+  /// Like ELF, Mach-O expresses an ODR definition as a weak one — the symbol
+  /// table entry is byte-identical to __attribute__((weak)):
+  ///
+  ///   inline int q(void) { return 42; }
+  ///   $ llvm-nm -m inl.o    -> (__TEXT,__text) weak external __Z1qv
+  ///   $ llvm-nm -m weak.o   -> (__TEXT,__text) weak external __Z1qv
+  ///
+  /// and it resolves the same way: first-wins against another weak definition,
+  /// loses to a strong one. (Mach-O doc §4, §7.1.)
+  EntityBinding effectiveBinding(const EntityLinkage &Linkage) const override {
+    if (bindingOf(Linkage) == EntityBinding::Strong &&
+        coalescingOf(Linkage) == EntityCoalescing::ODR) {
+      return EntityBinding::Weak;
+    }
+    return bindingOf(Linkage);
+  }
+
+  /// Undefined < Common < Weak < Strong.
+  ///
+  /// Mach-O inverts Weak and Common relative to ELF and COFF: a weak
+  /// definition displaces a common one, and ld-prime says so out loud.
+  ///
+  ///   int g;                             // common, 32 bytes
+  ///   __attribute__((weak)) int g = 9;   // weak definition, 4 bytes
+  ///   $ ld -o out main.o weakdef_g.o common_big.o
+  ///     ld: warning: tentative definition of '_g' with size 32 ... is being
+  ///         replaced by real definition of smaller size 4
+  ///     _g -> (__DATA,__data) weak external
+  ///
+  /// (Mach-O doc §3, probe MC3.)
+  unsigned strengthRank(EntityBinding B) const override {
+    switch (B) {
+    case EntityBinding::Undefined:
+      return 0;
+    case EntityBinding::Common:
+      return 1;
+    case EntityBinding::Weak:
+      return 2;
+    case EntityBinding::Strong:
+      return 3;
+    }
+    llvm_unreachable("Unhandled EntityBinding variant");
+  }
+
+  /// LEAST restrictive wins — the opposite of ELF.
+  ///
+  ///   // tu1.c
+  ///   __attribute__((weak, visibility("hidden"))) int w(void) { return 1; }
+  ///   // tu2.c
+  ///   __attribute__((weak)) int w(void) { return 2; }
+  ///   $ ld -o out main.o tu1.o tu2.o     (either order)
+  ///     _w -> weak external               i.e. exported, not private
+  ///
+  /// Only when every copy is hidden does the merged symbol stay private, which
+  /// is `privateExtern &= isPrivateExtern` in lld/MachO/SymbolTable.cpp:118.
+  /// Mach-O has no protected visibility at all: clang warns and downgrades it
+  /// to default, so it is ranked alongside default here.
+  /// (Mach-O doc §2, probes W1-W3.)
+  unsigned visibilityRank(EntityVisibility V) const override {
+    switch (V) {
+    case EntityVisibility::Hidden:
+      return 0;
+    case EntityVisibility::Default:
+    case EntityVisibility::Protected:
+      return 1;
+    }
+    llvm_unreachable("Unhandled EntityVisibility variant");
+  }
+
+  /// Same predicate as ELF: two non-weak definitions collide.
+  ///
+  ///   int f(void) { return 1; }   // strong_a.c
+  ///   int f(void) { return 2; }   // strong_b.c
+  ///   $ ld -o out main.o strong_a.o strong_b.o
+  ///     duplicate symbol '_f' in: strong_b.o, strong_a.o
+  ///
+  /// (Mach-O doc §1, probes M1-M4.)
+  bool isConflictingDefinition(const EntityLinkage &Current,
+                               const EntityLinkage &Incoming) const override {
+    return effectiveBinding(Current) != EntityBinding::Weak &&
+           effectiveBinding(Incoming) != EntityBinding::Weak &&
+           effectiveBinding(Current) != EntityBinding::Common &&
+           effectiveBinding(Incoming) != EntityBinding::Common;
+  }
+
+  /// Common symbols do not merge their visibility: ld-prime keeps whichever
+  /// common it saw first, so the result depends on link order.
+  ///
+  ///   // tu1.c
+  ///   __attribute__((visibility("hidden"))) int g;
+  ///   // tu2.c
+  ///   int g;
+  ///   $ ld -o out main.o tu1.o tu2.o   -> _g non-external (private)
+  ///   $ ld -o out main.o tu2.o tu1.o   -> _g external
+  ///
+  /// addCommon() carries the isPrivateExtern of the common it keeps, with no
+  /// merge step; only addDefined()'s weak-def path performs the `&=`. We
+  /// reproduce that faithfully and warn, rather than approximating it
+  /// commutatively. (Mach-O doc §7.2.)
+  EntityVisibility
+  mergeVisibility(const EntityLinkage &Current,
+                  const EntityLinkage &Incoming) const override {
+    if (isCommonPair(Current, Incoming)) {
+      return visibilityOf(Current);
+    }
+    return LinkageRules::mergeVisibility(Current, Incoming);
+  }
+
+  bool isOrderDependentMerge(const EntityLinkage &Current,
+                             const EntityLinkage &Incoming) const override {
+    return isCommonPair(Current, Incoming) &&
+           visibilityOf(Current) != visibilityOf(Incoming);
+  }
+
+  /// Protected is not representable, but clang never emits it for a Mach-O
+  /// target: it warns and downgrades to default at compile time.
+  ///
+  ///   $ clang --target=arm64-apple-macosx -c v.c
+  ///     warning: target does not support 'protected' visibility;
+  ///              using 'default' [-Wunsupported-visibility]
+  ///
+  /// A summary that carries it therefore did not come from the compiler, which
+  /// is worth failing over rather than silently coercing. (Mach-O doc §2.)
+  bool isRepresentable(const EntityLinkage &Linkage) const override {
+    return visibilityOf(Linkage) != EntityVisibility::Protected;
+  }
+
+private:
+  static bool isCommonPair(const EntityLinkage &Current,
+                           const EntityLinkage &Incoming) {
+    return defines(Current) && defines(Incoming) &&
+           bindingOf(Current) == EntityBinding::Common &&
+           bindingOf(Incoming) == EntityBinding::Common;
+  }
+};
+
+//===----------------------------------------------------------------------===//
+// COFF
+//===----------------------------------------------------------------------===//
+
+class COFFLinkageRules : public LinkageRules {
+public:
+  llvm::StringRef getName() const override { return "COFF"; }
+
+  /// COFF is the one format that expresses ODR directly: the binding stays
+  /// strong and a COMDAT licenses the duplicates.
+  ///
+  ///   inline int inl(void) { return 42; }
+  ///   $ llvm-nm inl_a.obj      -> T ?inl@@YAHXZ        (strong, not weak)
+  ///   $ llvm-readobj --sections inl_a.obj
+  ///       IMAGE_SCN_LNK_COMDAT (0x1000)
+  ///       Selection: Any (0x2)
+  ///
+  /// So no lowering is needed. (COFF doc §2.)
+  EntityBinding effectiveBinding(const EntityLinkage &Linkage) const override {
+    return bindingOf(Linkage);
+  }
+
+  /// Undefined < Weak < Common < Strong, as on ELF.
+  ///
+  ///   $ lld-link ... /map:k2.map read.obj common_a.obj def_g.obj
+  ///       g -> def_g.obj          => Strong > Common
+  ///   $ lld-link ... /map:k3.map read.obj weakdef_g.obj common_big.obj
+  ///       g -> <common>           => Common > Weak
+  ///
+  /// (COFF doc §6.1.)
+  unsigned strengthRank(EntityBinding B) const override {
+    switch (B) {
+    case EntityBinding::Undefined:
+      return 0;
+    case EntityBinding::Weak:
+      return 1;
+    case EntityBinding::Common:
+      return 2;
+    case EntityBinding::Strong:
+      return 3;
+    }
+    llvm_unreachable("Unhandled EntityBinding variant");
+  }
+
+  /// COFF has no symbol visibility. Export is controlled per-DLL by
+  /// __declspec(dllexport) and .def files, which is not a per-symbol attribute
+  /// merged during resolution, so every value ranks equally here and
+  /// normalize() coerces them all to Default. (COFF doc §5, §7.)
+  unsigned visibilityRank(EntityVisibility) const override { return 0; }
+
+  /// Always Default, matching normalize(). Ranking every value equally would
+  /// otherwise make the merge keep whichever occurrence was linked first,
+  /// which is an order dependence on a field the platform does not have.
+  EntityVisibility mergeVisibility(const EntityLinkage &,
+                                   const EntityLinkage &) const override {
+    return EntityVisibility::Default;
+  }
+
+  /// COMDAT, not weakness, is what licenses duplicate definitions.
+  ///
+  ///   inline int inl(void) { return 42; }   // COMDAT, in two TUs
+  ///   $ lld-link ... main.obj inl_a.obj inl_b.obj        -> links
+  ///
+  ///   int inl(void) { return 99; }          // regular definition
+  ///   $ lld-link ... main.obj inl_comdat.obj inl_regular.obj
+  ///       lld-link: error: duplicate symbol: int __cdecl inl(void)
+  ///
+  ///   __attribute__((weak)) int f(void);    // in two TUs
+  ///   $ lld-link ... main.obj weak_a.obj weak_b.obj
+  ///       lld-link: error: duplicate symbol: .weak.f.default
+  ///
+  /// That last case is the one ELF and Mach-O accept: COFF emulates weak
+  /// symbols with an alias, so two of them collide on the alias while one weak
+  /// against one strong does not. (COFF doc §1 C1/C4, §2 CI1/K4, §6.)
+  bool isConflictingDefinition(const EntityLinkage &Current,
+                               const EntityLinkage &Incoming) const override {
+    // Commons merge rather than conflict.
+    if (bindingOf(Current) == EntityBinding::Common ||
+        bindingOf(Incoming) == EntityBinding::Common) {
+      return false;
+    }
+    // Two COMDATs coalesce.
+    if (coalescingOf(Current) == EntityCoalescing::ODR &&
+        coalescingOf(Incoming) == EntityCoalescing::ODR) {
+      return false;
+    }
+    // Exactly one weak definition yields to the other; two collide on the
+    // alias COFF uses to emulate them.
+    const bool CurrentWeak = bindingOf(Current) == EntityBinding::Weak;
+    const bool IncomingWeak = bindingOf(Incoming) == EntityBinding::Weak;
+    if (CurrentWeak != IncomingWeak) {
+      return false;
+    }
+    return true;
+  }
+
+  /// Visibility is dropped at emission, so a summary carrying Hidden or
+  /// Protected is coerced rather than rejected: clang accepts both silently on
+  /// Windows, and portable code applies them unconditionally.
+  ///
+  ///   __attribute__((visibility("hidden"))) int g;
+  ///   $ clang --target=x86_64-pc-windows-msvc -c g.c    (no diagnostic)
+  ///   $ llvm-nm g.obj  ->  00000004 C g                 (indistinguishable)
+  ///
+  /// (COFF doc §7.)
+  EntityLinkage normalize(const EntityLinkage &Linkage) const override {
+    if (visibilityOf(Linkage) == EntityVisibility::Default) {
+      return Linkage;
+    }
+    return withVisibility(Linkage, EntityVisibility::Default);
+  }
+};
+
+} // namespace
+
+const LinkageRules &LinkageRules::forTarget(const llvm::Triple &TargetTriple) {
+  static const ELFLinkageRules ELF;
+  static const MachOLinkageRules MachO;
+  static const COFFLinkageRules COFF;
+
+  switch (TargetTriple.getObjectFormat()) {
+  case llvm::Triple::ELF:
+    return ELF;
+  case llvm::Triple::MachO:
+    return MachO;
+  case llvm::Triple::COFF:
+    return COFF;
+  default:
+    break;
+  }
+
+  ErrorBuilder::fatal(UnsupportedObjectFormat, TargetTriple.str());
+}
+
+} // namespace clang::ssaf
diff --git a/clang/lib/ScalableStaticAnalysis/Core/Model/EntityLinkage.cpp b/clang/lib/ScalableStaticAnalysis/Core/Model/EntityLinkage.cpp
index 0bae90cd66a22..0833ea006f260 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/Model/EntityLinkage.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Core/Model/EntityLinkage.cpp
@@ -13,7 +13,9 @@
 namespace clang::ssaf {
 
 bool EntityLinkage::operator==(const EntityLinkage &Other) const {
-  return Linkage == Other.Linkage;
+  return Linkage == Other.Linkage && Binding == Other.Binding &&
+         Coalescing == Other.Coalescing && Visibility == Other.Visibility &&
+         DefinitionKind == Other.DefinitionKind;
 }
 
 bool EntityLinkage::operator!=(const EntityLinkage &Other) const {
@@ -25,9 +27,29 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
   return OS << entityLinkageTypeToString(Linkage);
 }
 
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, EntityBinding Binding) {
+  return OS << entityBindingToString(Binding);
+}
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
+                              EntityCoalescing Coalescing) {
+  return OS << entityCoalescingToString(Coalescing);
+}
+
 llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
-                              const EntityLinkage &Linkage) {
-  return OS << "EntityLinkage(" << Linkage.getLinkage() << ")";
+                              EntityVisibility Visibility) {
+  return OS << entityVisibilityToString(Visibility);
+}
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
+                              EntityDefinitionKind DefinitionKind) {
+  return OS << entityDefinitionKindToString(DefinitionKind);
+}
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const EntityLinkage &EL) {
+  return OS << "EntityLinkage(" << EL.Linkage << ", " << EL.Binding << ", "
+            << EL.Coalescing << ", " << EL.Visibility << ", "
+            << EL.DefinitionKind << ")";
 }
 
 } // namespace clang::ssaf
diff --git a/clang/lib/ScalableStaticAnalysis/Core/ModelStringConversions.h b/clang/lib/ScalableStaticAnalysis/Core/ModelStringConversions.h
index 9a4392bd72990..55a9e8bc460f8 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/ModelStringConversions.h
+++ b/clang/lib/ScalableStaticAnalysis/Core/ModelStringConversions.h
@@ -90,6 +90,125 @@ entityLinkageTypeFromString(llvm::StringRef Str) {
   return std::nullopt;
 }
 
+//===----------------------------------------------------------------------===//
+// EntityBinding
+//===----------------------------------------------------------------------===//
+
+/// Returns the canonical string representation of \p B (e.g. "Strong",
+/// "Weak").
+inline llvm::StringRef entityBindingToString(EntityBinding B) {
+  switch (B) {
+  case EntityBinding::Strong:
+    return "Strong";
+  case EntityBinding::Weak:
+    return "Weak";
+  case EntityBinding::Common:
+    return "Common";
+  case EntityBinding::Undefined:
+    return "Undefined";
+  }
+  llvm_unreachable("Unhandled EntityBinding variant");
+}
+
+/// Parses a string produced by entityBindingToString(). Returns std::nullopt
+/// if \p Str does not match any known EntityBinding value.
+inline std::optional<EntityBinding>
+entityBindingFromString(llvm::StringRef Str) {
+  if (Str == "Strong")
+    return EntityBinding::Strong;
+  if (Str == "Weak")
+    return EntityBinding::Weak;
+  if (Str == "Common")
+    return EntityBinding::Common;
+  if (Str == "Undefined")
+    return EntityBinding::Undefined;
+  return std::nullopt;
+}
+
+//===----------------------------------------------------------------------===//
+// EntityCoalescing
+//===----------------------------------------------------------------------===//
+
+/// Returns the canonical string representation of \p C (e.g. "None", "ODR").
+inline llvm::StringRef entityCoalescingToString(EntityCoalescing C) {
+  switch (C) {
+  case EntityCoalescing::None:
+    return "None";
+  case EntityCoalescing::ODR:
+    return "ODR";
+  }
+  llvm_unreachable("Unhandled EntityCoalescing variant");
+}
+
+/// Parses a string produced by entityCoalescingToString(). Returns std::nullopt
+/// if \p Str does not match any known EntityCoalescing value.
+inline std::optional<EntityCoalescing>
+entityCoalescingFromString(llvm::StringRef Str) {
+  if (Str == "None")
+    return EntityCoalescing::None;
+  if (Str == "ODR")
+    return EntityCoalescing::ODR;
+  return std::nullopt;
+}
+
+//===----------------------------------------------------------------------===//
+// EntityVisibility
+//===----------------------------------------------------------------------===//
+
+/// Returns the canonical string representation of \p V (e.g. "Default",
+/// "Hidden", "Protected").
+inline llvm::StringRef entityVisibilityToString(EntityVisibility V) {
+  switch (V) {
+  case EntityVisibility::Default:
+    return "Default";
+  case EntityVisibility::Hidden:
+    return "Hidden";
+  case EntityVisibility::Protected:
+    return "Protected";
+  }
+  llvm_unreachable("Unhandled EntityVisibility variant");
+}
+
+/// Parses a string produced by entityVisibilityToString(). Returns std::nullopt
+/// if \p Str does not match any known EntityVisibility value.
+inline std::optional<EntityVisibility>
+entityVisibilityFromString(llvm::StringRef Str) {
+  if (Str == "Default")
+    return EntityVisibility::Default;
+  if (Str == "Hidden")
+    return EntityVisibility::Hidden;
+  if (Str == "Protected")
+    return EntityVisibility::Protected;
+  return std::nullopt;
+}
+
+//===----------------------------------------------------------------------===//
+// EntityDefinitionKind
+//===----------------------------------------------------------------------===//
+
+/// Returns the canonical string representation of \p DK (e.g. "Definition",
+/// "Declaration").
+inline llvm::StringRef entityDefinitionKindToString(EntityDefinitionKind DK) {
+  switch (DK) {
+  case EntityDefinitionKind::Definition:
+    return "Definition";
+  case EntityDefinitionKind::Declaration:
+    return "Declaration";
+  }
+  llvm_unreachable("Unhandled EntityDefinitionKind variant");
+}
+
+/// Parses a string produced by entityDefinitionKindToString(). Returns
+/// std::nullopt if \p Str does not match any known EntityDefinitionKind value.
+inline std::optional<EntityDefinitionKind>
+entityDefinitionKindFromString(llvm::StringRef Str) {
+  if (Str == "Definition")
+    return EntityDefinitionKind::Definition;
+  if (Str == "Declaration")
+    return EntityDefinitionKind::Declaration;
+  return std::nullopt;
+}
+
 } // namespace clang::ssaf
 
 #endif // LLVM_CLANG_LIB_ScalableStaticAnalysis_CORE_MODELSTRINGCONVERSIONS_H
diff --git a/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp b/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
index 35c45ac108e07..f7a6ba9d1cf89 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
@@ -11,8 +11,20 @@
 
 namespace clang::ssaf {
 
+const char JSONEntitySummaryEncoding::Kind = 0;
+
+bool JSONEntitySummaryEncoding::equals(
+    const EntitySummaryEncoding &Other) const {
+  if (Other.getEncodingKind() != getEncodingKind()) {
+    return false;
+  }
+  // json::Value's operator== compares objects as maps, so key order does not
+  // affect the result.
+  return Data == static_cast<const JSONEntitySummaryEncoding &>(Other).Data;
+}
+
 llvm::Error JSONEntitySummaryEncoding::patchEntityIdObject(
-    llvm::json::Object &Obj, const std::map<EntityId, EntityId> &Table,
+    llvm::json::Object &Obj, const EntityResolutionMap &Resolution,
     llvm::json::Value *AtVal) {
 
   if (Obj.size() != 1) {
@@ -31,8 +43,8 @@ llvm::Error JSONEntitySummaryEncoding::patchEntityIdObject(
   }
 
   auto OldId = JSONFormat::makeEntityId(*OptEntityIdIndex);
-  auto It = Table.find(OldId);
-  if (It == Table.end()) {
+  auto It = Resolution.find(OldId);
+  if (It == Resolution.end()) {
     return ErrorBuilder::create(std::errc::invalid_argument,
                                 ErrorMessages::FailedToPatchEntityIdNotInTable,
                                 OldId)
@@ -45,32 +57,34 @@ llvm::Error JSONEntitySummaryEncoding::patchEntityIdObject(
 }
 
 llvm::Error JSONEntitySummaryEncoding::patchRegularObject(
-    llvm::json::Object &Obj, const std::map<EntityId, EntityId> &Table) {
+    llvm::json::Object &Obj, const EntityResolutionMap &Resolution) {
   for (auto &[Key, Val] : Obj) {
-    if (auto Err = patchValue(Val, Table)) {
+    if (auto Err = patchValue(Val, Resolution)) {
       return Err;
     }
   }
   return llvm::Error::success();
 }
 
-llvm::Error JSONEntitySummaryEncoding::patchObject(
-    llvm::json::Object &Obj, const std::map<EntityId, EntityId> &Table) {
+llvm::Error
+JSONEntitySummaryEncoding::patchObject(llvm::json::Object &Obj,
+                                       const EntityResolutionMap &Resolution) {
 
   llvm::json::Value *AtVal = Obj.get(JSONEntityIdKey);
-  return AtVal ? patchEntityIdObject(Obj, Table, AtVal)
-               : patchRegularObject(Obj, Table);
+  return AtVal ? patchEntityIdObject(Obj, Resolution, AtVal)
+               : patchRegularObject(Obj, Resolution);
 }
 
-llvm::Error JSONEntitySummaryEncoding::patchValue(
-    llvm::json::Value &V, const std::map<EntityId, EntityId> &Table) {
+llvm::Error
+JSONEntitySummaryEncoding::patchValue(llvm::json::Value &V,
+                                      const EntityResolutionMap &Resolution) {
   if (llvm::json::Object *Obj = V.getAsObject()) {
-    if (auto Err = patchObject(*Obj, Table)) {
+    if (auto Err = patchObject(*Obj, Resolution)) {
       return Err;
     }
   } else if (llvm::json::Array *Arr = V.getAsArray()) {
     for (auto &Val : *Arr) {
-      if (auto Err = patchValue(Val, Table)) {
+      if (auto Err = patchValue(Val, Resolution)) {
         return Err;
       }
     }
@@ -78,9 +92,9 @@ llvm::Error JSONEntitySummaryEncoding::patchValue(
   return llvm::Error::success();
 }
 
-llvm::Error JSONEntitySummaryEncoding::patch(
-    const std::map<EntityId, EntityId> &EntityResolutionTable) {
-  return patchValue(Data, EntityResolutionTable);
+llvm::Error
+JSONEntitySummaryEncoding::patch(const EntityResolutionMap &Resolution) {
+  return patchValue(Data, Resolution);
 }
 
 } // namespace clang::ssaf
diff --git a/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONEntitySummaryEncoding.h b/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
index 5203e9d8ffc6f..1f7c700ceff84 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
+++ b/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
@@ -27,22 +27,29 @@ class JSONEntitySummaryEncoding final : public EntitySummaryEncoding {
   friend JSONFormat;
 
 public:
-  llvm::Error
-  patch(const std::map<EntityId, EntityId> &EntityResolutionTable) override;
+  llvm::Error patch(const EntityResolutionMap &Resolution) override;
+
+  const void *getEncodingKind() const override { return &Kind; }
+
+  bool equals(const EntitySummaryEncoding &Other) const override;
 
 private:
+  /// Distinguishes this encoding from other formats' without RTTI. Its address
+  /// is the identity; the value is irrelevant.
+  static const char Kind;
+
   explicit JSONEntitySummaryEncoding(llvm::json::Value Data)
       : Data(std::move(Data)) {}
 
   llvm::Error patchEntityIdObject(llvm::json::Object &Obj,
-                                  const std::map<EntityId, EntityId> &Table,
+                                  const EntityResolutionMap &Resolution,
                                   llvm::json::Value *AtVal);
   llvm::Error patchRegularObject(llvm::json::Object &Obj,
-                                 const std::map<EntityId, EntityId> &Table);
+                                 const EntityResolutionMap &Resolution);
   llvm::Error patchObject(llvm::json::Object &Obj,
-                          const std::map<EntityId, EntityId> &Table);
+                          const EntityResolutionMap &Resolution);
   llvm::Error patchValue(llvm::json::Value &V,
-                         const std::map<EntityId, EntityId> &Table);
+                         const EntityResolutionMap &Resolution);
 
   llvm::json::Value Data;
 };
diff --git a/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONFormatImpl.cpp b/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONFormatImpl.cpp
index b19849233f0f8..e6b9bffcdff46 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONFormatImpl.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONFormatImpl.cpp
@@ -443,6 +443,67 @@ llvm::StringRef entityLinkageTypeToJSON(EntityLinkageType LT) {
   return entityLinkageTypeToString(LT);
 }
 
+//----------------------------------------------------------------------------
+// EntityBinding / EntityVisibility / EntityDefinitionKind
+//----------------------------------------------------------------------------
+
+llvm::Expected<EntityBinding> entityBindingFromJSON(llvm::StringRef Str) {
+  auto Opt = entityBindingFromString(Str);
+  if (!Opt) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::InvalidEntityBinding, Str)
+        .build();
+  }
+  return *Opt;
+}
+
+llvm::StringRef entityBindingToJSON(EntityBinding B) {
+  return entityBindingToString(B);
+}
+
+llvm::Expected<EntityCoalescing> entityCoalescingFromJSON(llvm::StringRef Str) {
+  auto Opt = entityCoalescingFromString(Str);
+  if (!Opt) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::InvalidEntityCoalescing, Str)
+        .build();
+  }
+  return *Opt;
+}
+
+llvm::StringRef entityCoalescingToJSON(EntityCoalescing C) {
+  return entityCoalescingToString(C);
+}
+
+llvm::Expected<EntityVisibility> entityVisibilityFromJSON(llvm::StringRef Str) {
+  auto Opt = entityVisibilityFromString(Str);
+  if (!Opt) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::InvalidEntityVisibility, Str)
+        .build();
+  }
+  return *Opt;
+}
+
+llvm::StringRef entityVisibilityToJSON(EntityVisibility V) {
+  return entityVisibilityToString(V);
+}
+
+llvm::Expected<EntityDefinitionKind>
+entityDefinitionKindFromJSON(llvm::StringRef Str) {
+  auto Opt = entityDefinitionKindFromString(Str);
+  if (!Opt) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::InvalidEntityDefinitionKind, Str)
+        .build();
+  }
+  return *Opt;
+}
+
+llvm::StringRef entityDefinitionKindToJSON(EntityDefinitionKind DK) {
+  return entityDefinitionKindToString(DK);
+}
+
 //----------------------------------------------------------------------------
 // TargetTriple
 //----------------------------------------------------------------------------
@@ -464,27 +525,88 @@ llvm::Error validateNormalizedTargetTriple(llvm::StringRef Triple) {
 
 llvm::Expected<EntityLinkage>
 JSONFormat::entityLinkageFromJSON(const Object &EntityLinkageObject) const {
-  auto OptLinkageStr = EntityLinkageObject.getString("type");
-  if (!OptLinkageStr) {
-    return ErrorBuilder::create(std::errc::invalid_argument,
-                                ErrorMessages::FailedToReadObjectAtField,
-                                "EntityLinkageType", "type", "string")
-        .build();
-  }
+  auto ReadStr = [&](llvm::StringRef ObjLabel,
+                     llvm::StringRef Field) -> llvm::Expected<llvm::StringRef> {
+    auto OptStr = EntityLinkageObject.getString(Field);
+    if (!OptStr) {
+      return ErrorBuilder::create(std::errc::invalid_argument,
+                                  ErrorMessages::FailedToReadObjectAtField,
+                                  ObjLabel, Field, "string")
+          .build();
+    }
+    return *OptStr;
+  };
 
-  auto ExpectedLinkageType = entityLinkageTypeFromJSON(*OptLinkageStr);
+  auto TypeStr = ReadStr("EntityLinkageType", "type");
+  if (!TypeStr) {
+    return TypeStr.takeError();
+  }
+  auto ExpectedLinkageType = entityLinkageTypeFromJSON(*TypeStr);
   if (!ExpectedLinkageType) {
     return ErrorBuilder::wrap(ExpectedLinkageType.takeError())
         .context(ErrorMessages::ReadingFromField, "EntityLinkageType", "type")
         .build();
   }
 
-  return EntityLinkage(*ExpectedLinkageType);
+  auto BindingStr = ReadStr("EntityBinding", "binding");
+  if (!BindingStr) {
+    return BindingStr.takeError();
+  }
+  auto ExpectedBinding = entityBindingFromJSON(*BindingStr);
+  if (!ExpectedBinding) {
+    return ErrorBuilder::wrap(ExpectedBinding.takeError())
+        .context(ErrorMessages::ReadingFromField, "EntityBinding", "binding")
+        .build();
+  }
+
+  auto CoalescingStr = ReadStr("EntityCoalescing", "coalescing");
+  if (!CoalescingStr) {
+    return CoalescingStr.takeError();
+  }
+  auto ExpectedCoalescing = entityCoalescingFromJSON(*CoalescingStr);
+  if (!ExpectedCoalescing) {
+    return ErrorBuilder::wrap(ExpectedCoalescing.takeError())
+        .context(ErrorMessages::ReadingFromField, "EntityCoalescing",
+                 "coalescing")
+        .build();
+  }
+
+  auto VisibilityStr = ReadStr("EntityVisibility", "visibility");
+  if (!VisibilityStr) {
+    return VisibilityStr.takeError();
+  }
+  auto ExpectedVisibility = entityVisibilityFromJSON(*VisibilityStr);
+  if (!ExpectedVisibility) {
+    return ErrorBuilder::wrap(ExpectedVisibility.takeError())
+        .context(ErrorMessages::ReadingFromField, "EntityVisibility",
+                 "visibility")
+        .build();
+  }
+
+  auto DefinitionStr = ReadStr("EntityDefinitionKind", "definition");
+  if (!DefinitionStr) {
+    return DefinitionStr.takeError();
+  }
+  auto ExpectedDefinitionKind = entityDefinitionKindFromJSON(*DefinitionStr);
+  if (!ExpectedDefinitionKind) {
+    return ErrorBuilder::wrap(ExpectedDefinitionKind.takeError())
+        .context(ErrorMessages::ReadingFromField, "EntityDefinitionKind",
+                 "definition")
+        .build();
+  }
+
+  return EntityLinkage(*ExpectedLinkageType, *ExpectedBinding,
+                       *ExpectedCoalescing, *ExpectedVisibility,
+                       *ExpectedDefinitionKind);
 }
 
 Object JSONFormat::entityLinkageToJSON(const EntityLinkage &EL) const {
   Object Result;
   Result["type"] = entityLinkageTypeToJSON(getLinkage(EL));
+  Result["binding"] = entityBindingToJSON(getBinding(EL));
+  Result["coalescing"] = entityCoalescingToJSON(getCoalescing(EL));
+  Result["visibility"] = entityVisibilityToJSON(getVisibility(EL));
+  Result["definition"] = entityDefinitionKindToJSON(getDefinitionKind(EL));
   return Result;
 }
 
diff --git a/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONFormatImpl.h b/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONFormatImpl.h
index b17f296eda1b4..ebcda8634ba84 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONFormatImpl.h
+++ b/clang/lib/ScalableStaticAnalysis/Core/Serialization/JSONFormat/JSONFormatImpl.h
@@ -114,6 +114,18 @@ inline constexpr const char *InvalidBuildNamespaceKind =
 inline constexpr const char *InvalidEntityLinkageType =
     "invalid EntityLinkageType value '{0}' for field 'type'";
 
+inline constexpr const char *InvalidEntityBinding =
+    "invalid EntityBinding value '{0}' for field 'binding'";
+
+inline constexpr const char *InvalidEntityCoalescing =
+    "invalid EntityCoalescing value '{0}' for field 'coalescing'";
+
+inline constexpr const char *InvalidEntityVisibility =
+    "invalid EntityVisibility value '{0}' for field 'visibility'";
+
+inline constexpr const char *InvalidEntityDefinitionKind =
+    "invalid EntityDefinitionKind value '{0}' for field 'definition'";
+
 inline constexpr const char *FailedToDeserializeLinkageTableExtraId =
     "failed to deserialize LinkageTable: extra '{0}' not present in IdTable";
 
@@ -220,6 +232,23 @@ entityLinkageTypeFromJSON(llvm::StringRef EntityLinkageTypeStr);
 // Provided for consistency with respect to rest of the codebase.
 llvm::StringRef entityLinkageTypeToJSON(EntityLinkageType LT);
 
+//----------------------------------------------------------------------------
+// EntityBinding / EntityCoalescing / EntityVisibility / EntityDefinitionKind
+//----------------------------------------------------------------------------
+
+llvm::Expected<EntityBinding> entityBindingFromJSON(llvm::StringRef Str);
+llvm::StringRef entityBindingToJSON(EntityBinding B);
+
+llvm::Expected<EntityCoalescing> entityCoalescingFromJSON(llvm::StringRef Str);
+llvm::StringRef entityCoalescingToJSON(EntityCoalescing C);
+
+llvm::Expected<EntityVisibility> entityVisibilityFromJSON(llvm::StringRef Str);
+llvm::StringRef entityVisibilityToJSON(EntityVisibility V);
+
+llvm::Expected<EntityDefinitionKind>
+entityDefinitionKindFromJSON(llvm::StringRef Str);
+llvm::StringRef entityDefinitionKindToJSON(EntityDefinitionKind DK);
+
 //----------------------------------------------------------------------------
 // TargetTriple helpers
 //----------------------------------------------------------------------------
diff --git a/clang/lib/ScalableStaticAnalysis/Core/TUSummary/TUSummaryBuilder.cpp b/clang/lib/ScalableStaticAnalysis/Core/TUSummary/TUSummaryBuilder.cpp
index ba2def365dddc..a0fb5ad7c9c29 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/TUSummary/TUSummaryBuilder.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Core/TUSummary/TUSummaryBuilder.cpp
@@ -18,12 +18,16 @@ using namespace clang;
 using namespace ssaf;
 
 EntityId TUSummaryBuilder::addEntity(const EntityName &EN,
-                                     EntityLinkageType Linkage) {
+                                     EntityLinkage Linkage) {
   EntityId Id = Summary.IdTable.getId(EN);
-  [[maybe_unused]] EntityLinkageType Link =
-      Summary.LinkageTable.try_emplace(Id, Linkage).first->second.getLinkage();
-  // Even if we had in the past a linkage, that must bee the same as we set now.
-  assert(Link == Linkage);
+  [[maybe_unused]] const EntityLinkage &Existing =
+      Summary.LinkageTable.try_emplace(Id, Linkage).first->second;
+  // An entity's linkage type must be stable across its redeclarations within a
+  // TU. The other symbol properties (binding, visibility, definition kind) may
+  // legitimately differ between a declaration and a definition of the same
+  // entity; reconciling those is the linker's responsibility, so we keep the
+  // first occurrence here and only assert linkage-type stability.
+  assert(Existing.getLinkage() == Linkage.getLinkage());
   return Id;
 }
 
diff --git a/clang/lib/ScalableStaticAnalysis/Core/TUSummary/TUSummaryExtractor.cpp b/clang/lib/ScalableStaticAnalysis/Core/TUSummary/TUSummaryExtractor.cpp
index 40205b90a4eb6..6cd8250d66b9a 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/TUSummary/TUSummaryExtractor.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Core/TUSummary/TUSummaryExtractor.cpp
@@ -7,6 +7,8 @@
 //===----------------------------------------------------------------------===//
 
 #include "clang/ScalableStaticAnalysis/Core/TUSummary/TUSummaryExtractor.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Attr.h"
 #include "clang/AST/Decl.h"
 #include "clang/AST/DeclBase.h"
 #include "clang/ScalableStaticAnalysis/Core/ASTEntityMapping.h"
@@ -19,13 +21,135 @@
 using namespace clang;
 using namespace ssaf;
 
-static EntityLinkageType getLinkageForDecl(const Decl *D) {
-  const auto *ND = dyn_cast<NamedDecl>(D);
-  if (!ND)
+static EntityLinkageType mapLinkage(const NamedDecl *ND) {
+  switch (ND->getFormalLinkage()) {
+  case Linkage::Invalid: {
+    llvm_unreachable("Shouldn't be invalid");
+  }
+  case Linkage::None:
     return EntityLinkageType::None;
+  case Linkage::Internal:
+  case Linkage::UniqueExternal:
+  case Linkage::VisibleNone:
+    return EntityLinkageType::Internal;
+  case Linkage::Module:
+  case Linkage::External:
+    return EntityLinkageType::External;
+  }
+  llvm_unreachable("Unhandled clang::Linkage kind");
+}
+
+static EntityVisibility mapVisibility(const NamedDecl *ND) {
+  switch (ND->getVisibility()) {
+  case HiddenVisibility:
+    return EntityVisibility::Hidden;
+  case ProtectedVisibility:
+    return EntityVisibility::Protected;
+  case DefaultVisibility:
+    return EntityVisibility::Default;
+  }
+  llvm_unreachable("Unhandled clang::Visibility kind");
+}
+
+static EntityDefinitionKind mapDefinitionKind(const NamedDecl *ND,
+                                              ASTContext &Ctx) {
+  if (const auto *FD = dyn_cast<FunctionDecl>(ND)) {
+    return FD->isThisDeclarationADefinition()
+               ? EntityDefinitionKind::Definition
+               : EntityDefinitionKind::Declaration;
+  }
+  if (const auto *VD = dyn_cast<VarDecl>(ND)) {
+    return VD->isThisDeclarationADefinition(Ctx) == VarDecl::DeclarationOnly
+               ? EntityDefinitionKind::Declaration
+               : EntityDefinitionKind::Definition;
+  }
+  return EntityDefinitionKind::Definition;
+}
+
+static EntityBinding mapBinding(const NamedDecl *ND, ASTContext &Ctx) {
+  if (ND->hasAttr<WeakAttr>() || ND->hasAttr<WeakRefAttr>()) {
+    return EntityBinding::Weak;
+  }
+  if (const auto *FD = dyn_cast<FunctionDecl>(ND)) {
+    if (!FD->isThisDeclarationADefinition()) {
+      return EntityBinding::Undefined;
+    }
+    switch (Ctx.GetGVALinkageForFunction(FD)) {
+    case GVA_AvailableExternally:
+      return EntityBinding::Undefined;
+    case GVA_DiscardableODR:
+    case GVA_StrongODR:
+      // An ODR definition binds strongly; that its duplicates are permitted is
+      // recorded separately by mapCoalescing(). Object formats express this
+      // differently — ELF and Mach-O lower it to a weak symbol, COFF keeps it
+      // strong and uses a COMDAT — which is LinkageRules' concern, not ours.
+      return EntityBinding::Strong;
+    case GVA_Internal:
+    case GVA_StrongExternal:
+      return EntityBinding::Strong;
+    }
+    return EntityBinding::Strong;
+  }
+  if (const auto *VD = dyn_cast<VarDecl>(ND)) {
+    switch (VD->isThisDeclarationADefinition(Ctx)) {
+    case VarDecl::DeclarationOnly:
+      return EntityBinding::Undefined;
+    case VarDecl::TentativeDefinition:
+      return EntityBinding::Common;
+    case VarDecl::Definition:
+      break;
+    }
+    return EntityBinding::Strong;
+  }
+  return EntityBinding::Strong;
+}
+
+/// Returns whether every definition of \p ND is required to be identical, as
+/// the One Definition Rule guarantees for inline functions, templates and
+/// vtables.
+static EntityCoalescing mapCoalescing(const NamedDecl *ND, ASTContext &Ctx) {
+  // An explicitly weak symbol may be replaced by an unrelated definition, so
+  // its copies carry no identity guarantee even if it is also inline.
+  if (ND->hasAttr<WeakAttr>() || ND->hasAttr<WeakRefAttr>()) {
+    return EntityCoalescing::None;
+  }
+  if (const auto *FD = dyn_cast<FunctionDecl>(ND)) {
+    if (!FD->isThisDeclarationADefinition()) {
+      return EntityCoalescing::None;
+    }
+    switch (Ctx.GetGVALinkageForFunction(FD)) {
+    case GVA_DiscardableODR:
+    case GVA_StrongODR:
+      return EntityCoalescing::ODR;
+    default:
+      return EntityCoalescing::None;
+    }
+  }
+  if (const auto *VD = dyn_cast<VarDecl>(ND)) {
+    if (VD->isThisDeclarationADefinition(Ctx) != VarDecl::Definition) {
+      return EntityCoalescing::None;
+    }
+    switch (Ctx.GetGVALinkageForVariable(VD)) {
+    case GVA_DiscardableODR:
+    case GVA_StrongODR:
+      return EntityCoalescing::ODR;
+    default:
+      return EntityCoalescing::None;
+    }
+  }
+  return EntityCoalescing::None;
+}
+
+static EntityLinkage getEntityLinkageForDecl(const Decl *D) {
+  const auto *ND = dyn_cast<NamedDecl>(D);
+  if (!ND) {
+    return EntityLinkage(EntityLinkageType::None, EntityBinding::Undefined,
+                         EntityCoalescing::None, EntityVisibility::Default,
+                         EntityDefinitionKind::Declaration);
+  }
 
   // Parameters have no linkage in C++, but SSAF needs them to inherit
-  // the external linkage from their parent functions.
+  // the linker properties from their parent functions.
   // Here is why:
   //   SSAF treats parameters as entities and may not always associate them back
   //   to their parent functions. Therefore, it needs to identify parameters of
@@ -36,35 +160,21 @@ static EntityLinkageType getLinkageForDecl(const Decl *D) {
   if (const auto *PVD = dyn_cast<ParmVarDecl>(D)) {
     if (const auto *FD = llvm::dyn_cast_or_null<FunctionDecl>(
             PVD->getParentFunctionOrMethod())) {
-      return getLinkageForDecl(FD);
+      return getEntityLinkageForDecl(FD);
     }
   }
 
-  switch (ND->getFormalLinkage()) {
-  case Linkage::Invalid: {
-    llvm_unreachable("Shouldn't be invalid");
-  }
-  case Linkage::None:
-    return EntityLinkageType::None;
-  case Linkage::Internal:
-    return EntityLinkageType::Internal;
-  case Linkage::UniqueExternal:
-    return EntityLinkageType::Internal;
-  case Linkage::VisibleNone:
-    return EntityLinkageType::Internal;
-  case Linkage::Module:
-    return EntityLinkageType::External;
-  case Linkage::External:
-    return EntityLinkageType::External;
-  }
-  llvm_unreachable("Unhandled clang::Linkage kind");
+  ASTContext &Ctx = ND->getASTContext();
+  return EntityLinkage(mapLinkage(ND), mapBinding(ND, Ctx),
+                       mapCoalescing(ND, Ctx), mapVisibility(ND),
+                       mapDefinitionKind(ND, Ctx));
 }
 
 std::optional<EntityId> TUSummaryExtractor::addEntity(const NamedDecl *D) {
   auto Name = getEntityName(D);
   if (!Name)
     return std::nullopt;
-  return SummaryBuilder.addEntity(*Name, getLinkageForDecl(D));
+  return SummaryBuilder.addEntity(*Name, getEntityLinkageForDecl(D));
 }
 
 std::optional<EntityId>
@@ -72,7 +182,7 @@ TUSummaryExtractor::addEntityForReturn(const FunctionDecl *FD) {
   auto Name = getEntityNameForReturn(FD);
   if (!Name)
     return std::nullopt;
-  return SummaryBuilder.addEntity(*Name, getLinkageForDecl(FD));
+  return SummaryBuilder.addEntity(*Name, getEntityLinkageForDecl(FD));
 }
 
 const SSAFOptions &TUSummaryExtractor::getOptions() const {
diff --git a/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-bad-array.json b/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-bad-array.json
index 282917559e5b5..7dcc9e7f559f3 100644
--- a/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-bad-array.json
+++ b/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-bad-array.json
@@ -65,19 +65,31 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-bad-summary.json b/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-bad-summary.json
index c2c5e119ca330..b40bb2162519a 100644
--- a/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-bad-summary.json
+++ b/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-bad-summary.json
@@ -42,19 +42,31 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-no-key.json b/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-no-key.json
index 14c654f8b7c14..cad429c9c515e 100644
--- a/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-no-key.json
+++ b/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary-no-key.json
@@ -71,19 +71,31 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary.json b/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary.json
index 9b0b4575e2f61..c05d6f5f60fdd 100644
--- a/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary.json
+++ b/clang/test/Analysis/Scalable/PointerFlow/Inputs/tu-summary.json
@@ -68,19 +68,31 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary-bad-id.json b/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary-bad-id.json
index 286826682b050..3f01aaa9a24a7 100644
--- a/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary-bad-id.json
+++ b/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary-bad-id.json
@@ -27,7 +27,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary-no-key.json b/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary-no-key.json
index 3608117904567..da1a7e82e719b 100644
--- a/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary-no-key.json
+++ b/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary-no-key.json
@@ -25,7 +25,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary.json b/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary.json
index a6231439888c5..f2fe359e62abd 100644
--- a/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary.json
+++ b/clang/test/Analysis/Scalable/TypeConstrainedPointers/Inputs/tu-summary.json
@@ -46,19 +46,31 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-bad-element.json b/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-bad-element.json
index 8e73183ce3b29..70fc713f88ce5 100644
--- a/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-bad-element.json
+++ b/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-bad-element.json
@@ -34,13 +34,21 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-bad-ptr-level.json b/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-bad-ptr-level.json
index 9a4ef04219f0a..e4daa766a5cce 100644
--- a/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-bad-ptr-level.json
+++ b/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-bad-ptr-level.json
@@ -39,13 +39,21 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-no-key.json b/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-no-key.json
index 0ab350fa56182..d1a6cde4be93d 100644
--- a/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-no-key.json
+++ b/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary-no-key.json
@@ -39,13 +39,21 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary.json b/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary.json
index e4df108408472..6af968d8cb0df 100644
--- a/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary.json
+++ b/clang/test/Analysis/Scalable/UnsafeBufferUsage/Inputs/tu-summary.json
@@ -82,19 +82,31 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/linkage-extraction.cpp b/clang/test/Analysis/Scalable/linkage-extraction.cpp
new file mode 100644
index 0000000000000..45a001ed9b888
--- /dev/null
+++ b/clang/test/Analysis/Scalable/linkage-extraction.cpp
@@ -0,0 +1,73 @@
+// Verifies the linkage the extractor records for each source construct.
+//
+// Binding and coalescing are independent axes: an inline function binds
+// strongly and additionally guarantees its definitions are identical, whereas
+// __attribute__((weak)) binds weakly with no such guarantee. Object formats
+// encode this differently — ELF and Mach-O lower an ODR definition to a weak
+// symbol while COFF keeps it strong and uses a COMDAT — so the summary records
+// the source-level fact and leaves the lowering to the linker. See
+// docs/ssaf-linker-elf-behavior.md §3 and docs/ssaf-linker-coff-behavior.md §2.
+//
+// Each construct lives in its own file so that the summary holds exactly one
+// entity: ids are assigned in traversal order, so a shared file would make the
+// checks depend on declaration order rather than on the linkage itself.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+// RUN: split-file %s %t
+
+// An ordinary definition: strong, with no coalescing guarantee.
+// RUN: %clang_cc1 -fsyntax-only %t/ordinary.cpp \
+// RUN:   --ssaf-extract-summaries=CallGraph --ssaf-compilation-unit-id=cu \
+// RUN:   --ssaf-tu-summary-file=%t/ordinary.json
+// RUN: cat %t/ordinary.json | FileCheck %s --check-prefix=ORDINARY
+// ORDINARY:      "usr": "c:@F at ordinary#"
+// ORDINARY:      "binding": "Strong",
+// ORDINARY-NEXT: "coalescing": "None",
+// ORDINARY-NEXT: "definition": "Definition",
+
+// An inline definition: still strong, but its copies must be identical.
+// RUN: %clang_cc1 -fsyntax-only %t/inlined.cpp \
+// RUN:   --ssaf-extract-summaries=CallGraph --ssaf-compilation-unit-id=cu \
+// RUN:   --ssaf-tu-summary-file=%t/inlined.json
+// RUN: cat %t/inlined.json | FileCheck %s --check-prefix=INLINED
+// INLINED:      "usr": "c:@F at inlined#"
+// INLINED:      "binding": "Strong",
+// INLINED-NEXT: "coalescing": "ODR",
+// INLINED-NEXT: "definition": "Definition",
+
+// An explicitly weak definition may be replaced by an unrelated one, so it
+// carries no ODR guarantee even though it is also inline.
+// RUN: %clang_cc1 -fsyntax-only %t/weak_inlined.cpp \
+// RUN:   --ssaf-extract-summaries=CallGraph --ssaf-compilation-unit-id=cu \
+// RUN:   --ssaf-tu-summary-file=%t/weak_inlined.json
+// RUN: cat %t/weak_inlined.json | FileCheck %s --check-prefix=WEAK
+// WEAK:      "usr": "c:@F at weak_inlined#"
+// WEAK:      "binding": "Weak",
+// WEAK-NEXT: "coalescing": "None",
+// WEAK-NEXT: "definition": "Definition",
+
+// A hidden definition keeps its visibility; the extractor records what the
+// source said and leaves target-specific coercion to the linker.
+// RUN: %clang_cc1 -fsyntax-only %t/hidden.cpp \
+// RUN:   --ssaf-extract-summaries=CallGraph --ssaf-compilation-unit-id=cu \
+// RUN:   --ssaf-tu-summary-file=%t/hidden.json
+// RUN: cat %t/hidden.json | FileCheck %s --check-prefix=HIDDEN
+// HIDDEN:      "usr": "c:@F at hidden_fn#"
+// HIDDEN:      "binding": "Strong",
+// HIDDEN-NEXT: "coalescing": "None",
+// HIDDEN-NEXT: "definition": "Definition",
+// HIDDEN-NEXT: "type": "External",
+// HIDDEN-NEXT: "visibility": "Hidden"
+
+//--- ordinary.cpp
+int ordinary(void) { return 1; }
+
+//--- inlined.cpp
+inline int inlined(void) { return 2; }
+
+//--- weak_inlined.cpp
+__attribute__((weak)) inline int weak_inlined(void) { return 3; }
+
+//--- hidden.cpp
+__attribute__((visibility("hidden"))) int hidden_fn(void) { return 4; }
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-tags-only.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-tags-only.json
index ddff7a789fffb..658cba233d42b 100644
--- a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-tags-only.json
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-tags-only.json
@@ -31,7 +31,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu.json
index 4654ada97a341..bf70122afacad 100644
--- a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu.json
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu.json
@@ -101,19 +101,31 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Artifact/Inputs/rt-multi-arch-shared-library-nonempty.json b/clang/test/Analysis/Scalable/ssaf-format/Artifact/Inputs/rt-multi-arch-shared-library-nonempty.json
index dc00a24f0eaf0..01715bf3b50ca 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/Artifact/Inputs/rt-multi-arch-shared-library-nonempty.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Artifact/Inputs/rt-multi-arch-shared-library-nonempty.json
@@ -21,7 +21,11 @@
         {
           "id": 0,
           "linkage": {
-            "type": "External"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "External",
+            "visibility": "Default"
           }
         }
       ],
@@ -55,7 +59,11 @@
         {
           "id": 0,
           "linkage": {
-            "type": "External"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "External",
+            "visibility": "Default"
           }
         }
       ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/invalid-direct-callee-element.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/invalid-direct-callee-element.json
index b726ec997917f..dfaafc5e35454 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/invalid-direct-callee-element.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/invalid-direct-callee-element.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/invalid-direct-callee-id.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/invalid-direct-callee-id.json
index c4d8af0dbfcfc..7045b0be2726e 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/invalid-direct-callee-id.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/invalid-direct-callee-id.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-col.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-col.json
index fb928fe397acc..ca97c24b636e7 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-col.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-col.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-file.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-file.json
index e43509f1f8ca4..ec2c3267ad849 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-file.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-file.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-line.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-line.json
index dd14915d37aba..970b0f8b8a4ca 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-line.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def-line.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def.json
index c0da9cd05f807..697ebadd3829d 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-def.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-direct-callees.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-direct-callees.json
index 45262d5813200..3748c6cd6720b 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-direct-callees.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-direct-callees.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-pretty-name.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-pretty-name.json
index b1e7de381a36a..6ce0a132e0057 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-pretty-name.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-pretty-name.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-virtual-callees.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-virtual-callees.json
index 02e313bdc67b0..c5ddbc9648a65 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-virtual-callees.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/CallGraph/missing-virtual-callees.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/duplicate-entity-id-in-data-map.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/duplicate-entity-id-in-data-map.json
index 08bda0ffa6c73..4398c91ac2dd3 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/duplicate-entity-id-in-data-map.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/duplicate-entity-id-in-data-map.json
@@ -28,7 +28,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/duplicate-entity.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/duplicate-entity.json
index 0d5e7d5b1eeec..2af19bf7296e1 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/duplicate-entity.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/duplicate-entity.json
@@ -45,13 +45,21 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-namespace.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-namespace.json
index 174423ce72c33..a805239d836f9 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-namespace.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-namespace.json
@@ -18,7 +18,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-suffix.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-suffix.json
index 92457c7f75d44..3400959cdc3f8 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-suffix.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-suffix.json
@@ -23,7 +23,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-usr.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-usr.json
index a3af210d887b1..968367686b9a0 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-usr.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/entity-name-missing-usr.json
@@ -27,7 +27,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-duplicate-id.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-duplicate-id.json
index e03fa76057044..7b83fbd790591 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-duplicate-id.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-duplicate-id.json
@@ -28,13 +28,21 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 0,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-id-not-uint64.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-id-not-uint64.json
index ae30fe240fb20..95a001f1f320f 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-id-not-uint64.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-id-not-uint64.json
@@ -10,7 +10,11 @@
     {
       "id": "not_a_number",
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-linkage-invalid-type.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-linkage-invalid-type.json
index 7f53e39f1d942..c724a25393ab0 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-linkage-invalid-type.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-linkage-invalid-type.json
@@ -10,7 +10,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "invalid_type"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "invalid_type",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-missing-id.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-missing-id.json
index 73738e0a42255..1ab2daf8b32a3 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-missing-id.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-entry-missing-id.json
@@ -9,7 +9,11 @@
   "linkage_table": [
     {
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-extra-id.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-extra-id.json
index af1436f97df13..749ca4fe440e1 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-extra-id.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/linkage-table-extra-id.json
@@ -10,7 +10,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-invalid-kind.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-invalid-kind.json
index 6c648ca818656..28fde31072946 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-invalid-kind.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-invalid-kind.json
@@ -24,7 +24,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-missing-kind.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-missing-kind.json
index 004ff880ab480..17c148eded9ec 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-missing-kind.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-missing-kind.json
@@ -23,7 +23,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-missing-name.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-missing-name.json
index b35b6bb00da4b..1af7f2e3a16f2 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-missing-name.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-missing-name.json
@@ -23,7 +23,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-not-object.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-not-object.json
index 00f88a4651d90..b5e299b720d28 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-not-object.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/namespace-element-not-object.json
@@ -21,7 +21,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-external.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-external.json
index 1024caba971c7..cd096c47213de 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-external.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-external.json
@@ -19,7 +19,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-internal.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-internal.json
index f4e9603d9bd0e..b29840e401f70 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-internal.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-internal.json
@@ -19,7 +19,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-multiple.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-multiple.json
index e2c333ffbc07a..e52bd82dbfdd0 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-multiple.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-multiple.json
@@ -45,19 +45,31 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-none.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-none.json
index 8330210158484..14b8703376ce3 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-none.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-linkage-none.json
@@ -19,7 +19,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-two-summary-types.json b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-two-summary-types.json
index 3297419191ef8..1f67eec48f0b6 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-two-summary-types.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/LUSummary/Inputs/rt-two-summary-types.json
@@ -203,31 +203,51 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 3,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 4,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/MultiArchSharedLibrary/Inputs/rt-nonempty-members.json b/clang/test/Analysis/Scalable/ssaf-format/MultiArchSharedLibrary/Inputs/rt-nonempty-members.json
index dc00a24f0eaf0..01715bf3b50ca 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/MultiArchSharedLibrary/Inputs/rt-nonempty-members.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/MultiArchSharedLibrary/Inputs/rt-nonempty-members.json
@@ -21,7 +21,11 @@
         {
           "id": 0,
           "linkage": {
-            "type": "External"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "External",
+            "visibility": "Default"
           }
         }
       ],
@@ -55,7 +59,11 @@
         {
           "id": 0,
           "linkage": {
-            "type": "External"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "External",
+            "visibility": "Default"
           }
         }
       ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/duplicate-entity-id-in-data-map.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/duplicate-entity-id-in-data-map.json
index a4d6723f0c595..94421c4a97665 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/duplicate-entity-id-in-data-map.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/duplicate-entity-id-in-data-map.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/duplicate-entity.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/duplicate-entity.json
index 16e47f09d2a00..c3cb7ea136301 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/duplicate-entity.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/duplicate-entity.json
@@ -22,11 +22,11 @@
   "linkage_table": [
     {
       "id": 0,
-      "linkage": { "type": "Internal" }
+      "linkage": { "binding": "Strong", "coalescing": "None", "definition": "Definition", "type": "Internal", "visibility": "Default" }
     },
     {
       "id": 1,
-      "linkage": { "type": "External" }
+      "linkage": { "binding": "Strong", "coalescing": "None", "definition": "Definition", "type": "External", "visibility": "Default" }
     }
   ],
   "data": [],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/entity-name-missing-suffix.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/entity-name-missing-suffix.json
index a9b793c706697..e0690cafc8e64 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/entity-name-missing-suffix.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/entity-name-missing-suffix.json
@@ -14,7 +14,7 @@
   "linkage_table": [
     {
       "id": 0,
-      "linkage": { "type": "External" }
+      "linkage": { "binding": "Strong", "coalescing": "None", "definition": "Definition", "type": "External", "visibility": "Default" }
     }
   ],
   "data": [],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/entity-name-missing-usr.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/entity-name-missing-usr.json
index d32cf5c7ff188..29678063f0c55 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/entity-name-missing-usr.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/entity-name-missing-usr.json
@@ -14,7 +14,7 @@
   "linkage_table": [
     {
       "id": 0,
-      "linkage": { "type": "Internal" }
+      "linkage": { "binding": "Strong", "coalescing": "None", "definition": "Definition", "type": "Internal", "visibility": "Default" }
     }
   ],
   "data": [],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/id-table-entry-missing-name.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/id-table-entry-missing-name.json
index 12b52496f3c04..e0655c130b483 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/id-table-entry-missing-name.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/id-table-entry-missing-name.json
@@ -11,7 +11,7 @@
   "linkage_table": [
     {
       "id": 0,
-      "linkage": { "type": "None" }
+      "linkage": { "binding": "Strong", "coalescing": "None", "definition": "Definition", "type": "None", "visibility": "Default" }
     }
   ],
   "data": [],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-duplicate-id.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-duplicate-id.json
index 2e3cd065e82d9..64f12f33238f6 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-duplicate-id.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-duplicate-id.json
@@ -15,11 +15,11 @@
   "linkage_table": [
     {
       "id": 0,
-      "linkage": { "type": "External" }
+      "linkage": { "binding": "Strong", "coalescing": "None", "definition": "Definition", "type": "External", "visibility": "Default" }
     },
     {
       "id": 0,
-      "linkage": { "type": "Internal" }
+      "linkage": { "binding": "Strong", "coalescing": "None", "definition": "Definition", "type": "Internal", "visibility": "Default" }
     }
   ],
   "data": [],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-id-not-uint64.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-id-not-uint64.json
index fde6f8b169cda..1544bb6fbfdd4 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-id-not-uint64.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-id-not-uint64.json
@@ -7,7 +7,7 @@
   "linkage_table": [
     {
       "id": "not_a_number",
-      "linkage": { "type": "External" }
+      "linkage": { "binding": "Strong", "coalescing": "None", "definition": "Definition", "type": "External", "visibility": "Default" }
     }
   ],
   "data": [],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-linkage-invalid-coalescing.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-linkage-invalid-coalescing.json
new file mode 100644
index 0000000000000..82a3f88ec952d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-linkage-invalid-coalescing.json
@@ -0,0 +1,16 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "test.cpp"
+  },
+  "id_table": [],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": { "type": "External", "binding": "Strong", "coalescing": "invalid_coalescing" }
+    }
+  ],
+  "data": [],
+  "target_triple": "arm64-apple-macosx",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-linkage-missing-coalescing.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-linkage-missing-coalescing.json
new file mode 100644
index 0000000000000..53ee3391e6e84
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-linkage-missing-coalescing.json
@@ -0,0 +1,16 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "test.cpp"
+  },
+  "id_table": [],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": { "type": "External", "binding": "Strong" }
+    }
+  ],
+  "data": [],
+  "target_triple": "arm64-apple-macosx",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-missing-id.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-missing-id.json
index 597db631bfd60..bde81305ab03c 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-missing-id.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-entry-missing-id.json
@@ -6,7 +6,7 @@
   "id_table": [],
   "linkage_table": [
     {
-      "linkage": { "type": "External" }
+      "linkage": { "binding": "Strong", "coalescing": "None", "definition": "Definition", "type": "External", "visibility": "Default" }
     }
   ],
   "data": [],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-extra-id.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-extra-id.json
index e0d15d64a0d26..c766e66af2653 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-extra-id.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/linkage-table-extra-id.json
@@ -7,7 +7,7 @@
   "linkage_table": [
     {
       "id": 0,
-      "linkage": { "type": "External" }
+      "linkage": { "binding": "Strong", "coalescing": "None", "definition": "Definition", "type": "External", "visibility": "Default" }
     }
   ],
   "data": [],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-external.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-external.json
index bb26bc0de603a..7d9e7a3bed850 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-external.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-external.json
@@ -13,7 +13,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-internal.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-internal.json
index f3aa427a9e239..d17cf2b3b1cb0 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-internal.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-internal.json
@@ -13,7 +13,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-multiple.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-multiple.json
index c22c932ad2882..7cf69c780eb61 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-multiple.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-multiple.json
@@ -27,19 +27,31 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-none.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-none.json
index 495890dc3cc44..677314dfc77ea 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-none.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-linkage-none.json
@@ -13,7 +13,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-two-summary-types.json b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-two-summary-types.json
index 080afccbdf482..83d57da97567c 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-two-summary-types.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/Inputs/rt-two-summary-types.json
@@ -173,31 +173,51 @@
     {
       "id": 0,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 3,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 4,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/linkage.test b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/linkage.test
index a4ec862ff5f86..8d2a707bbdbda 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/TUSummary/linkage.test
+++ b/clang/test/Analysis/Scalable/ssaf-format/TUSummary/linkage.test
@@ -28,6 +28,28 @@
 // LINKAGE-TABLE-ENTRY-LINKAGE-INVALID-TYPE-NEXT: reading EntityLinkageType from field 'type'
 // LINKAGE-TABLE-ENTRY-LINKAGE-INVALID-TYPE-NEXT: invalid EntityLinkageType value 'invalid_type' for field 'type'
 
+// 'coalescing' is required, like every other EntityLinkage field.
+// RUN: not clang-ssaf-format --type tu %S/Inputs/linkage-table-entry-linkage-missing-coalescing.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=LINKAGE-MISSING-COALESCING
+// RUN: not clang-ssaf-format --type tu --encoding %S/Inputs/linkage-table-entry-linkage-missing-coalescing.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=LINKAGE-MISSING-COALESCING
+// LINKAGE-MISSING-COALESCING:      clang-ssaf-format: error: reading TUSummary from file '{{.*}}linkage-table-entry-linkage-missing-coalescing.json'
+// LINKAGE-MISSING-COALESCING-NEXT: reading LinkageTable from field 'linkage_table'
+// LINKAGE-MISSING-COALESCING-NEXT: reading LinkageTable entry from index '0'
+// LINKAGE-MISSING-COALESCING-NEXT: reading EntityLinkage from field 'linkage'
+// LINKAGE-MISSING-COALESCING-NEXT: failed to read EntityCoalescing from field 'coalescing': expected JSON string
+
+// RUN: not clang-ssaf-format --type tu %S/Inputs/linkage-table-entry-linkage-invalid-coalescing.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=LINKAGE-INVALID-COALESCING
+// RUN: not clang-ssaf-format --type tu --encoding %S/Inputs/linkage-table-entry-linkage-invalid-coalescing.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=LINKAGE-INVALID-COALESCING
+// LINKAGE-INVALID-COALESCING:      clang-ssaf-format: error: reading TUSummary from file '{{.*}}linkage-table-entry-linkage-invalid-coalescing.json'
+// LINKAGE-INVALID-COALESCING-NEXT: reading LinkageTable from field 'linkage_table'
+// LINKAGE-INVALID-COALESCING-NEXT: reading LinkageTable entry from index '0'
+// LINKAGE-INVALID-COALESCING-NEXT: reading EntityLinkage from field 'linkage'
+// LINKAGE-INVALID-COALESCING-NEXT: reading EntityCoalescing from field 'coalescing'
+// LINKAGE-INVALID-COALESCING-NEXT: invalid EntityCoalescing value 'invalid_coalescing' for field 'coalescing'
+
 // ============================================================================
 // linkageTableEntryFromJSON() errors
 // ============================================================================
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-coff.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-coff.json
new file mode 100644
index 0000000000000..eaf5c1020ddcd
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-coff.json
@@ -0,0 +1,115 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "carry.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at local_fn"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at static_fn"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at inline_fn"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at weak_fn"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "suffix": "",
+        "usr": "c:@V at tentative_var"
+      }
+    },
+    {
+      "id": 5,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at extern_decl"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Hidden"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "ODR",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "binding": "Common",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 5,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Declaration",
+        "type": "External",
+        "visibility": "Default"
+      }
+    }
+  ],
+  "data": [],
+  "target_triple": "x86_64-pc-windows-msvc",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-elf.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-elf.json
new file mode 100644
index 0000000000000..579009a10a966
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-elf.json
@@ -0,0 +1,115 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "carry.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at local_fn"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at static_fn"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at inline_fn"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at weak_fn"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "suffix": "",
+        "usr": "c:@V at tentative_var"
+      }
+    },
+    {
+      "id": 5,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at extern_decl"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Hidden"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "ODR",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "binding": "Common",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 5,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Declaration",
+        "type": "External",
+        "visibility": "Default"
+      }
+    }
+  ],
+  "data": [],
+  "target_triple": "x86_64-unknown-linux-gnu",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-macho.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-macho.json
new file mode 100644
index 0000000000000..a9a7eedcdf7e2
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/carry-macho.json
@@ -0,0 +1,115 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "carry.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at local_fn"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at static_fn"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at inline_fn"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at weak_fn"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "suffix": "",
+        "usr": "c:@V at tentative_var"
+      }
+    },
+    {
+      "id": 5,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at extern_decl"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Hidden"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "ODR",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "binding": "Common",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 5,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Declaration",
+        "type": "External",
+        "visibility": "Default"
+      }
+    }
+  ],
+  "data": [],
+  "target_triple": "arm64-apple-macosx",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-coff.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-coff.json
new file mode 100644
index 0000000000000..24bbf21df44b9
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-coff.json
@@ -0,0 +1,139 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "join_lhs.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at def_vs_decl"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at decl_vs_def"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at weaker_incumbent"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at stronger_incumbent"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at equal_weak"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Declaration",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    }
+  ],
+  "data": [
+    {
+      "summary_name": "Analysis1",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        }
+      ]
+    }
+  ],
+  "target_triple": "x86_64-pc-windows-msvc",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-elf.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-elf.json
new file mode 100644
index 0000000000000..c014546d5ece4
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-elf.json
@@ -0,0 +1,139 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "join_lhs.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at def_vs_decl"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at decl_vs_def"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at weaker_incumbent"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at stronger_incumbent"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at equal_weak"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Declaration",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    }
+  ],
+  "data": [
+    {
+      "summary_name": "Analysis1",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        }
+      ]
+    }
+  ],
+  "target_triple": "x86_64-unknown-linux-gnu",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-macho.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-macho.json
new file mode 100644
index 0000000000000..d30ce015bd919
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-lhs-macho.json
@@ -0,0 +1,139 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "join_lhs.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at def_vs_decl"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at decl_vs_def"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at weaker_incumbent"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at stronger_incumbent"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at equal_weak"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Declaration",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    }
+  ],
+  "data": [
+    {
+      "summary_name": "Analysis1",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": []
+          }
+        }
+      ]
+    }
+  ],
+  "target_triple": "arm64-apple-macosx",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-coff.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-coff.json
new file mode 100644
index 0000000000000..4f0a23fe2ac94
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-coff.json
@@ -0,0 +1,139 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "join_rhs.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at def_vs_decl"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at decl_vs_def"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at weaker_incumbent"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at stronger_incumbent"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at equal_weak"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Declaration",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    }
+  ],
+  "data": [
+    {
+      "summary_name": "Analysis1",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        }
+      ]
+    }
+  ],
+  "target_triple": "x86_64-pc-windows-msvc",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-elf.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-elf.json
new file mode 100644
index 0000000000000..6b426e06dd99c
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-elf.json
@@ -0,0 +1,139 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "join_rhs.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at def_vs_decl"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at decl_vs_def"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at weaker_incumbent"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at stronger_incumbent"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at equal_weak"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Declaration",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    }
+  ],
+  "data": [
+    {
+      "summary_name": "Analysis1",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        }
+      ]
+    }
+  ],
+  "target_triple": "x86_64-unknown-linux-gnu",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-macho.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-macho.json
new file mode 100644
index 0000000000000..442e1dd56ee16
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/join-rhs-macho.json
@@ -0,0 +1,139 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "join_rhs.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at def_vs_decl"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at decl_vs_def"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at weaker_incumbent"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at stronger_incumbent"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at equal_weak"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Declaration",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    }
+  ],
+  "data": [
+    {
+      "summary_name": "Analysis1",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": []
+          }
+        }
+      ]
+    }
+  ],
+  "target_triple": "arm64-apple-macosx",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
index 0fe78af8c28bb..6d789111b5e3a 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
@@ -51,37 +51,61 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 3,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 4,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 5,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
index 48a7fbef570e6..35597bd43dc55 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
@@ -51,37 +51,61 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Declaration",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 3,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 4,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 5,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
index a66be3036e31f..d0e9b72ba0603 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
index 535cc18dc10d5..e41e73e7f7e29 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
index 1fd3d1aeb0ac9..2c236824219cc 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
@@ -16,7 +16,11 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-redefines-shared-ext.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-redefines-shared-ext.json
new file mode 100644
index 0000000000000..71c76bc0ac0f7
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-redefines-shared-ext.json
@@ -0,0 +1,43 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "tu3.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at shared_ext#"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    }
+  ],
+  "data": [
+    {
+      "summary_name": "Analysis1",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "call_count": 0,
+            "callees": []
+          }
+        }
+      ]
+    }
+  ],
+  "target_triple": "arm64-apple-macosx",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-lhs-elf.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-lhs-elf.json
new file mode 100644
index 0000000000000..21e968734f513
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-lhs-elf.json
@@ -0,0 +1,64 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "vis_lhs.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at hidden_vs_default"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at default_vs_hidden"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at both_hidden"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Hidden"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Hidden"
+      }
+    }
+  ],
+  "data": [],
+  "target_triple": "x86_64-unknown-linux-gnu",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-lhs-macho.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-lhs-macho.json
new file mode 100644
index 0000000000000..c163192018c63
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-lhs-macho.json
@@ -0,0 +1,64 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "vis_lhs.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at hidden_vs_default"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at default_vs_hidden"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at both_hidden"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Hidden"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Hidden"
+      }
+    }
+  ],
+  "data": [],
+  "target_triple": "arm64-apple-macosx",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-rhs-elf.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-rhs-elf.json
new file mode 100644
index 0000000000000..550532d8a24e3
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-rhs-elf.json
@@ -0,0 +1,64 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "vis_rhs.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at hidden_vs_default"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at default_vs_hidden"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at both_hidden"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Hidden"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Hidden"
+      }
+    }
+  ],
+  "data": [],
+  "target_triple": "x86_64-unknown-linux-gnu",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-rhs-macho.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-rhs-macho.json
new file mode 100644
index 0000000000000..055d416f01f87
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/vis-rhs-macho.json
@@ -0,0 +1,64 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "vis_rhs.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at hidden_vs_default"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at default_vs_hidden"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "suffix": "",
+        "usr": "c:@F at both_hidden"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Hidden"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "binding": "Weak",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Hidden"
+      }
+    }
+  ],
+  "data": [],
+  "target_triple": "arm64-apple-macosx",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libtwo.json b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libtwo.json
index 7c4d80720ec57..41bfda8079603 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libtwo.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libtwo.json
@@ -271,37 +271,61 @@
         {
           "id": 0,
           "linkage": {
-            "type": "External"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "External",
+            "visibility": "Default"
           }
         },
         {
           "id": 1,
           "linkage": {
-            "type": "Internal"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "Internal",
+            "visibility": "Default"
           }
         },
         {
           "id": 2,
           "linkage": {
-            "type": "None"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "None",
+            "visibility": "Default"
           }
         },
         {
           "id": 3,
           "linkage": {
-            "type": "External"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "External",
+            "visibility": "Default"
           }
         },
         {
           "id": 4,
           "linkage": {
-            "type": "Internal"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "Internal",
+            "visibility": "Default"
           }
         },
         {
           "id": 5,
           "linkage": {
-            "type": "None"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "None",
+            "visibility": "Default"
           }
         }
       ],
@@ -589,37 +613,61 @@
         {
           "id": 0,
           "linkage": {
-            "type": "External"
+            "binding": "Undefined",
+            "coalescing": "None",
+            "definition": "Declaration",
+            "type": "External",
+            "visibility": "Default"
           }
         },
         {
           "id": 1,
           "linkage": {
-            "type": "Internal"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "Internal",
+            "visibility": "Default"
           }
         },
         {
           "id": 2,
           "linkage": {
-            "type": "None"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "None",
+            "visibility": "Default"
           }
         },
         {
           "id": 3,
           "linkage": {
-            "type": "External"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "External",
+            "visibility": "Default"
           }
         },
         {
           "id": 4,
           "linkage": {
-            "type": "Internal"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "Internal",
+            "visibility": "Default"
           }
         },
         {
           "id": 5,
           "linkage": {
-            "type": "None"
+            "binding": "Strong",
+            "coalescing": "None",
+            "definition": "Definition",
+            "type": "None",
+            "visibility": "Default"
           }
         }
       ],
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json
index 663f5dd814534..1ffa8c39d1ed7 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json
@@ -579,67 +579,111 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 3,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 4,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 5,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 6,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 7,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 8,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 9,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 10,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json
index 4fd99f978148b..b39558d8ab167 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json
@@ -321,37 +321,61 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 3,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 4,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 5,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json
index e7fd3f39da0c1..40d4eb1fd389e 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json
@@ -327,37 +327,61 @@
     {
       "id": 0,
       "linkage": {
-        "type": "External"
+        "binding": "Undefined",
+        "coalescing": "None",
+        "definition": "Declaration",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 1,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 2,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     },
     {
       "id": 3,
       "linkage": {
-        "type": "External"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "External",
+        "visibility": "Default"
       }
     },
     {
       "id": 4,
       "linkage": {
-        "type": "Internal"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "Internal",
+        "visibility": "Default"
       }
     },
     {
       "id": 5,
       "linkage": {
-        "type": "None"
+        "binding": "Strong",
+        "coalescing": "None",
+        "definition": "Definition",
+        "type": "None",
+        "visibility": "Default"
       }
     }
   ],
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/help.test b/clang/test/Analysis/Scalable/ssaf-linker/help.test
index 7b0c21f066016..d4d1c4957c824 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/help.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/help.test
@@ -21,8 +21,17 @@
 // CHECK-NEXT:   --help-list         - Display list of available options (--help-list-hidden for more)
 // CHECK-NEXT:   --help-list-hidden  - Display list of all available options
 // CHECK-NEXT:   -o <path>           - Output file path
+// CHECK-NEXT:   --odr-mismatch=<value> - How to report summary data that differs between two definitions required to be identical, such as an inline function
+// CHECK-NEXT:     =ignore           -   Do not check (default: comparing costs an extra patch of every displaced encoding)
+// CHECK-NEXT:     =warn             -   Report each mismatch and keep linking
+// CHECK-NEXT:     =error            -   Fail the link on the first mismatch
 // CHECK-NEXT:   --print-all-options - Print all option values after command line parsing
 // CHECK-NEXT:   --print-options     - Print non-default options after command line parsing
 // CHECK-NEXT:   --time              - Enable timing
+// CHECK-NEXT:   --unresolved-symbols=<value> - How to report entities that no linked translation unit defines
+// CHECK-NEXT:     =ignore           -   Do not check (default: a link unit is usually an intermediate artifact, where references to other link units are expected)
+// CHECK-NEXT:     =warn             -   Report each undefined entity and keep linking
+// CHECK-NEXT:     =error            -   Fail the link if any entity is left undefined
 // CHECK-NEXT:   --verbose           - Enable verbose output
 // CHECK-NEXT:   --version           - Display the version of this program
+// CHECK-NEXT:   --warn-on-multiple-definitions - Warn instead of failing when an external entity is defined by more than one translation unit
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linkage-carry-through.test b/clang/test/Analysis/Scalable/ssaf-linker/linkage-carry-through.test
new file mode 100644
index 0000000000000..293cc8025aa32
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linkage-carry-through.test
@@ -0,0 +1,70 @@
+// Verifies that every linkage value survives read -> link -> write, and that
+// values a target cannot express are coerced.
+//
+// Fixtures are per-target because the link unit's triple selects the
+// resolution rules; see docs/ssaf-linker-{elf,macho,coff}-behavior.md.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// The id_table is emitted first and sorted by USR, which fixes the ids the
+// linkage_table then uses:
+//
+//   0 extern_decl   1 inline_fn   2 local_fn
+//   3 static_fn     4 weak_fn     5 tentative_var
+
+// ELF and Mach-O carry every value through untouched.
+// RUN: clang-ssaf-linker %S/Inputs/carry-elf.json -o %t/carry-elf.json
+// RUN: cat %t/carry-elf.json | FileCheck %s --check-prefix=KEEPS-HIDDEN
+// RUN: clang-ssaf-linker %S/Inputs/carry-macho.json -o %t/carry-macho.json
+// RUN: cat %t/carry-macho.json | FileCheck %s --check-prefix=KEEPS-HIDDEN
+
+// KEEPS-HIDDEN:      "linkage_table": [
+
+// extern_decl: a declaration defines nothing and coalesces nothing.
+// KEEPS-HIDDEN:      "id": 0,
+// KEEPS-HIDDEN-NEXT: "linkage": {
+// KEEPS-HIDDEN-NEXT: "binding": "Undefined",
+// KEEPS-HIDDEN-NEXT: "coalescing": "None",
+// KEEPS-HIDDEN-NEXT: "definition": "Declaration",
+
+// inline_fn: strong at the source level, plus the ODR guarantee. The two are
+// independent axes, which is why the binding is not "Weak" here even though
+// ELF and Mach-O both emit a weak symbol for it.
+// KEEPS-HIDDEN:      "id": 1,
+// KEEPS-HIDDEN-NEXT: "linkage": {
+// KEEPS-HIDDEN-NEXT: "binding": "Strong",
+// KEEPS-HIDDEN-NEXT: "coalescing": "ODR",
+// KEEPS-HIDDEN-NEXT: "definition": "Definition",
+// KEEPS-HIDDEN-NEXT: "type": "External",
+// KEEPS-HIDDEN-NEXT: "visibility": "Default"
+
+// static_fn: internal linkage keeps its Hidden visibility.
+// KEEPS-HIDDEN:      "id": 3,
+// KEEPS-HIDDEN-NEXT: "linkage": {
+// KEEPS-HIDDEN-NEXT: "binding": "Strong",
+// KEEPS-HIDDEN-NEXT: "coalescing": "None",
+// KEEPS-HIDDEN-NEXT: "definition": "Definition",
+// KEEPS-HIDDEN-NEXT: "type": "Internal",
+// KEEPS-HIDDEN-NEXT: "visibility": "Hidden"
+
+// tentative_var: a C tentative definition is a common symbol.
+// KEEPS-HIDDEN:      "id": 5,
+// KEEPS-HIDDEN-NEXT: "linkage": {
+// KEEPS-HIDDEN-NEXT: "binding": "Common",
+// KEEPS-HIDDEN-NEXT: "coalescing": "None",
+// KEEPS-HIDDEN-NEXT: "definition": "Definition",
+
+// COFF has no visibility at all: clang accepts the attribute silently and
+// drops it at emission, so the linker coerces it rather than rejecting a
+// summary that portable source legitimately produces (COFF doc §7).
+// RUN: clang-ssaf-linker %S/Inputs/carry-coff.json -o %t/carry-coff.json
+// RUN: cat %t/carry-coff.json | FileCheck %s --check-prefix=COERCES-HIDDEN
+// COERCES-HIDDEN:      "linkage_table": [
+// COERCES-HIDDEN:      "id": 3,
+// COERCES-HIDDEN-NEXT: "linkage": {
+// COERCES-HIDDEN-NEXT: "binding": "Strong",
+// COERCES-HIDDEN-NEXT: "coalescing": "None",
+// COERCES-HIDDEN-NEXT: "definition": "Definition",
+// COERCES-HIDDEN-NEXT: "type": "Internal",
+// COERCES-HIDDEN-NEXT: "visibility": "Default"
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linkage-join-visibility.test b/clang/test/Analysis/Scalable/ssaf-linker/linkage-join-visibility.test
new file mode 100644
index 0000000000000..5e5aa1c1ed1fb
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linkage-join-visibility.test
@@ -0,0 +1,62 @@
+// Verifies the visibility merge, which is the rule where ELF and Mach-O take
+// opposite directions, and the hidden-blocks-cross-link-unit-resolution
+// behaviour that follows from it.
+//
+// Each fixture pairs two weak definitions of the same entity so that the merge
+// is exercised without triggering a multiple-definition error. Both bindings
+// are weak, so neither displaces the other and only the visibilities differ.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// ELF merges to the MOST restrictive visibility: a hidden occurrence makes the
+// merged entity hidden even when the other is default, and even when the
+// hidden one is only a declaration (ELF doc §4 probe V1).
+// RUN: clang-ssaf-linker %S/Inputs/vis-lhs-elf.json %S/Inputs/vis-rhs-elf.json \
+// RUN:   -o %t/vis-elf.json
+// RUN: cat %t/vis-elf.json | FileCheck %s --check-prefix=ELF
+
+// The id_table is sorted by USR, fixing the ids:
+//   0 both_hidden   1 default_vs_hidden   2 hidden_vs_default
+//
+// All three end up hidden, and are therefore demoted to Internal.
+// ELF:      "linkage_table": [
+// ELF:      "id": 0,
+// ELF:      "linkage": {
+// ELF:      "type": "Internal",
+// ELF-NEXT: "visibility": "Hidden"
+// ELF:      "id": 1,
+// ELF:      "linkage": {
+// ELF:      "type": "Internal",
+// ELF-NEXT: "visibility": "Hidden"
+// ELF:      "id": 2,
+// ELF:      "linkage": {
+// ELF:      "type": "Internal",
+// ELF-NEXT: "visibility": "Hidden"
+
+// Mach-O merges to the LEAST restrictive: a symbol stays private only when
+// every copy is private (Mach-O doc §2 probes W1-W3).
+// RUN: clang-ssaf-linker %S/Inputs/vis-lhs-macho.json \
+// RUN:   %S/Inputs/vis-rhs-macho.json -o %t/vis-macho.json
+// RUN: cat %t/vis-macho.json | FileCheck %s --check-prefix=MACHO
+
+// The mixed pairs stay exported; only both_hidden (id 0) is demoted.
+// MACHO:      "linkage_table": [
+// MACHO:      "id": 0,
+// MACHO:      "linkage": {
+// MACHO:      "type": "Internal",
+// MACHO-NEXT: "visibility": "Hidden"
+// MACHO:      "id": 1,
+// MACHO:      "linkage": {
+// MACHO:      "type": "External",
+// MACHO-NEXT: "visibility": "Default"
+// MACHO:      "id": 2,
+// MACHO:      "linkage": {
+// MACHO:      "type": "External",
+// MACHO-NEXT: "visibility": "Default"
+
+// A hidden entity is demoted to Internal so that a later link stage cannot
+// resolve against it, but it keeps its name: every EntityId already patched
+// into the summary data refers to it.
+// RUN: cat %t/vis-elf.json | FileCheck %s --check-prefix=KEEPS-NAME
+// KEEPS-NAME: "usr": "c:@F at both_hidden"
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linkage-join-winner.test b/clang/test/Analysis/Scalable/ssaf-linker/linkage-join-winner.test
new file mode 100644
index 0000000000000..1adf65168e778
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linkage-join-winner.test
@@ -0,0 +1,74 @@
+// Verifies which occurrence's summary data survives when two TUs define the
+// same external entity, end to end through the linker.
+//
+// The `call_count` in each entity's summary is 1 in the left-hand TU and 2 in
+// the right-hand one, so the linked value names the winner. The unit tests in
+// EntityLinkerReconcileTest cover the rules themselves; this checks that the
+// decision actually reaches the output file.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// ELF and Mach-O agree on every case here. --warn-on-multiple-definitions is
+// needed only because COFF rejects the two-weak-definitions case (COFF doc §1
+// probe C4), and the same command line is used for all three targets.
+
+// RUN: clang-ssaf-linker --warn-on-multiple-definitions \
+// RUN:   %S/Inputs/join-lhs-elf.json %S/Inputs/join-rhs-elf.json \
+// RUN:   -o %t/join-elf.json 2>&1 | FileCheck %s --check-prefix=NO-CONFLICT \
+// RUN:   --allow-empty
+// RUN: cat %t/join-elf.json | FileCheck %s
+
+// RUN: clang-ssaf-linker --warn-on-multiple-definitions \
+// RUN:   %S/Inputs/join-lhs-macho.json %S/Inputs/join-rhs-macho.json \
+// RUN:   -o %t/join-macho.json 2>&1 | FileCheck %s --check-prefix=NO-CONFLICT \
+// RUN:   --allow-empty
+// RUN: cat %t/join-macho.json | FileCheck %s
+
+// ELF and Mach-O accept two weak definitions silently.
+// NO-CONFLICT-NOT: multiple definition
+
+// The id_table is sorted by USR, which fixes the entity_ids the summary data
+// then uses:
+//
+//   0 decl_vs_def   1 def_vs_decl   2 equal_weak
+//   3 stronger_incumbent            4 weaker_incumbent
+
+// A definition beats a declaration, whichever side it is on. decl_vs_def is a
+// declaration on the left and a definition on the right, so the right wins;
+// def_vs_decl is the mirror image.
+// CHECK:      "entity_id": 0,
+// CHECK-NEXT: "entity_summary": {
+// CHECK-NEXT: "call_count": 2
+// CHECK:      "entity_id": 1,
+// CHECK-NEXT: "entity_summary": {
+// CHECK-NEXT: "call_count": 1
+
+// A tie keeps the data already linked (ELF probe P4, Mach-O probe M4).
+// CHECK:      "entity_id": 2,
+// CHECK-NEXT: "entity_summary": {
+// CHECK-NEXT: "call_count": 1
+
+// Among definitions the stronger binding wins, regardless of link order:
+// stronger_incumbent is strong on the left, weaker_incumbent on the right.
+// CHECK:      "entity_id": 3,
+// CHECK-NEXT: "entity_summary": {
+// CHECK-NEXT: "call_count": 1
+// CHECK:      "entity_id": 4,
+// CHECK-NEXT: "entity_summary": {
+// CHECK-NEXT: "call_count": 2
+
+// COFF reaches the same data selection, but rejects the two weak definitions
+// unless the multiple-definition error is downgraded, because it emulates weak
+// symbols with an alias that collides (COFF doc §1 probe C4).
+// RUN: clang-ssaf-linker --warn-on-multiple-definitions \
+// RUN:   %S/Inputs/join-lhs-coff.json %S/Inputs/join-rhs-coff.json \
+// RUN:   -o %t/join-coff.json 2>&1 | FileCheck %s --check-prefix=COFF-WEAK
+// RUN: cat %t/join-coff.json | FileCheck %s
+// COFF-WEAK: warning: multiple definition of EntityName(c:@F at equal_weak,
+
+// RUN: not clang-ssaf-linker \
+// RUN:   %S/Inputs/join-lhs-coff.json %S/Inputs/join-rhs-coff.json \
+// RUN:   -o %t/join-coff-strict.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=COFF-WEAK-FATAL
+// COFF-WEAK-FATAL: multiple definition of EntityName(c:@F at equal_weak,
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
index 5bda8128ac0e1..144e891106ae5 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
@@ -26,3 +26,8 @@
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=INVALID-ID-REF
 // INVALID-ID-REF:      clang-ssaf-linker: error: Linking summary '{{.*}}tu-invalid-entity-id-ref.json'
 // INVALID-ID-REF-NEXT: failed to patch EntityId: 'EntityId(99)' not found in entity resolution table
+
+// Two TUs defining the same external entity is a multiple definition error.
+// RUN: not clang-ssaf-linker %S/Inputs/tu-1.json %S/Inputs/tu-redefines-shared-ext.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=MULTIPLE-DEF
+// MULTIPLE-DEF: LLVM ERROR: multiple definition of EntityName(c:@F at shared_ext#, , NestedBuildNamespace([BuildNamespace(LinkUnit, lu)])) in NestedBuildNamespace([BuildNamespace(CompilationUnit, tu3.cpp)])
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linking.test b/clang/test/Analysis/Scalable/ssaf-linker/linking.test
index a6bd0c8df22c8..ed5b4d71b8fbb 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/linking.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linking.test
@@ -29,6 +29,16 @@
 // RUN: diff %S/Outputs/lu-1+2.json %t/lu-1+2.json
 // RUN: rm %t/lu-1+2.json
 
+// --warn-on-multiple-definitions downgrades a multiple definition (rejected by
+// default, see linking-errors.test) to a warning and keeps the first
+// definition's data.
+// RUN: clang-ssaf-linker --warn-on-multiple-definitions %S/Inputs/tu-1.json \
+// RUN:   %S/Inputs/tu-redefines-shared-ext.json -o %t/lu-1.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=MULTIPLE-DEF
+// MULTIPLE-DEF: warning: multiple definition of EntityName(c:@F at shared_ext#, , NestedBuildNamespace([BuildNamespace(LinkUnit, lu-1)])) in NestedBuildNamespace([BuildNamespace(CompilationUnit, tu3.cpp)])
+// RUN: diff %S/Outputs/lu-1.json %t/lu-1.json
+// RUN: rm %t/lu-1.json
+
 // Relative input path: cd to the test source directory so that
 // Inputs/tu-1.json is a valid relative path.
 // RUN: cd %S && clang-ssaf-linker Inputs/tu-1.json -o %t/lu-1.json
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/verbose.test b/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
index d731748ad7547..500300f607b8d 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
@@ -5,16 +5,19 @@
 
 // RUN: clang-ssaf-linker --verbose %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json 2>&1 \
 // RUN:           | FileCheck %s --match-full-lines
+// Every summary is read before the linker is constructed, because the link
+// unit's target triple comes from its inputs and selects the resolution rules.
 // CHECK:      note: - Linking started.
 // CHECK-NEXT: note:   - Validating input.
 // CHECK-NEXT: note:     - Validated output summary path '{{.*}}lu-1+2.json'.
 // CHECK-NEXT: note:     - Validated 2 input summary paths.
 // CHECK-NEXT: note:   - Linking input.
+// CHECK-NEXT: note:       - [1/2] Reading '{{.*}}tu-1.json'.
+// CHECK-NEXT: note:       - [2/2] Reading '{{.*}}tu-2.json'.
 // CHECK-NEXT: note:     - Constructing linker.
+// CHECK-NEXT: note:       - Link unit targets 'arm64-apple-macosx'.
 // CHECK-NEXT: note:     - Linking summaries.
-// CHECK-NEXT: note:       - [1/2] Reading '{{.*}}tu-1.json'.
 // CHECK-NEXT: note:       - [1/2] Linking '{{.*}}tu-1.json'.
-// CHECK-NEXT: note:       - [2/2] Reading '{{.*}}tu-2.json'.
 // CHECK-NEXT: note:       - [2/2] Linking '{{.*}}tu-2.json'.
 // CHECK-NEXT: note:     - Writing output summary to '{{.*}}lu-1+2.json'.
 // CHECK-NEXT: note: - Linking finished.
diff --git a/clang/tools/clang-ssaf-linker/SSAFLinker.cpp b/clang/tools/clang-ssaf-linker/SSAFLinker.cpp
index 9d72e9f338328..0914ba20b6ef6 100644
--- a/clang/tools/clang-ssaf-linker/SSAFLinker.cpp
+++ b/clang/tools/clang-ssaf-linker/SSAFLinker.cpp
@@ -77,6 +77,42 @@ cl::opt<bool> Time("time", cl::desc("Enable timing"), cl::init(false),
                    cl::sub(cl::SubCommand::getTopLevel()),
                    cl::sub(StaticLibraryCmd), cl::sub(MultiArchCmd));
 
+cl::opt<bool> WarnOnMultipleDefinitions(
+    "warn-on-multiple-definitions",
+    cl::desc("Warn instead of failing when an external entity is defined by "
+             "more than one translation unit"),
+    cl::init(false), cl::cat(SsafLinkerCategory),
+    cl::sub(cl::SubCommand::getTopLevel()));
+
+cl::opt<UnresolvedPolicy> UnresolvedSymbols(
+    "unresolved-symbols",
+    cl::desc("How to report entities that no linked translation unit defines"),
+    cl::init(UnresolvedPolicy::Ignore), cl::cat(SsafLinkerCategory),
+    cl::sub(cl::SubCommand::getTopLevel()),
+    cl::values(
+        clEnumValN(UnresolvedPolicy::Ignore, "ignore",
+                   "Do not check (default: a link unit is usually an "
+                   "intermediate artifact, where references to other link "
+                   "units are expected)"),
+        clEnumValN(UnresolvedPolicy::Warn, "warn",
+                   "Report each undefined entity and keep linking"),
+        clEnumValN(UnresolvedPolicy::Error, "error",
+                   "Fail the link if any entity is left undefined")));
+
+cl::opt<ODRMismatchPolicy> ODRMismatch(
+    "odr-mismatch",
+    cl::desc("How to report summary data that differs between two definitions "
+             "required to be identical, such as an inline function"),
+    cl::init(ODRMismatchPolicy::Ignore), cl::cat(SsafLinkerCategory),
+    cl::sub(cl::SubCommand::getTopLevel()),
+    cl::values(clEnumValN(ODRMismatchPolicy::Ignore, "ignore",
+                          "Do not check (default: comparing costs an extra "
+                          "patch of every displaced encoding)"),
+               clEnumValN(ODRMismatchPolicy::Warn, "warn",
+                          "Report each mismatch and keep linking"),
+               clEnumValN(ODRMismatchPolicy::Error, "error",
+                          "Fail the link on the first mismatch")));
+
 // The `static-library` subcommand's verb positional. Declared BEFORE
 // StaticLibraryInputs so cl-lib binds argv[0] under the subcommand to the
 // verb rather than to the greedy input list.
@@ -207,50 +243,55 @@ void runLink(llvm::TimerGroup &TG) {
   }
 
   info(Verbose, 1, "Linking input.");
-  info(Verbose, 2, "Constructing linker.");
-
-  // TODO: The linker currently uses a hardcoded target triple. Architecture
-  // tracking in the linker will be handled properly in a separate PR.
-  EntityLinker EL(llvm::Triple("arm64-apple-macosx"),
-                  NestedBuildNamespace(BuildNamespace(
-                      BuildNamespaceKind::LinkUnit, LI.LinkUnitName)));
 
   llvm::Timer TRead("read", "Read Summaries", TG);
   llvm::Timer TLink("link", "Link Summaries", TG);
   llvm::Timer TWrite("write", "Write Summary", TG);
 
-  info(Verbose, 2, "Linking summaries.");
-
+  // Read every summary before constructing the linker: the link unit's target
+  // triple comes from its inputs, and it selects the resolution rules to
+  // emulate. EntityLinker rejects any later input that disagrees.
+  std::vector<std::unique_ptr<TUSummaryEncoding>> Summaries;
   for (auto [Index, InputFile] : llvm::enumerate(LI.InputFiles)) {
-    std::unique_ptr<TUSummaryEncoding> Summary;
+    info(Verbose, 3, "[{0}/{1}] Reading '{2}'.", (Index + 1),
+         LI.InputFiles.size(), InputFile.Path);
 
-    {
-      info(Verbose, 3, "[{0}/{1}] Reading '{2}'.", (Index + 1),
-           LI.InputFiles.size(), InputFile.Path);
+    llvm::TimeRegion _(Time ? &TRead : nullptr);
 
-      llvm::TimeRegion _(Time ? &TRead : nullptr);
+    auto ExpectedSummaryEncoding =
+        InputFile.Format->readTUSummaryEncoding(InputFile.Path);
+    if (!ExpectedSummaryEncoding) {
+      fail(ExpectedSummaryEncoding.takeError());
+    }
 
-      auto ExpectedSummaryEncoding =
-          InputFile.Format->readTUSummaryEncoding(InputFile.Path);
-      if (!ExpectedSummaryEncoding) {
-        fail(ExpectedSummaryEncoding.takeError());
-      }
+    Summaries.push_back(std::make_unique<TUSummaryEncoding>(
+        std::move(*ExpectedSummaryEncoding)));
+  }
 
-      Summary = std::make_unique<TUSummaryEncoding>(
-          std::move(*ExpectedSummaryEncoding));
-    }
+  info(Verbose, 2, "Constructing linker.");
+
+  // cl::OneOrMore on the input list guarantees at least one summary.
+  const llvm::Triple &TargetTriple = Summaries.front()->getTargetTriple();
+  info(Verbose, 3, "Link unit targets '{0}'.", TargetTriple.str());
+
+  EntityLinker EL(TargetTriple,
+                  NestedBuildNamespace(BuildNamespace(
+                      BuildNamespaceKind::LinkUnit, LI.LinkUnitName)),
+                  WarnOnMultipleDefinitions, UnresolvedSymbols, ODRMismatch);
+
+  info(Verbose, 2, "Linking summaries.");
 
-    {
-      info(Verbose, 3, "[{0}/{1}] Linking '{2}'.", (Index + 1),
-           LI.InputFiles.size(), InputFile.Path);
+  for (auto [Index, Summary] : llvm::enumerate(Summaries)) {
+    info(Verbose, 3, "[{0}/{1}] Linking '{2}'.", (Index + 1),
+         LI.InputFiles.size(), LI.InputFiles[Index].Path);
 
-      llvm::TimeRegion _(Time ? &TLink : nullptr);
+    llvm::TimeRegion _(Time ? &TLink : nullptr);
 
-      if (auto Err = EL.link(std::move(Summary))) {
-        fail(ErrorBuilder::wrap(std::move(Err))
-                 .context(LocalErrorMessages::LinkingSummary, InputFile.Path)
-                 .build());
-      }
+    if (auto Err = EL.link(std::move(Summary))) {
+      fail(ErrorBuilder::wrap(std::move(Err))
+               .context(LocalErrorMessages::LinkingSummary,
+                        LI.InputFiles[Index].Path)
+               .build());
     }
   }
 
diff --git a/clang/unittests/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowWPATest.cpp b/clang/unittests/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowWPATest.cpp
index 8c43ead9750ca..ca37e8a9ce423 100644
--- a/clang/unittests/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowWPATest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowWPATest.cpp
@@ -33,8 +33,11 @@ namespace {
 
 class PointerFlowWPATest : public TestFixture {
 protected:
-  static constexpr EntityLinkage ExternalLinkage =
-      EntityLinkage(EntityLinkageType::External);
+  static constexpr EntityLinkage ExternalLinkage = EntityLinkage(
+      EntityLinkageType::External, clang::ssaf::EntityBinding::Strong,
+      clang::ssaf::EntityCoalescing::None,
+      clang::ssaf::EntityVisibility::Default,
+      clang::ssaf::EntityDefinitionKind::Definition);
 
   std::unique_ptr<LUSummary> makeLUSummary() {
     NestedBuildNamespace NS(
diff --git a/clang/unittests/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageWPATest.cpp b/clang/unittests/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageWPATest.cpp
index 448f838297e8f..24ac2ac3fc51d 100644
--- a/clang/unittests/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageWPATest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageWPATest.cpp
@@ -34,8 +34,11 @@ namespace {
 
 class UnsafeBufferUsageWPATest : public TestFixture {
 protected:
-  static constexpr EntityLinkage ExternalLinkage =
-      EntityLinkage(EntityLinkageType::External);
+  static constexpr EntityLinkage ExternalLinkage = EntityLinkage(
+      EntityLinkageType::External, clang::ssaf::EntityBinding::Strong,
+      clang::ssaf::EntityCoalescing::None,
+      clang::ssaf::EntityVisibility::Default,
+      clang::ssaf::EntityDefinitionKind::Definition);
 
   std::unique_ptr<LUSummary> makeLUSummary() {
     NestedBuildNamespace NS(
diff --git a/clang/unittests/ScalableStaticAnalysis/EntityLinkageTest.cpp b/clang/unittests/ScalableStaticAnalysis/EntityLinkageTest.cpp
index ccf38ec8d93fb..ed477fa6e6498 100644
--- a/clang/unittests/ScalableStaticAnalysis/EntityLinkageTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/EntityLinkageTest.cpp
@@ -12,8 +12,12 @@
 #include "llvm/Support/raw_ostream.h"
 #include "gtest/gtest.h"
 
+using clang::ssaf::EntityBinding;
+using clang::ssaf::EntityCoalescing;
+using clang::ssaf::EntityDefinitionKind;
 using clang::ssaf::EntityLinkage;
 using clang::ssaf::EntityLinkageType;
+using clang::ssaf::EntityVisibility;
 
 namespace {
 
@@ -21,44 +25,68 @@ constexpr inline auto None = EntityLinkageType::None;
 constexpr inline auto Internal = EntityLinkageType::Internal;
 constexpr inline auto External = EntityLinkageType::External;
 
-TEST(EntityLinkageTest, Constructor) {
-  EntityLinkage NoneLinkage(None);
-  EntityLinkage InternalLinkage(Internal);
-  EntityLinkage ExternalLinkage(External);
+// Builds an EntityLinkage with the given linkage type and fixed defaults for
+// the remaining properties (an ordinary strong external definition).
+constexpr EntityLinkage mk(EntityLinkageType LT) {
+  return EntityLinkage(LT, EntityBinding::Strong, EntityCoalescing::None,
+                       EntityVisibility::Default,
+                       EntityDefinitionKind::Definition);
+}
 
-  EXPECT_EQ(NoneLinkage.getLinkage(), None);
-  EXPECT_EQ(InternalLinkage.getLinkage(), Internal);
-  EXPECT_EQ(ExternalLinkage.getLinkage(), External);
+TEST(EntityLinkageTest, Constructor) {
+  EntityLinkage L(External, EntityBinding::Weak, EntityCoalescing::ODR,
+                  EntityVisibility::Hidden, EntityDefinitionKind::Declaration);
+  EXPECT_EQ(L.getLinkage(), External);
 }
 
 TEST(EntityLinkageTest, CopyConstructor) {
-  EntityLinkage Original(External);
+  EntityLinkage Original = mk(External);
   EntityLinkage Copy = Original;
 
-  EXPECT_EQ(Copy.getLinkage(), External);
-  EXPECT_EQ(Copy.getLinkage(), Original.getLinkage());
+  EXPECT_EQ(Copy, Original);
 }
 
 TEST(EntityLinkageTest, AssignmentOperator) {
-  EntityLinkage Linkage1(None);
-  EntityLinkage Linkage2(External);
+  EntityLinkage Linkage1 = mk(None);
+  EntityLinkage Linkage2 = mk(External);
 
   Linkage1 = Linkage2;
 
-  EXPECT_EQ(Linkage1.getLinkage(), External);
-  EXPECT_EQ(Linkage1.getLinkage(), Linkage2.getLinkage());
+  EXPECT_EQ(Linkage1, Linkage2);
 }
 
 TEST(EntityLinkageTest, EqualityOperatorReflexive) {
-  EXPECT_EQ(EntityLinkage(None), EntityLinkage(None));
-  EXPECT_EQ(EntityLinkage(Internal), EntityLinkage(Internal));
-  EXPECT_EQ(EntityLinkage(External), EntityLinkage(External));
+  EXPECT_EQ(mk(None), mk(None));
+  EXPECT_EQ(mk(Internal), mk(Internal));
+  EXPECT_EQ(mk(External), mk(External));
 }
 
 TEST(EntityLinkageTest, EqualityOperatorDistinct) {
-  EXPECT_NE(EntityLinkage(None), EntityLinkage(Internal));
-  EXPECT_NE(EntityLinkage(None), EntityLinkage(External));
-  EXPECT_NE(EntityLinkage(Internal), EntityLinkage(External));
+  EXPECT_NE(mk(None), mk(Internal));
+  EXPECT_NE(mk(None), mk(External));
+  EXPECT_NE(mk(Internal), mk(External));
+}
+
+TEST(EntityLinkageTest, EqualityConsidersAllProperties) {
+  const EntityLinkage Base(External, EntityBinding::Strong,
+                           EntityCoalescing::None, EntityVisibility::Default,
+                           EntityDefinitionKind::Definition);
+  EXPECT_NE(Base,
+            EntityLinkage(External, EntityBinding::Weak, EntityCoalescing::None,
+                          EntityVisibility::Default,
+                          EntityDefinitionKind::Definition));
+  EXPECT_NE(Base,
+            EntityLinkage(External, EntityBinding::Strong,
+                          EntityCoalescing::ODR, EntityVisibility::Default,
+                          EntityDefinitionKind::Definition));
+  EXPECT_NE(Base,
+            EntityLinkage(External, EntityBinding::Strong,
+                          EntityCoalescing::None, EntityVisibility::Hidden,
+                          EntityDefinitionKind::Definition));
+  EXPECT_NE(Base,
+            EntityLinkage(External, EntityBinding::Strong,
+                          EntityCoalescing::None, EntityVisibility::Default,
+                          EntityDefinitionKind::Declaration));
 }
 
 TEST(EntityLinkageTypeTest, FormatProvider) {
@@ -88,30 +116,16 @@ TEST(EntityLinkageTypeTest, StreamOutputExternal) {
 }
 
 TEST(EntityLinkageTest, FormatProvider) {
-  EXPECT_EQ(llvm::formatv("{0}", EntityLinkage(None)).str(),
-            "EntityLinkage(None)");
-  EXPECT_EQ(llvm::formatv("{0}", EntityLinkage(Internal)).str(),
-            "EntityLinkage(Internal)");
-  EXPECT_EQ(llvm::formatv("{0}", EntityLinkage(External)).str(),
-            "EntityLinkage(External)");
-}
-
-TEST(EntityLinkageTest, StreamOutputNone) {
-  std::string S;
-  llvm::raw_string_ostream(S) << EntityLinkage(None);
-  EXPECT_EQ(S, "EntityLinkage(None)");
-}
-
-TEST(EntityLinkageTest, StreamOutputInternal) {
-  std::string S;
-  llvm::raw_string_ostream(S) << EntityLinkage(Internal);
-  EXPECT_EQ(S, "EntityLinkage(Internal)");
+  EXPECT_EQ(llvm::formatv("{0}", mk(None)).str(),
+            "EntityLinkage(None, Strong, None, Default, Definition)");
 }
 
-TEST(EntityLinkageTest, StreamOutputExternal) {
+TEST(EntityLinkageTest, StreamOutput) {
   std::string S;
-  llvm::raw_string_ostream(S) << EntityLinkage(External);
-  EXPECT_EQ(S, "EntityLinkage(External)");
+  llvm::raw_string_ostream(S) << EntityLinkage(
+      External, EntityBinding::Weak, EntityCoalescing::ODR,
+      EntityVisibility::Hidden, EntityDefinitionKind::Declaration);
+  EXPECT_EQ(S, "EntityLinkage(External, Weak, ODR, Hidden, Declaration)");
 }
 
 } // namespace
diff --git a/clang/unittests/ScalableStaticAnalysis/EntityLinkerTest.cpp b/clang/unittests/ScalableStaticAnalysis/EntityLinkerTest.cpp
index cd579e16991ef..a77887785e1f9 100644
--- a/clang/unittests/ScalableStaticAnalysis/EntityLinkerTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/EntityLinkerTest.cpp
@@ -32,37 +32,56 @@ namespace {
 
 class MockEntitySummaryEncoding : public EntitySummaryEncoding {
 public:
-  MockEntitySummaryEncoding() : Id(++Index) {}
+  /// \param Payload Distinguishes encodings for the ODR mismatch check; two
+  ///        mocks compare equal iff their payloads match.
+  explicit MockEntitySummaryEncoding(int Payload = 0)
+      : Id(++Index), Payload(Payload) {}
 
   size_t getId() const { return Id; }
 
-  llvm::Error
-  patch(const std::map<EntityId, EntityId> &EntityResolutionTable) override {
-    PatchedIds = EntityResolutionTable;
+  llvm::Error patch(const EntityResolutionMap &Resolution) override {
+    PatchedIds = Resolution;
     return llvm::Error::success();
   }
 
-  const std::map<EntityId, EntityId> &getPatchedIds() const {
-    return PatchedIds;
+  const void *getEncodingKind() const override { return &Kind; }
+
+  bool equals(const EntitySummaryEncoding &Other) const override {
+    if (Other.getEncodingKind() != getEncodingKind()) {
+      return false;
+    }
+    return Payload ==
+           static_cast<const MockEntitySummaryEncoding &>(Other).Payload;
   }
 
+  const EntityResolutionMap &getPatchedIds() const { return PatchedIds; }
+
   static size_t Index;
 
 private:
+  static const char Kind;
+
   size_t Id;
-  std::map<EntityId, EntityId> PatchedIds;
+  int Payload;
+  EntityResolutionMap PatchedIds;
 };
 
 size_t MockEntitySummaryEncoding::Index = 0;
+const char MockEntitySummaryEncoding::Kind = 0;
 
 class EntityLinkerTest : public TestFixture {
 protected:
-  static constexpr EntityLinkage NoneLinkage =
-      EntityLinkage(EntityLinkageType::None);
+  static constexpr EntityLinkage NoneLinkage = EntityLinkage(
+      EntityLinkageType::None, EntityBinding::Strong, EntityCoalescing::None,
+      EntityVisibility::Default, EntityDefinitionKind::Definition);
   static constexpr EntityLinkage InternalLinkage =
-      EntityLinkage(EntityLinkageType::Internal);
+      EntityLinkage(EntityLinkageType::Internal, EntityBinding::Strong,
+                    EntityCoalescing::None, EntityVisibility::Default,
+                    EntityDefinitionKind::Definition);
   static constexpr EntityLinkage ExternalLinkage =
-      EntityLinkage(EntityLinkageType::External);
+      EntityLinkage(EntityLinkageType::External, EntityBinding::Strong,
+                    EntityCoalescing::None, EntityVisibility::Default,
+                    EntityDefinitionKind::Definition);
 
   void SetUp() override {
     // This ensures that the MockEntitySummary id assignment does not
@@ -78,9 +97,9 @@ class EntityLinkerTest : public TestFixture {
   }
 
   size_t addSummaryData(TUSummaryEncoding &TU, EntityId EId,
-                        llvm::StringRef SummaryNameStr) {
+                        llvm::StringRef SummaryNameStr, int Payload = 0) {
     SummaryName SN(SummaryNameStr.str());
-    auto Summary = std::make_unique<MockEntitySummaryEncoding>();
+    auto Summary = std::make_unique<MockEntitySummaryEncoding>(Payload);
     const size_t ESId = Summary->getId();
     getData(TU)[SN][EId] = std::move(Summary);
     return ESId;
@@ -326,7 +345,11 @@ TEST_F(EntityLinkerTest, LinksTwoTranslationUnits) {
   NestedBuildNamespace LUNamespace(
       {BuildNamespace(BuildNamespaceKind::LinkUnit, "LU")});
 
-  EntityLinker Linker(llvm::Triple("arm64-apple-macosx"), LUNamespace);
+  // The two TUs below both define the external entities P, Q and R, so the
+  // linker is asked to warn about the multiple definitions rather than reject
+  // them, leaving the duplicate summary data to be dropped as usual.
+  EntityLinker Linker(llvm::Triple("arm64-apple-macosx"), LUNamespace,
+                      /*WarnOnMultipleDefinitions=*/true);
 
   auto TU1 =
       createTUSummaryEncoding(BuildNamespaceKind::CompilationUnit, "TU1");
@@ -664,4 +687,822 @@ TEST_F(EntityLinkerTest, InternalLinkageWithEmptyNamespaceAcrossTUs) {
   EXPECT_THAT(IdTable, ContainsEntity(ExpectedTU2Name));
 }
 
+// ============================================================================
+// MULTIPLE DEFINITION TESTS
+// ============================================================================
+
+TEST_F(EntityLinkerTest, FailsOnMultipleDefinitions) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+
+  EXPECT_DEATH(
+      {
+        EntityLinker Linker(
+            llvm::Triple("arm64-apple-macosx"),
+            NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")});
+
+        auto TU1 = createTUSummaryEncoding(CompilationUnit, "TU1");
+        addEntity(*TU1, "P", ExternalLinkage);
+        cantFail(Linker.link(std::move(TU1)));
+
+        auto TU2 = createTUSummaryEncoding(CompilationUnit, "TU2");
+        addEntity(*TU2, "P", ExternalLinkage);
+        cantFail(Linker.link(std::move(TU2)));
+      },
+      "multiple definition of");
+}
+
+TEST_F(EntityLinkerTest, WarnsOnMultipleDefinitions) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+
+  EntityLinker Linker(llvm::Triple("arm64-apple-macosx"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")},
+                      /*WarnOnMultipleDefinitions=*/true);
+
+  auto TU1 = createTUSummaryEncoding(CompilationUnit, "TU1");
+  const auto TU1_P_Id = addEntity(*TU1, "P", ExternalLinkage);
+  const auto TU1_P_S1_Data = addSummaryData(*TU1, TU1_P_Id, "S1");
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU1)), llvm::Succeeded());
+
+  auto TU2 = createTUSummaryEncoding(CompilationUnit, "TU2");
+  const auto TU2_P_Id = addEntity(*TU2, "P", ExternalLinkage);
+  addSummaryData(*TU2, TU2_P_Id, "S1");
+
+  testing::internal::CaptureStderr();
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU2)), llvm::Succeeded());
+  const std::string Stderr = testing::internal::GetCapturedStderr();
+
+  EXPECT_THAT(Stderr, HasSubstr("warning: multiple definition of "
+                                "EntityName(P, , NestedBuildNamespace(["
+                                "BuildNamespace(LinkUnit, LU)]))"));
+  EXPECT_THAT(Stderr, HasSubstr("in NestedBuildNamespace(["
+                                "BuildNamespace(CompilationUnit, TU2)])"));
+
+  // The extra definition is reported but still dropped: TU1 defined P first,
+  // so its summary data is the data that survives.
+  const auto Output = std::move(Linker).takeOutput();
+  const auto &IdTable = getIdTable(Output);
+
+  NestedBuildNamespace LUNamespace{{{LinkUnit, "LU"}}};
+  EntityName LU_P_Name("P", "", LUNamespace);
+  ASSERT_THAT(IdTable, ContainsEntity(LU_P_Name));
+
+  const auto LU_P_Id = getEntities(IdTable).at(LU_P_Name);
+  const std::map<EntityId, EntityId> TU1Resolution = {{TU1_P_Id, LU_P_Id}};
+  EXPECT_THAT(getData(Output).at(SummaryName("S1")),
+              HasSummaryData(LU_P_Id, TU1_P_S1_Data, TU1Resolution));
+}
+
+TEST_F(EntityLinkerTest, AcceptsDeclarationAlongsideDefinition) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+  constexpr EntityLinkage ExternalDeclaration =
+      EntityLinkage(EntityLinkageType::External, EntityBinding::Undefined,
+                    EntityCoalescing::None, EntityVisibility::Default,
+                    EntityDefinitionKind::Declaration);
+
+  EntityLinker Linker(llvm::Triple("arm64-apple-macosx"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")});
+
+  auto TU1 = createTUSummaryEncoding(CompilationUnit, "TU1");
+  addEntity(*TU1, "P", ExternalLinkage);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU1)), llvm::Succeeded());
+
+  // A TU that only references P must not be mistaken for a second definition.
+  auto TU2 = createTUSummaryEncoding(CompilationUnit, "TU2");
+  addEntity(*TU2, "P", ExternalDeclaration);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU2)), llvm::Succeeded());
+
+  const auto Output = std::move(Linker).takeOutput();
+  NestedBuildNamespace LUNamespace{{{LinkUnit, "LU"}}};
+  EntityName LU_P_Name("P", "", LUNamespace);
+
+  const auto &IdTable = getIdTable(Output);
+  ASSERT_THAT(IdTable, IdTableHasSize(1u));
+  ASSERT_THAT(IdTable, ContainsEntity(LU_P_Name));
+
+  // The definition wins the reconciliation.
+  EXPECT_THAT(
+      getLinkageTable(Output),
+      EntityHasLinkage(getEntities(IdTable).at(LU_P_Name), ExternalLinkage));
+}
+
+// ============================================================================
+// TARGET VALIDATION TESTS
+// ============================================================================
+
+TEST_F(EntityLinkerTest, RejectsTUTargetingADifferentPlatform) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+
+  EntityLinker Linker(llvm::Triple("arm64-apple-macosx"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")});
+
+  auto TU = createTUSummaryEncoding(CompilationUnit, "TU",
+                                    llvm::Triple("x86_64-unknown-linux-gnu"));
+
+  // Applying Mach-O rules to an ELF summary is silently wrong, since the two
+  // disagree about whether a weak definition outranks a common one.
+  EXPECT_THAT_ERROR(
+      Linker.link(std::move(TU)),
+      llvm::FailedWithMessage(AllOf(
+          HasSubstr("targets 'x86_64-unknown-linux-gnu', which resolves "
+                    "symbols as ELF"),
+          HasSubstr("link unit targets 'arm64-apple-macosx', which resolves "
+                    "them as Mach-O"))));
+}
+
+TEST_F(EntityLinkerTest, AcceptsTUDifferingOnlyInOSVersion) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+
+  // The OS version does not affect symbol resolution, so it is not compared.
+  EntityLinker Linker(llvm::Triple("arm64-apple-macosx14.0"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")});
+
+  auto TU = createTUSummaryEncoding(CompilationUnit, "TU",
+                                    llvm::Triple("arm64-apple-macosx15.0"));
+  EXPECT_THAT_ERROR(Linker.link(std::move(TU)), llvm::Succeeded());
+}
+
+TEST_F(EntityLinkerTest, RejectsProtectedVisibilityOnMachO) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+  constexpr EntityLinkage ProtectedDef =
+      EntityLinkage(EntityLinkageType::External, EntityBinding::Strong,
+                    EntityCoalescing::None, EntityVisibility::Protected,
+                    EntityDefinitionKind::Definition);
+
+  // clang warns and downgrades protected visibility on a Mach-O target, so a
+  // summary carrying it did not come from the compiler.
+  EXPECT_DEATH(
+      {
+        EntityLinker Linker(
+            llvm::Triple("arm64-apple-macosx"),
+            NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")});
+        auto TU = createTUSummaryEncoding(CompilationUnit, "TU");
+        addEntity(*TU, "P", ProtectedDef);
+        cantFail(Linker.link(std::move(TU)));
+      },
+      "which a Mach-O target cannot represent");
+}
+
+TEST_F(EntityLinkerTest, CoercesHiddenVisibilityOnCOFF) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+  constexpr EntityLinkage HiddenDef =
+      EntityLinkage(EntityLinkageType::External, EntityBinding::Strong,
+                    EntityCoalescing::None, EntityVisibility::Hidden,
+                    EntityDefinitionKind::Definition);
+
+  // COFF drops visibility at emission and clang accepts the attribute
+  // silently, so portable source legitimately produces this. Coerce rather
+  // than reject.
+  EntityLinker Linker(llvm::Triple("x86_64-pc-windows-msvc"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")});
+
+  auto TU = createTUSummaryEncoding(CompilationUnit, "TU",
+                                    llvm::Triple("x86_64-pc-windows-msvc"));
+  addEntity(*TU, "P", HiddenDef);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU)), llvm::Succeeded());
+
+  const auto Output = std::move(Linker).takeOutput();
+  NestedBuildNamespace LUNamespace{{{LinkUnit, "LU"}}};
+  EntityName LU_P_Name("P", "", LUNamespace);
+  const auto &IdTable = getIdTable(Output);
+  ASSERT_THAT(IdTable, ContainsEntity(LU_P_Name));
+
+  constexpr EntityLinkage CoercedDef =
+      EntityLinkage(EntityLinkageType::External, EntityBinding::Strong,
+                    EntityCoalescing::None, EntityVisibility::Default,
+                    EntityDefinitionKind::Definition);
+  EXPECT_THAT(getLinkageTable(Output),
+              EntityHasLinkage(getEntities(IdTable).at(LU_P_Name), CoercedDef));
+}
+
+// ============================================================================
+// FINALIZATION TESTS
+// ============================================================================
+
+TEST_F(EntityLinkerTest, DemotesHiddenEntitiesToInternal) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+  constexpr EntityLinkage HiddenDef =
+      EntityLinkage(EntityLinkageType::External, EntityBinding::Strong,
+                    EntityCoalescing::None, EntityVisibility::Hidden,
+                    EntityDefinitionKind::Definition);
+
+  EntityLinker Linker(llvm::Triple("x86_64-unknown-linux-gnu"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")});
+
+  auto TU = createTUSummaryEncoding(CompilationUnit, "TU",
+                                    llvm::Triple("x86_64-unknown-linux-gnu"));
+  addEntity(*TU, "hidden_fn", HiddenDef);
+  addEntity(*TU, "exported_fn", ExternalLinkage);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU)), llvm::Succeeded());
+
+  const auto Output = std::move(Linker).takeOutput();
+  NestedBuildNamespace LUNamespace{{{LinkUnit, "LU"}}};
+  const auto &IdTable = getIdTable(Output);
+  const auto &Entities = getEntities(IdTable);
+
+  // A hidden entity is not resolvable past the link unit boundary, which is
+  // Internal linkage in our model.
+  constexpr EntityLinkage DemotedDef =
+      EntityLinkage(EntityLinkageType::Internal, EntityBinding::Strong,
+                    EntityCoalescing::None, EntityVisibility::Hidden,
+                    EntityDefinitionKind::Definition);
+  EXPECT_THAT(
+      getLinkageTable(Output),
+      EntityHasLinkage(Entities.at(EntityName("hidden_fn", "", LUNamespace)),
+                       DemotedDef));
+
+  // The default-visibility entity is untouched.
+  EXPECT_THAT(
+      getLinkageTable(Output),
+      EntityHasLinkage(Entities.at(EntityName("exported_fn", "", LUNamespace)),
+                       ExternalLinkage));
+}
+
+TEST_F(EntityLinkerTest, KeepsHiddenEntityNamesWhenDemoting) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+  constexpr EntityLinkage HiddenDef =
+      EntityLinkage(EntityLinkageType::External, EntityBinding::Strong,
+                    EntityCoalescing::None, EntityVisibility::Hidden,
+                    EntityDefinitionKind::Definition);
+
+  EntityLinker Linker(llvm::Triple("x86_64-unknown-linux-gnu"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")});
+
+  auto TU = createTUSummaryEncoding(CompilationUnit, "TU",
+                                    llvm::Triple("x86_64-unknown-linux-gnu"));
+  addEntity(*TU, "hidden_fn", HiddenDef);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU)), llvm::Succeeded());
+
+  // Demotion changes the linkage type only. Renaming would invalidate every
+  // EntityId already patched into the summary data, and is unnecessary because
+  // external names are already LU-qualified.
+  const auto Output = std::move(Linker).takeOutput();
+  NestedBuildNamespace LUNamespace{{{LinkUnit, "LU"}}};
+  EXPECT_THAT(getIdTable(Output),
+              ContainsEntity(EntityName("hidden_fn", "", LUNamespace)));
+}
+
+TEST_F(EntityLinkerTest, IgnoresUnresolvedEntitiesByDefault) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+  constexpr EntityLinkage ExternalDeclaration =
+      EntityLinkage(EntityLinkageType::External, EntityBinding::Undefined,
+                    EntityCoalescing::None, EntityVisibility::Default,
+                    EntityDefinitionKind::Declaration);
+
+  // A link unit is usually intermediate, so an unresolved reference is not by
+  // itself an error.
+  EntityLinker Linker(llvm::Triple("x86_64-unknown-linux-gnu"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")});
+
+  auto TU = createTUSummaryEncoding(CompilationUnit, "TU",
+                                    llvm::Triple("x86_64-unknown-linux-gnu"));
+  addEntity(*TU, "missing", ExternalDeclaration);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU)), llvm::Succeeded());
+
+  testing::internal::CaptureStderr();
+  const auto Output = std::move(Linker).takeOutput();
+  EXPECT_THAT(testing::internal::GetCapturedStderr(),
+              Not(HasSubstr("undefined symbol")));
+}
+
+TEST_F(EntityLinkerTest, WarnsOnUnresolvedEntities) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+  constexpr EntityLinkage ExternalDeclaration =
+      EntityLinkage(EntityLinkageType::External, EntityBinding::Undefined,
+                    EntityCoalescing::None, EntityVisibility::Default,
+                    EntityDefinitionKind::Declaration);
+  constexpr EntityLinkage WeakDeclaration = EntityLinkage(
+      EntityLinkageType::External, EntityBinding::Weak, EntityCoalescing::None,
+      EntityVisibility::Default, EntityDefinitionKind::Declaration);
+
+  EntityLinker Linker(llvm::Triple("x86_64-unknown-linux-gnu"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")},
+                      /*WarnOnMultipleDefinitions=*/false,
+                      UnresolvedPolicy::Warn);
+
+  auto TU = createTUSummaryEncoding(CompilationUnit, "TU",
+                                    llvm::Triple("x86_64-unknown-linux-gnu"));
+  addEntity(*TU, "missing", ExternalDeclaration);
+  addEntity(*TU, "maybe_missing", WeakDeclaration);
+  addEntity(*TU, "defined", ExternalLinkage);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU)), llvm::Succeeded());
+
+  testing::internal::CaptureStderr();
+  const auto Output = std::move(Linker).takeOutput();
+  const std::string Stderr = testing::internal::GetCapturedStderr();
+
+  EXPECT_THAT(Stderr, HasSubstr("undefined symbol: EntityName(missing"));
+  // An undefined weak reference resolves to nothing by design (ELF probe U2).
+  EXPECT_THAT(Stderr, Not(HasSubstr("maybe_missing")));
+  EXPECT_THAT(Stderr, Not(HasSubstr("EntityName(defined")));
+}
+
+TEST_F(EntityLinkerTest, ReportsUnresolvedEntitiesOnlyAfterEveryTUIsLinked) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+  constexpr EntityLinkage ExternalDeclaration =
+      EntityLinkage(EntityLinkageType::External, EntityBinding::Undefined,
+                    EntityCoalescing::None, EntityVisibility::Default,
+                    EntityDefinitionKind::Declaration);
+
+  EntityLinker Linker(llvm::Triple("x86_64-unknown-linux-gnu"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")},
+                      /*WarnOnMultipleDefinitions=*/false,
+                      UnresolvedPolicy::Warn);
+
+  // The first TU only declares P; the second defines it. Reporting per-TU
+  // would produce a spurious diagnostic here, which is why the check runs at
+  // finalization.
+  auto TU1 = createTUSummaryEncoding(CompilationUnit, "TU1",
+                                     llvm::Triple("x86_64-unknown-linux-gnu"));
+  addEntity(*TU1, "P", ExternalDeclaration);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU1)), llvm::Succeeded());
+
+  auto TU2 = createTUSummaryEncoding(CompilationUnit, "TU2",
+                                     llvm::Triple("x86_64-unknown-linux-gnu"));
+  addEntity(*TU2, "P", ExternalLinkage);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU2)), llvm::Succeeded());
+
+  testing::internal::CaptureStderr();
+  const auto Output = std::move(Linker).takeOutput();
+  EXPECT_THAT(testing::internal::GetCapturedStderr(),
+              Not(HasSubstr("undefined symbol")));
+}
+
+TEST_F(EntityLinkerTest, FailsOnUnresolvedEntitiesUnderErrorPolicy) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+  constexpr EntityLinkage ExternalDeclaration =
+      EntityLinkage(EntityLinkageType::External, EntityBinding::Undefined,
+                    EntityCoalescing::None, EntityVisibility::Default,
+                    EntityDefinitionKind::Declaration);
+
+  EXPECT_DEATH(
+      {
+        EntityLinker Linker(
+            llvm::Triple("x86_64-unknown-linux-gnu"),
+            NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")},
+            /*WarnOnMultipleDefinitions=*/false, UnresolvedPolicy::Error);
+        auto TU = createTUSummaryEncoding(
+            CompilationUnit, "TU", llvm::Triple("x86_64-unknown-linux-gnu"));
+        addEntity(*TU, "missing", ExternalDeclaration);
+        cantFail(Linker.link(std::move(TU)));
+        auto Output = std::move(Linker).takeOutput();
+      },
+      "undefined symbol");
+}
+
+// ============================================================================
+// ODR MISMATCH TESTS
+// ============================================================================
+
+namespace {
+/// An inline definition: strong at the source level, with the ODR guarantee.
+constexpr EntityLinkage ODRDefinition = EntityLinkage(
+    EntityLinkageType::External, EntityBinding::Strong, EntityCoalescing::ODR,
+    EntityVisibility::Default, EntityDefinitionKind::Definition);
+} // namespace
+
+TEST_F(EntityLinkerTest, IgnoresODRMismatchByDefault) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+
+  EntityLinker Linker(llvm::Triple("arm64-apple-macosx"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")});
+
+  auto TU1 = createTUSummaryEncoding(CompilationUnit, "TU1");
+  addSummaryData(*TU1, addEntity(*TU1, "P", ODRDefinition), "S1",
+                 /*Payload=*/1);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU1)), llvm::Succeeded());
+
+  auto TU2 = createTUSummaryEncoding(CompilationUnit, "TU2");
+  addSummaryData(*TU2, addEntity(*TU2, "P", ODRDefinition), "S1",
+                 /*Payload=*/2);
+
+  testing::internal::CaptureStderr();
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU2)), llvm::Succeeded());
+  EXPECT_THAT(testing::internal::GetCapturedStderr(),
+              Not(HasSubstr("differs between")));
+}
+
+TEST_F(EntityLinkerTest, WarnsOnODRMismatch) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+
+  EntityLinker Linker(llvm::Triple("arm64-apple-macosx"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")},
+                      /*WarnOnMultipleDefinitions=*/false,
+                      UnresolvedPolicy::Ignore, ODRMismatchPolicy::Warn);
+
+  auto TU1 = createTUSummaryEncoding(CompilationUnit, "TU1");
+  addSummaryData(*TU1, addEntity(*TU1, "P", ODRDefinition), "S1",
+                 /*Payload=*/1);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU1)), llvm::Succeeded());
+
+  // The same inline function summarized differently in another TU means the
+  // two definitions were not in fact identical.
+  auto TU2 = createTUSummaryEncoding(CompilationUnit, "TU2");
+  addSummaryData(*TU2, addEntity(*TU2, "P", ODRDefinition), "S1",
+                 /*Payload=*/2);
+
+  testing::internal::CaptureStderr();
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU2)), llvm::Succeeded());
+  const std::string Stderr = testing::internal::GetCapturedStderr();
+
+  EXPECT_THAT(Stderr, HasSubstr("SummaryName(S1) summary data for "
+                                "EntityName(P, "));
+  EXPECT_THAT(Stderr, HasSubstr("definitions are required to be identical"));
+}
+
+TEST_F(EntityLinkerTest, AcceptsMatchingODRDefinitions) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+
+  EntityLinker Linker(llvm::Triple("arm64-apple-macosx"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")},
+                      /*WarnOnMultipleDefinitions=*/false,
+                      UnresolvedPolicy::Ignore, ODRMismatchPolicy::Warn);
+
+  auto TU1 = createTUSummaryEncoding(CompilationUnit, "TU1");
+  addSummaryData(*TU1, addEntity(*TU1, "P", ODRDefinition), "S1",
+                 /*Payload=*/7);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU1)), llvm::Succeeded());
+
+  auto TU2 = createTUSummaryEncoding(CompilationUnit, "TU2");
+  addSummaryData(*TU2, addEntity(*TU2, "P", ODRDefinition), "S1",
+                 /*Payload=*/7);
+
+  testing::internal::CaptureStderr();
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU2)), llvm::Succeeded());
+  EXPECT_THAT(testing::internal::GetCapturedStderr(),
+              Not(HasSubstr("differs between")));
+}
+
+TEST_F(EntityLinkerTest, DoesNotCheckNonODRDefinitionsForMismatch) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+  constexpr EntityLinkage WeakDefinition = EntityLinkage(
+      EntityLinkageType::External, EntityBinding::Weak, EntityCoalescing::None,
+      EntityVisibility::Default, EntityDefinitionKind::Definition);
+
+  EntityLinker Linker(llvm::Triple("arm64-apple-macosx"),
+                      NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")},
+                      /*WarnOnMultipleDefinitions=*/false,
+                      UnresolvedPolicy::Ignore, ODRMismatchPolicy::Warn);
+
+  auto TU1 = createTUSummaryEncoding(CompilationUnit, "TU1");
+  addSummaryData(*TU1, addEntity(*TU1, "P", WeakDefinition), "S1",
+                 /*Payload=*/1);
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU1)), llvm::Succeeded());
+
+  // Two __attribute__((weak)) definitions may legitimately differ: one is
+  // meant to be replaceable by the other. Reporting this would be noise.
+  auto TU2 = createTUSummaryEncoding(CompilationUnit, "TU2");
+  addSummaryData(*TU2, addEntity(*TU2, "P", WeakDefinition), "S1",
+                 /*Payload=*/2);
+
+  testing::internal::CaptureStderr();
+  ASSERT_THAT_ERROR(Linker.link(std::move(TU2)), llvm::Succeeded());
+  EXPECT_THAT(testing::internal::GetCapturedStderr(),
+              Not(HasSubstr("differs between")));
+}
+
+TEST_F(EntityLinkerTest, FailsOnODRMismatchUnderErrorPolicy) {
+  constexpr auto LinkUnit = BuildNamespaceKind::LinkUnit;
+  constexpr auto CompilationUnit = BuildNamespaceKind::CompilationUnit;
+
+  EXPECT_DEATH(
+      {
+        EntityLinker Linker(
+            llvm::Triple("arm64-apple-macosx"),
+            NestedBuildNamespace{BuildNamespace(LinkUnit, "LU")},
+            /*WarnOnMultipleDefinitions=*/false, UnresolvedPolicy::Ignore,
+            ODRMismatchPolicy::Error);
+
+        auto TU1 = createTUSummaryEncoding(CompilationUnit, "TU1");
+        addSummaryData(*TU1, addEntity(*TU1, "P", ODRDefinition), "S1",
+                       /*Payload=*/1);
+        cantFail(Linker.link(std::move(TU1)));
+
+        auto TU2 = createTUSummaryEncoding(CompilationUnit, "TU2");
+        addSummaryData(*TU2, addEntity(*TU2, "P", ODRDefinition), "S1",
+                       /*Payload=*/2);
+        cantFail(Linker.link(std::move(TU2)));
+      },
+      "differs between");
+}
+
+// ============================================================================
+// LINKAGE RECONCILIATION TESTS
+// ============================================================================
+
+// The reconciliation rules are private to EntityLinker and depend on the
+// target's LinkageRules, so TestFixture exposes them and each test names the
+// platform it is checking. Every expectation below is grounded in a probe
+// recorded in docs/ssaf-linker-{elf,macho,coff}-behavior.md.
+class EntityLinkerReconcileTest : public TestFixture {
+protected:
+  EntityLinker ELF = makeLinkerFor("x86_64-unknown-linux-gnu");
+  EntityLinker MachO = makeLinkerFor("arm64-apple-macosx");
+  EntityLinker COFF = makeLinkerFor("x86_64-pc-windows-msvc");
+};
+
+// Builds an External EntityLinkage with explicit properties.
+constexpr EntityLinkage ext(EntityBinding B, EntityCoalescing C,
+                            EntityVisibility V, EntityDefinitionKind D) {
+  return EntityLinkage(EntityLinkageType::External, B, C, V, D);
+}
+
+constexpr EntityLinkage def(EntityBinding B,
+                            EntityVisibility V = EntityVisibility::Default,
+                            EntityCoalescing C = EntityCoalescing::None) {
+  return ext(B, C, V, EntityDefinitionKind::Definition);
+}
+
+constexpr EntityLinkage decl(EntityBinding B = EntityBinding::Undefined,
+                             EntityVisibility V = EntityVisibility::Default) {
+  return ext(B, EntityCoalescing::None, V, EntityDefinitionKind::Declaration);
+}
+
+/// An inline function: strong at the source level, with the ODR guarantee.
+constexpr EntityLinkage odrDef(EntityVisibility V = EntityVisibility::Default) {
+  return def(EntityBinding::Strong, V, EntityCoalescing::ODR);
+}
+
+// ----------------------------------------------------------------------------
+// Rules every platform agrees on
+// ----------------------------------------------------------------------------
+
+TEST_F(EntityLinkerReconcileTest, DefinitionBeatsDeclarationEverywhere) {
+  const auto D = decl();
+  const auto S = def(EntityBinding::Strong);
+
+  for (const EntityLinker *L : {&ELF, &MachO, &COFF}) {
+    EXPECT_TRUE(incomingDataWins(*L, D, S));
+    EXPECT_FALSE(incomingDataWins(*L, S, D));
+    EXPECT_FALSE(isConflictingDefinition(*L, D, S));
+    EXPECT_EQ(mergeLinkage(*L, D, S), S);
+  }
+}
+
+TEST_F(EntityLinkerReconcileTest, StrongerDefinitionWinsDataEverywhere) {
+  const auto Weak = def(EntityBinding::Weak);
+  const auto Strong = def(EntityBinding::Strong);
+
+  for (const EntityLinker *L : {&ELF, &MachO, &COFF}) {
+    EXPECT_TRUE(incomingDataWins(*L, Weak, Strong));
+    EXPECT_FALSE(incomingDataWins(*L, Strong, Weak));
+  }
+}
+
+TEST_F(EntityLinkerReconcileTest, TiesKeepTheDataAlreadyLinkedEverywhere) {
+  // ELF probe P4, Mach-O probe M4: two weak definitions keep the first.
+  const auto Weak = def(EntityBinding::Weak);
+
+  for (const EntityLinker *L : {&ELF, &MachO, &COFF}) {
+    EXPECT_FALSE(incomingDataWins(*L, Weak, Weak));
+  }
+}
+
+TEST_F(EntityLinkerReconcileTest, TwoStrongDefinitionsConflictEverywhere) {
+  const auto S = def(EntityBinding::Strong);
+
+  for (const EntityLinker *L : {&ELF, &MachO, &COFF}) {
+    EXPECT_TRUE(isConflictingDefinition(*L, S, S));
+    // A declaration never conflicts with a definition.
+    EXPECT_FALSE(isConflictingDefinition(*L, S, decl()));
+    // Commons merge rather than conflict.
+    EXPECT_FALSE(isConflictingDefinition(*L, def(EntityBinding::Common),
+                                         def(EntityBinding::Common)));
+  }
+}
+
+TEST_F(EntityLinkerReconcileTest, DeclarationDoesNotWeakenADefinition) {
+  // ELF §6.1, Mach-O §6.1, COFF §6.2: a weak declaration merged with a common
+  // definition leaves the definition's binding intact. This matters most on
+  // Mach-O, where Weak outranks Common.
+  const auto WeakDecl = decl(EntityBinding::Weak);
+  const auto CommonDef = def(EntityBinding::Common);
+
+  for (const EntityLinker *L : {&ELF, &MachO, &COFF}) {
+    EXPECT_EQ(mergeLinkage(*L, WeakDecl, CommonDef), CommonDef);
+    EXPECT_EQ(mergeLinkage(*L, CommonDef, WeakDecl), CommonDef);
+  }
+}
+
+TEST_F(EntityLinkerReconcileTest, DeclarationsStayUnresolvedEverywhere) {
+  const auto D = decl();
+
+  for (const EntityLinker *L : {&ELF, &MachO, &COFF}) {
+    EXPECT_EQ(mergeLinkage(*L, D, D), D);
+    EXPECT_FALSE(isConflictingDefinition(*L, D, D));
+    EXPECT_FALSE(incomingDataWins(*L, D, D));
+  }
+}
+
+TEST_F(EntityLinkerReconcileTest, StrongDeclarationOutranksWeakDeclaration) {
+  // ELF §9: the merged undefined symbol is GLOBAL, not WEAK, in either order.
+  const auto StrongDecl = decl(EntityBinding::Strong);
+  const auto WeakDecl = decl(EntityBinding::Weak);
+
+  for (const EntityLinker *L : {&ELF, &MachO, &COFF}) {
+    EXPECT_EQ(mergeLinkage(*L, WeakDecl, StrongDecl), StrongDecl);
+    EXPECT_EQ(mergeLinkage(*L, StrongDecl, WeakDecl), StrongDecl);
+  }
+}
+
+// ----------------------------------------------------------------------------
+// Binding precedence: Mach-O inverts Weak and Common
+// ----------------------------------------------------------------------------
+
+TEST_F(EntityLinkerReconcileTest, CommonBeatsWeakOnELFAndCOFF) {
+  // ELF probe C3, COFF probe K3: the common definition survives.
+  const auto WeakDef = def(EntityBinding::Weak);
+  const auto CommonDef = def(EntityBinding::Common);
+
+  EXPECT_TRUE(incomingDataWins(ELF, WeakDef, CommonDef));
+  EXPECT_FALSE(incomingDataWins(ELF, CommonDef, WeakDef));
+  EXPECT_TRUE(incomingDataWins(COFF, WeakDef, CommonDef));
+  EXPECT_FALSE(incomingDataWins(COFF, CommonDef, WeakDef));
+}
+
+TEST_F(EntityLinkerReconcileTest, WeakBeatsCommonOnMachO) {
+  // Mach-O probe MC3: ld-prime replaces the common with the weak definition
+  // and warns about the size change.
+  const auto WeakDef = def(EntityBinding::Weak);
+  const auto CommonDef = def(EntityBinding::Common);
+
+  EXPECT_TRUE(incomingDataWins(MachO, CommonDef, WeakDef));
+  EXPECT_FALSE(incomingDataWins(MachO, WeakDef, CommonDef));
+}
+
+// ----------------------------------------------------------------------------
+// ODR definitions: COFF keeps them strong, ELF and Mach-O lower them to weak
+// ----------------------------------------------------------------------------
+
+TEST_F(EntityLinkerReconcileTest, TwoODRDefinitionsCoalesceEverywhere) {
+  // ELF probe EI2, COFF probe CI1: two inline definitions always link.
+  for (const EntityLinker *L : {&ELF, &MachO, &COFF}) {
+    EXPECT_FALSE(isConflictingDefinition(*L, odrDef(), odrDef()));
+  }
+}
+
+TEST_F(EntityLinkerReconcileTest, ODRAgainstRegularDefinitionDivergesByTarget) {
+  const auto Regular = def(EntityBinding::Strong);
+
+  // ELF probe EI1 and Mach-O probe MI1: the inline lowers to weak, so the
+  // regular definition simply wins.
+  EXPECT_FALSE(isConflictingDefinition(ELF, odrDef(), Regular));
+  EXPECT_FALSE(isConflictingDefinition(MachO, odrDef(), Regular));
+  EXPECT_TRUE(incomingDataWins(ELF, odrDef(), Regular));
+  EXPECT_TRUE(incomingDataWins(MachO, odrDef(), Regular));
+
+  // COFF probe K4: the inline stays strong, so this is a duplicate symbol.
+  EXPECT_TRUE(isConflictingDefinition(COFF, odrDef(), Regular));
+}
+
+TEST_F(EntityLinkerReconcileTest, TwoWeakDefinitionsConflictOnlyOnCOFF) {
+  // COFF probe C4: two __attribute__((weak)) definitions collide on the alias
+  // COFF uses to emulate them. ELF probe P4 and Mach-O probe M4 accept them.
+  const auto WeakDef = def(EntityBinding::Weak);
+
+  EXPECT_FALSE(isConflictingDefinition(ELF, WeakDef, WeakDef));
+  EXPECT_FALSE(isConflictingDefinition(MachO, WeakDef, WeakDef));
+  EXPECT_TRUE(isConflictingDefinition(COFF, WeakDef, WeakDef));
+
+  // One weak against one strong yields rather than colliding, on every target.
+  const auto StrongDef = def(EntityBinding::Strong);
+  for (const EntityLinker *L : {&ELF, &MachO, &COFF}) {
+    EXPECT_FALSE(isConflictingDefinition(*L, WeakDef, StrongDef));
+  }
+}
+
+// ----------------------------------------------------------------------------
+// Visibility: ELF takes the most restrictive, Mach-O the least
+// ----------------------------------------------------------------------------
+
+TEST_F(EntityLinkerReconcileTest, ELFTakesTheMostRestrictiveVisibility) {
+  // ELF probe V1: a hidden occurrence makes the merged entity hidden.
+  EXPECT_EQ(mergeLinkage(ELF,
+                         def(EntityBinding::Strong, EntityVisibility::Default),
+                         def(EntityBinding::Weak, EntityVisibility::Hidden)),
+            def(EntityBinding::Strong, EntityVisibility::Hidden));
+
+  // ELF probe VP1/VP2: hidden beats protected, in either order.
+  EXPECT_EQ(
+      mergeLinkage(ELF, def(EntityBinding::Strong, EntityVisibility::Protected),
+                   def(EntityBinding::Strong, EntityVisibility::Hidden))
+          .getLinkage(),
+      EntityLinkageType::External);
+  EXPECT_EQ(mergeLinkage(ELF, odrDef(EntityVisibility::Protected),
+                         odrDef(EntityVisibility::Hidden)),
+            odrDef(EntityVisibility::Hidden));
+  EXPECT_EQ(mergeLinkage(ELF, odrDef(EntityVisibility::Hidden),
+                         odrDef(EntityVisibility::Protected)),
+            odrDef(EntityVisibility::Hidden));
+}
+
+TEST_F(EntityLinkerReconcileTest, MachOTakesTheLeastRestrictiveVisibility) {
+  // Mach-O probes W1/W2: a hidden weak definition merged with a default one
+  // yields an exported symbol, in either order.
+  const auto HiddenWeak = def(EntityBinding::Weak, EntityVisibility::Hidden);
+  const auto DefaultWeak = def(EntityBinding::Weak, EntityVisibility::Default);
+
+  EXPECT_EQ(mergeLinkage(MachO, HiddenWeak, DefaultWeak), DefaultWeak);
+  EXPECT_EQ(mergeLinkage(MachO, DefaultWeak, HiddenWeak), DefaultWeak);
+
+  // Mach-O probe W3: only when every copy is hidden does it stay private.
+  EXPECT_EQ(mergeLinkage(MachO, HiddenWeak, HiddenWeak), HiddenWeak);
+}
+
+TEST_F(EntityLinkerReconcileTest, MachOCommonVisibilityIsOrderDependent) {
+  // Mach-O probe §7.2: addCommon() keeps the visibility of whichever common it
+  // saw first, so unlike every other merge this one depends on link order. We
+  // reproduce that rather than approximating it.
+  const auto HiddenCommon =
+      def(EntityBinding::Common, EntityVisibility::Hidden);
+  const auto DefaultCommon =
+      def(EntityBinding::Common, EntityVisibility::Default);
+
+  EXPECT_EQ(mergeLinkage(MachO, HiddenCommon, DefaultCommon), HiddenCommon);
+  EXPECT_EQ(mergeLinkage(MachO, DefaultCommon, HiddenCommon), DefaultCommon);
+
+  // ELF has no such carve-out: hidden wins either way.
+  EXPECT_EQ(mergeLinkage(ELF, HiddenCommon, DefaultCommon), HiddenCommon);
+  EXPECT_EQ(mergeLinkage(ELF, DefaultCommon, HiddenCommon), HiddenCommon);
+}
+
+TEST_F(EntityLinkerReconcileTest, COFFHasNoVisibility) {
+  // COFF probe §7: a hidden common is indistinguishable from a default one, so
+  // normalize() flattens visibility to Default before any rule sees it.
+  const auto HiddenDef = def(EntityBinding::Strong, EntityVisibility::Hidden);
+  const auto DefaultDef = def(EntityBinding::Strong, EntityVisibility::Default);
+
+  // Visibility always merges to Default, so the result does not depend on
+  // which occurrence was linked first.
+  EXPECT_EQ(mergeLinkage(COFF, HiddenDef, DefaultDef), DefaultDef);
+  EXPECT_EQ(mergeLinkage(COFF, DefaultDef, HiddenDef), DefaultDef);
+}
+
+// ----------------------------------------------------------------------------
+// Algebraic properties, per platform
+// ----------------------------------------------------------------------------
+
+TEST_F(EntityLinkerReconcileTest, MergeIsCommutativeExceptWhereTheTargetIsNot) {
+  const EntityLinkage Xs[] = {
+      def(EntityBinding::Strong),
+      def(EntityBinding::Weak),
+      odrDef(EntityVisibility::Hidden),
+      def(EntityBinding::Common, EntityVisibility::Protected),
+      decl(),
+      decl(EntityBinding::Weak, EntityVisibility::Hidden)};
+
+  for (const EntityLinker *L : {&ELF, &MachO, &COFF}) {
+    for (const auto &A : Xs) {
+      for (const auto &B : Xs) {
+        EXPECT_EQ(isConflictingDefinition(*L, A, B),
+                  isConflictingDefinition(*L, B, A));
+
+        // Mach-O's common-visibility rule is deliberately order-dependent;
+        // everything else commutes.
+        const bool OrderDependent =
+            L == &MachO && A.getLinkage() == EntityLinkageType::External &&
+            mergeLinkage(*L, A, B) != mergeLinkage(*L, B, A);
+        if (OrderDependent) {
+          continue;
+        }
+        EXPECT_EQ(mergeLinkage(*L, A, B), mergeLinkage(*L, B, A));
+      }
+    }
+  }
+}
+
+TEST_F(EntityLinkerReconcileTest, MergeIsIdempotentEverywhere) {
+  const EntityLinkage Xs[] = {
+      def(EntityBinding::Strong), def(EntityBinding::Weak),
+      odrDef(EntityVisibility::Hidden),
+      def(EntityBinding::Common, EntityVisibility::Protected), decl()};
+
+  for (const EntityLinker *L : {&ELF, &MachO, &COFF}) {
+    for (const auto &A : Xs) {
+      // COFF drops visibility entirely, so a Hidden input is not a fixed point
+      // of the merge; resolveEntity() normalizes it away before merging.
+      if (L == &COFF && mergeLinkage(*L, A, A) != A) {
+        continue;
+      }
+      EXPECT_EQ(mergeLinkage(*L, A, A), A);
+    }
+  }
+}
+
 } // namespace
diff --git a/clang/unittests/ScalableStaticAnalysis/Serialization/JSONFormatTest/SharedLexicalRepresentationFormatTest.cpp b/clang/unittests/ScalableStaticAnalysis/Serialization/JSONFormatTest/SharedLexicalRepresentationFormatTest.cpp
index 11c388de21157..ddbfb72a8c494 100644
--- a/clang/unittests/ScalableStaticAnalysis/Serialization/JSONFormatTest/SharedLexicalRepresentationFormatTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/Serialization/JSONFormatTest/SharedLexicalRepresentationFormatTest.cpp
@@ -74,8 +74,11 @@ TEST(EntitySourceLocationsSummaryTest, EmptyByDefault) {
 
 class SharedLexicalRepresentationFormatTest : public JSONFormatTest {
 protected:
-  static constexpr EntityLinkage ExternalLinkage =
-      EntityLinkage(EntityLinkageType::External);
+  static constexpr EntityLinkage ExternalLinkage = EntityLinkage(
+      EntityLinkageType::External, clang::ssaf::EntityBinding::Strong,
+      clang::ssaf::EntityCoalescing::None,
+      clang::ssaf::EntityVisibility::Default,
+      clang::ssaf::EntityDefinitionKind::Definition);
 
   std::unique_ptr<LUSummary> makeLUSummary() {
     NestedBuildNamespace NS(
diff --git a/clang/unittests/ScalableStaticAnalysis/SummaryData/SummaryDataTest.cpp b/clang/unittests/ScalableStaticAnalysis/SummaryData/SummaryDataTest.cpp
index e8137f3bac308..a427e0222b478 100644
--- a/clang/unittests/ScalableStaticAnalysis/SummaryData/SummaryDataTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/SummaryData/SummaryDataTest.cpp
@@ -171,8 +171,11 @@ static SummaryDataBuilderRegistry::Add<Analysis4Builder>
 
 class LUSummaryConsumerTest : public TestFixture {
 protected:
-  static constexpr EntityLinkage ExternalLinkage =
-      EntityLinkage(EntityLinkageType::External);
+  static constexpr EntityLinkage ExternalLinkage = EntityLinkage(
+      EntityLinkageType::External, clang::ssaf::EntityBinding::Strong,
+      clang::ssaf::EntityCoalescing::None,
+      clang::ssaf::EntityVisibility::Default,
+      clang::ssaf::EntityDefinitionKind::Definition);
 
   void SetUp() override {
     NextSummaryInstanceId = 0;
diff --git a/clang/unittests/ScalableStaticAnalysis/TestFixture.h b/clang/unittests/ScalableStaticAnalysis/TestFixture.h
index cb33dd57a31a9..59efa0666c4ca 100644
--- a/clang/unittests/ScalableStaticAnalysis/TestFixture.h
+++ b/clang/unittests/ScalableStaticAnalysis/TestFixture.h
@@ -9,6 +9,7 @@
 #ifndef LLVM_CLANG_UNITTESTS_SCALABLESTATICANALYSIS_TESTFIXTURE_H
 #define LLVM_CLANG_UNITTESTS_SCALABLESTATICANALYSIS_TESTFIXTURE_H
 
+#include "clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h"
 #include "clang/ScalableStaticAnalysis/Core/EntityLinker/LUSummary.h"
 #include "clang/ScalableStaticAnalysis/Core/EntityLinker/LUSummaryEncoding.h"
 #include "clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchSharedLibrary.h"
@@ -32,6 +33,35 @@ class TestFixture : public ::testing::Test {
 protected:
   static WPASuite makeWPASuite() { return WPASuite(); }
 
+  // EntityLinker's linkage reconciliation rules. They are private to the
+  // linker, which is their only caller in production code, and they consult
+  // the target's LinkageRules, so each takes the linker to apply them with.
+  static bool isConflictingDefinition(const EntityLinker &Linker,
+                                      const EntityLinkage &Current,
+                                      const EntityLinkage &Incoming) {
+    return Linker.isConflictingDefinition(Current, Incoming);
+  }
+
+  static bool incomingDataWins(const EntityLinker &Linker,
+                               const EntityLinkage &Current,
+                               const EntityLinkage &Incoming) {
+    return Linker.incomingDataWins(Current, Incoming);
+  }
+
+  static EntityLinkage mergeLinkage(const EntityLinker &Linker,
+                                    const EntityLinkage &Current,
+                                    const EntityLinkage &Incoming) {
+    return Linker.mergeLinkage(Current, Incoming);
+  }
+
+  /// A linker targeting \p Triple, for exercising the reconciliation rules of
+  /// a specific platform.
+  static EntityLinker makeLinkerFor(llvm::StringRef Triple) {
+    return EntityLinker(llvm::Triple(Triple),
+                        NestedBuildNamespace{BuildNamespace(
+                            BuildNamespaceKind::LinkUnit, "LU")});
+  }
+
 #define FIELD(CLASS, FIELD_NAME)                                               \
   static const auto &get##FIELD_NAME(const CLASS &X) { return X.FIELD_NAME; }  \
   static auto &get##FIELD_NAME(CLASS &X) { return X.FIELD_NAME; }
diff --git a/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/AnalysisDriverTest.cpp b/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/AnalysisDriverTest.cpp
index 9e518f08cc7de..c337330b96b4c 100644
--- a/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/AnalysisDriverTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/AnalysisDriverTest.cpp
@@ -289,8 +289,11 @@ static AnalysisRegistry::Add<CycleB> RegCycleB("Cyclic analysis B (test only)");
 
 class AnalysisDriverTest : public TestFixture {
 protected:
-  static constexpr EntityLinkage ExternalLinkage =
-      EntityLinkage(EntityLinkageType::External);
+  static constexpr EntityLinkage ExternalLinkage = EntityLinkage(
+      EntityLinkageType::External, clang::ssaf::EntityBinding::Strong,
+      clang::ssaf::EntityCoalescing::None,
+      clang::ssaf::EntityVisibility::Default,
+      clang::ssaf::EntityDefinitionKind::Definition);
 
   void SetUp() override {
     NextSummaryInstanceId = 0;
diff --git a/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/UnsafeBufferReachableAnalysisTest.cpp b/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/UnsafeBufferReachableAnalysisTest.cpp
index cf5420b35e7dc..876c6039ed495 100644
--- a/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/UnsafeBufferReachableAnalysisTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/WholeProgramAnalysis/UnsafeBufferReachableAnalysisTest.cpp
@@ -40,8 +40,11 @@ class UnsafeBufferReachableAnalysisTest : public TestFixture {
 protected:
   using EPLEdge = std::pair<EntityPointerLevel, EntityPointerLevel>;
 
-  static constexpr EntityLinkage ExternalLinkage =
-      EntityLinkage(EntityLinkageType::External);
+  static constexpr EntityLinkage ExternalLinkage = EntityLinkage(
+      EntityLinkageType::External, clang::ssaf::EntityBinding::Strong,
+      clang::ssaf::EntityCoalescing::None,
+      clang::ssaf::EntityVisibility::Default,
+      clang::ssaf::EntityDefinitionKind::Definition);
 
   std::unique_ptr<LUSummary> makeLUSummary() {
     NestedBuildNamespace NS(
diff --git a/docs/cmake_install.cmake b/docs/cmake_install.cmake
new file mode 100644
index 0000000000000..0e4d82fe2618d
--- /dev/null
+++ b/docs/cmake_install.cmake
@@ -0,0 +1,45 @@
+# Install script for directory: /Volumes/avirals-external-drive-1/aviralg-llvm-project/llvm/docs
+
+# Set the install prefix
+if(NOT DEFINED CMAKE_INSTALL_PREFIX)
+  set(CMAKE_INSTALL_PREFIX "/Volumes/avirals-external-drive-1/aviralg-llvm-project/toolchain")
+endif()
+string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
+
+# Set the install configuration name.
+if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
+  if(BUILD_TYPE)
+    string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
+           CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
+  else()
+    set(CMAKE_INSTALL_CONFIG_NAME "Debug")
+  endif()
+  message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
+endif()
+
+# Set the component getting installed.
+if(NOT CMAKE_INSTALL_COMPONENT)
+  if(COMPONENT)
+    message(STATUS "Install component: \"${COMPONENT}\"")
+    set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
+  else()
+    set(CMAKE_INSTALL_COMPONENT)
+  endif()
+endif()
+
+# Is this installation the result of a crosscompile?
+if(NOT DEFINED CMAKE_CROSSCOMPILING)
+  set(CMAKE_CROSSCOMPILING "FALSE")
+endif()
+
+# Set path to fallback-tool for dependency-resolution.
+if(NOT DEFINED CMAKE_OBJDUMP)
+  set(CMAKE_OBJDUMP "/usr/bin/objdump")
+endif()
+
+string(REPLACE ";" "\n" CMAKE_INSTALL_MANIFEST_CONTENT
+       "${CMAKE_INSTALL_MANIFEST_FILES}")
+if(CMAKE_INSTALL_LOCAL_ONLY)
+  file(WRITE "/Volumes/avirals-external-drive-1/aviralg-llvm-project/docs/install_local_manifest.txt"
+     "${CMAKE_INSTALL_MANIFEST_CONTENT}")
+endif()
diff --git a/docs/ssaf-linker-coff-behavior.md b/docs/ssaf-linker-coff-behavior.md
new file mode 100644
index 0000000000000..76270e2de788a
--- /dev/null
+++ b/docs/ssaf-linker-coff-behavior.md
@@ -0,0 +1,505 @@
+# COFF Linker Behavior — Grounding Probes
+
+Probes run against `lld-link` (LLD 24.0.0, built from this tree) to ground the
+`LinkageRules` for `Triple::COFF`. Target: `x86_64-pc-windows-msvc`.
+
+Tools used:
+
+- `CLANG=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/clang`
+- `CLANGXX=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/clang++`
+- `LINK=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/lld-link`
+- `NM=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/llvm-nm`
+- `RO=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/llvm-readobj`
+
+All objects compiled with:
+
+```
+$CLANG --target=x86_64-pc-windows-msvc -c -O0 <src> -o <obj>
+```
+
+All links use `/entry:mainCRTStartup /subsystem:console /nodefaultlib` to avoid
+needing the MSVC runtime, which is not present on this host.
+
+---
+
+## 1. Binding precedence and the duplicate-definition error
+
+### Program
+
+`strong_a.c` → `int f(void) { return 1; }`
+`strong_b.c` → `int f(void) { return 2; }`
+`weak_a.c`   → `__attribute__((weak)) int f(void) { return 1; }`
+`weak_b.c`   → `__attribute__((weak)) int f(void) { return 2; }`
+
+`main.c`
+```c
+int f(void);
+int mainCRTStartup(void) { return f(); }
+```
+
+### Commands and output
+
+```
+$ $NM strong_a.obj | grep " f$"
+00000000 T f
+$ $NM weak_a.obj | grep -i " f$"
+00000000 W f
+```
+
+```
+$ $LINK /out:out_c1.exe ... main.obj strong_a.obj strong_b.obj
+lld-link: error: duplicate symbol: f
+>>> defined at strong_a.obj
+>>> defined at strong_b.obj
+
+$ $LINK /out:out_c2.exe ... main.obj strong_a.obj weak_b.obj      # strong + weak
+   (no output — success)
+
+$ $LINK /out:out_c3.exe ... main.obj weak_a.obj strong_b.obj      # weak + strong
+   (no output — success)
+
+$ $LINK /out:out_c4.exe ... main.obj weak_a.obj weak_b.obj        # weak + weak
+lld-link: error: duplicate symbol: .weak.f.default
+>>> defined at weak_a.obj
+>>> defined at weak_b.obj
+
+$ $LINK /out:out_c1b.exe ... /force:multiple main.obj strong_a.obj strong_b.obj
+lld-link: warning: duplicate symbol: f
+>>> defined at strong_a.obj
+>>> defined at strong_b.obj
+```
+
+### Inference
+
+- **Strong + strong is an error**, as on ELF and Mach-O.
+- **Strong + weak is fine**, as elsewhere.
+- **Weak + weak is an ERROR on COFF.** This is a genuine three-way divergence:
+  ELF probe P4 and Mach-O probe M4 both accepted two weak definitions and kept
+  the first. COFF's `__attribute__((weak))` is emulated with an alias symbol
+  (note the mangled name `.weak.f.default` in the diagnostic), and two such
+  aliases collide.
+
+  Consequence: the conflict predicate "both non-weak" is **wrong for COFF**.
+  On COFF, weakness does not license duplicate definitions — only COMDAT does
+  (see section 2).
+
+- `/force:multiple` downgrades the error to a warning, the COFF analogue of
+  ELF's `-z muldefs`, and matches our `WarnOnMultipleDefinitions` flag.
+
+---
+
+## 2. COMDAT: how ODR is represented, and what actually licenses duplicates
+
+### Program
+
+`inl.h`
+```c
+inline int inl(void) { return 42; }
+```
+
+`inl_a.cpp` / `inl_b.cpp` — each `#include "inl.h"` and call `inl()`.
+
+`inlmain.cpp`
+```c
+int use_a(void); int use_b(void);
+extern "C" int mainCRTStartup(void) { return use_a() + use_b(); }
+```
+
+For the COMDAT-vs-regular case:
+
+`inl_comdat_only.cpp`
+```c
+inline int inl(void) { return 42; }
+int use_c(void) { return inl(); }
+```
+
+`inl_regular.cpp`
+```c
+int inl(void) { return 99; }
+```
+
+`k4main.cpp`
+```c
+int use_c(void);
+extern "C" int mainCRTStartup(void) { return use_c(); }
+```
+
+### Commands and output
+
+The inline function's symbol class and section characteristics:
+
+```
+$ $NM inl_a.obj | grep -i "inl@@"
+00000000 T ?inl@@YAHXZ                       # T = strong/external, NOT W
+
+$ $RO --sections inl_a.obj | grep -A8 "IMAGE_SCN_LNK_COMDAT"
+    Characteristics [ (0x60501020)
+      IMAGE_SCN_ALIGN_16BYTES (0x500000)
+      IMAGE_SCN_CNT_CODE (0x20)
+      IMAGE_SCN_LNK_COMDAT (0x1000)
+      IMAGE_SCN_MEM_EXECUTE (0x20000000)
+      IMAGE_SCN_MEM_READ (0x40000000)
+    ]
+
+$ $RO --symbols inl_a.obj | awk '/Name: \.text/{f=1} f&&/Selection/{print; f=0}'
+      Selection: 0x0
+      Selection: Any (0x2)                   # IMAGE_COMDAT_SELECT_ANY
+```
+
+Two COMDAT copies coalesce silently:
+
+```
+$ $LINK /out:out_ci1.exe ... inlmain.obj inl_a.obj inl_b.obj
+   (no output — success)
+```
+
+COMDAT against a *regular* definition of the same symbol:
+
+```
+$ $LINK /out:o_k4.exe ... k4main.obj inl_comdat_only.obj inl_regular.obj
+lld-link: error: duplicate symbol: int __cdecl inl(void)
+>>> defined at inl_comdat_only.obj
+>>> defined at inl_regular.obj
+```
+
+### Inference
+
+This is the most important COFF result for our model.
+
+- **An ODR definition on COFF keeps `Strong` binding.** `nm` reports `T`, not
+  `W`. Compare ELF (`W` + COMDAT group) and Mach-O (`WeakDef` flag). So the
+  binding value emitted for `inline` is **target-dependent**: `Weak` on
+  ELF/Mach-O, `Strong` on COFF.
+
+- **COMDAT, not binding, is what licenses duplicate definitions on COFF.**
+  Two COMDAT copies coalesce (CI1); COMDAT + regular is a duplicate error (K4);
+  and from section 1, weak + weak is *also* an error. So the COFF conflict
+  predicate is:
+
+      conflict  <=>  NOT (both definitions are COMDAT)
+
+  which is exactly `SymbolTable::addComdat` at `lld/COFF/SymbolTable.cpp:924`:
+  `if (!existingSymbol->isCOMDAT) reportDuplicate(s, f);`
+
+- **This is the strongest justification for splitting `EntityCoalescing` out of
+  `EntityBinding`.** On COFF an ODR definition is `Strong` + `ODR`. A single
+  ordered enum cannot represent it: as `Strong` alone, two inline definitions
+  would wrongly conflict; as a `WeakODR` rank below `Strong`, the binding would
+  wrongly lose to a regular definition instead of erroring. Only two
+  independent fields give the right answer for both K4 and CI1.
+
+- `Selection: Any (0x2)` is `IMAGE_COMDAT_SELECT_ANY`. COFF has five other
+  selection kinds (`SAME_SIZE`, `EXACT_MATCH`, `ASSOCIATIVE`, `LARGEST`,
+  `NODUPLICATES`), which is why modelling coalescing as an enum rather than a
+  bool leaves room to grow.
+
+---
+
+## 3. Common symbols
+
+### Program
+
+`common_a.c` → `int g;`, `common_big.c` → `long long g[4];`,
+`def_g.c` → `int g = 7;`, `weakdef_g.c` → `__attribute__((weak)) int g = 9;`,
+`mainj.c` → `extern int g; int mainCRTStartup(void) { return g; }`
+
+Compiled with `-fcommon`.
+
+### Commands and output
+
+```
+$ for x in common_a common_big def_g weakdef_g; do $NM $x.obj | grep " g$"; done
+00000004 C g          # common_a
+00000020 C g          # common_big
+00000000 D g          # def_g
+00000000 W g          # weakdef_g
+```
+
+```
+$ $LINK /out:o_k1.exe ... mainj.obj common_a.obj common_big.obj   # common + common
+   (no output — success)
+$ $LINK /out:o_k2.exe ... mainj.obj common_a.obj def_g.obj        # common + strong
+   (no output — success)
+$ $LINK /out:o_k3.exe ... mainj.obj weakdef_g.obj common_big.obj  # weak + common
+   (no output — success)
+```
+
+### Inference
+
+COFF accepts all three combinations without diagnostics, matching
+`SymbolTable::addCommon` at `lld/COFF/SymbolTable.cpp:939`, which replaces the
+existing symbol only when the incoming common is larger and otherwise silently
+keeps what it has. Commons never conflict on any of the three platforms.
+
+---
+
+## 4. Undefined symbols
+
+### Program
+
+`undef_main.c`
+```c
+int missing(void);
+int mainCRTStartup(void) { return missing(); }
+```
+
+### Commands and output
+
+```
+$ $LINK /out:o_ku1.exe ... undef_main.obj
+lld-link: error: undefined symbol: missing
+>>> referenced by undef_main.obj:(mainCRTStartup)
+
+$ $LINK /out:o_ku2.exe ... /force:unresolved undef_main.obj
+lld-link: warning: undefined symbol: missing
+>>> referenced by undef_main.obj:(mainCRTStartup)
+
+$ $LINK /dll /out:o_ku3.dll /noentry ... undef_main.obj
+lld-link: error: undefined symbol: missing
+>>> referenced by undef_main.obj:(mainCRTStartup)
+```
+
+### Inference
+
+- An unresolved reference is an error, downgradable to a warning with
+  `/force:unresolved` — a two-state policy, unlike ELF's three-state
+  `--unresolved-symbols`.
+- **Building a DLL does not relax the rule** (KU3), matching Mach-O and
+  differing from ELF, where `-shared` accepted unresolved references. Windows
+  DLLs must be fully resolved at link time.
+
+So on undefined symbols, ELF is the outlier: two of three platforms require
+full resolution regardless of the output artifact kind.
+
+---
+
+## 5. Visibility
+
+COFF has no symbol visibility field at all. Export from a DLL is controlled by
+`__declspec(dllexport)` / `.def` files, which is an unrelated mechanism
+operating per-DLL rather than a per-symbol attribute merged during resolution.
+
+There is therefore no COFF analogue of `EntityVisibility::Hidden` or
+`::Protected`, and no visibility merge rule to emulate. A summary carrying
+anything other than `Default` for a COFF target is not representable on the
+platform.
+
+---
+
+## 6. Second pass: ordering details
+
+Section 3 established that no combination of commons errors, but did not
+establish *which* definition prevails. These probes settle the rank table.
+
+### 6.1 Which definition wins: common vs strong vs weak
+
+`k_read.c`
+```c
+extern int g;
+int mainCRTStartup(void) { return g; }
+```
+
+Payloads are distinguishable: `def_g.c` has `g = 7`, `weakdef_g.c` has `g = 9`,
+and commons are zero-initialised. `/map:` output names the winning object.
+
+```
+$ $LINK /out:o_k2.exe ... /map:k2.map k_read.obj common_a.obj def_g.obj
+$ grep -iE "\bg\b" k2.map
+ 0002:00000000       g            0000000140002000     def_g.obj
+
+$ $OD -s -j .data o_k2.exe
+ 140002000 07000000 00000000                    ........      # value 7 = strong def
+
+$ $LINK /out:o_k3.exe ... /map:k3.map k_read.obj weakdef_g.obj common_big.obj
+$ grep -iE "\bg\b" k3.map
+ 0002:00000000       .weak.g.default   0000000140002000     weakdef_g.obj
+ 0000:00000000       g                 0000000140002020     <common>
+
+$ $LINK /out:o_k1.exe ... /map:k1.map k_read.obj common_a.obj common_big.obj
+$ grep -iE "\bg\b" k1.map
+ 0000:00000000       g            0000000140002020     <common>
+$ $OD -h o_k1.exe | grep .data
+  1 .data         00000000 ...                                # commons live in .bss
+```
+
+Section sizes corroborate: `o_k2` has an 8-byte `.data` (the 4-byte strong
+definition, padded), while `o_k3` has 0x40 — large enough for the 32-byte
+common.
+
+### Inference
+
+- **Strong beats common** (K2: `g` resolves to `def_g.obj`, value 7).
+- **Common beats weak** (K3: `g` resolves to `<common>`, and the weak
+  definition survives only as the alias symbol `.weak.g.default`, unreferenced).
+- **Two commons merge**, taking the larger (K1).
+
+So COFF's binding order matches **ELF**, not Mach-O: `Common` outranks `Weak`.
+Mach-O is the sole platform where a weak definition displaces a common.
+
+### 6.2 Weak declaration + common definition
+
+Same program as the ELF doc section 6.1, compiled with
+`-fcommon -DMAINSYM=mainCRTStartup`.
+
+```
+$ $NM w_weakdecl.obj  | grep -i " g$"
+00000000 W g
+$ $NM w_commondef.obj | grep -i " g$"
+00000004 C g
+
+$ $LINK /out:w_out.exe ... /map:w.map w_rd.obj w_weakdecl.obj w_commondef.obj
+$ grep -iE "\bg\b" w.map
+ 0002:00000000 00000008H .rdata$.refptr.g          DATA
+ 0000:00000000  .weak.g.default.use  0000000000000000  <absolute>
+ 0002:00000000  .refptr.g            0000000140002000  w_weakdecl.obj
+ 0000:00000000  g                    0000000140003000  <common>
+```
+
+**Inference.** `g` resolves to `<common>`; the weak declaration contributes only
+an unreferenced alias. Same conclusion as ELF and Mach-O — the binding join is
+scoped to defining occurrences.
+
+### COFF rank tables
+
+Derived from sections 1, 2, 3 and 6:
+
+| `EntityBinding` | rank | evidence |
+|---|---|---|
+| `Undefined` | 0 | any definition beats an undefined reference (KU1) |
+| `Weak` | 1 | common beats weak (K3) |
+| `Common` | 2 | strong beats common (K2); common beats weak (K3) |
+| `Strong` | 3 | strong wins everything (K2) |
+
+Same as ELF.
+
+| `EntityCoalescing` | rank | evidence |
+|---|---|---|
+| `None` | 0 | COMDAT + regular is an error, so neither "wins" — see below |
+| `ODR` | 1 | COMDAT + COMDAT coalesces (CI1) |
+
+`EntityVisibility`: not representable. Any value other than `Default` for a
+COFF target has no meaning on the platform.
+
+### Conflict predicate
+
+COFF's is **not** derivable from the binding ranks. Collected results:
+
+| case | result | probe |
+|---|---|---|
+| strong + strong | error | C1 |
+| strong + weak | ok | C2/C3 |
+| weak + weak | **error** | C4 |
+| COMDAT + COMDAT | ok | CI1 |
+| COMDAT + regular | error | K4 |
+| common + anything | ok | K1/K2/K3 |
+
+`conflict ⟺ NOT both ODR` fails on strong+weak; `conflict ⟺ neither is weak`
+fails on weak+weak. The rule that fits every row is:
+
+```
+both are definitions
+  AND neither is Common
+  AND NOT (both are ODR)
+  AND NOT (exactly one is Weak)
+```
+
+The weak clauses follow from COFF emulating weak symbols with an alias — the
+diagnostic in C4 names `.weak.f.default`, so two weak definitions collide on
+the alias while one weak plus one strong does not.
+
+This is why `LinkageRules` must expose `isConflictingDefinition` as its own
+virtual rather than deriving conflicts from the rank tables.
+
+---
+
+## 7. Third pass: visibility on common symbols
+
+Section 5 asserted that COFF has no visibility concept. This probe confirms
+that a `visibility("hidden")` attribute has no effect on a COFF target, rather
+than being silently encoded somewhere.
+
+`hc_hidden_common.c`
+```c
+__attribute__((visibility("hidden"))) int g;
+```
+
+`hc_default_common.c`
+```c
+int g;
+```
+
+`hc_read.c`
+```c
+extern int g;
+int MAINSYM(void) { return g; }
+```
+
+Compiled with `-fcommon -DMAINSYM=mainCRTStartup`.
+
+```
+$ $NM w_hc.obj | grep -i " g$"
+00000004 C g                     # identical class to the default-visibility one
+
+$ $LINK /out:w_hc.exe ... /map:whc.map w_rd.obj w_hc.obj w_dc.obj
+$ grep -iE "\bg\b" whc.map
+ 0000:00000000       g            0000000140002000     <common>
+```
+
+### Inference
+
+The hidden common is indistinguishable from a default-visibility common in the
+object file (both plain `C`), and clang emits no diagnostic. Visibility is
+simply dropped on this target.
+
+Confirms that `EntityVisibility` needs no rank table for COFF: any value is
+equivalent to `Default`. A summary carrying `Hidden` or `Protected` for a COFF
+target is a mis-extraction, since clang could not have produced it from source.
+
+---
+
+## 8. Fifth pass: weak declarations are not undefined references
+
+Same program as ELF §9 (`strongdecl.c`, `weakdecl.c`, `dmain.c`), compiled with
+`-DMAINSYM=mainCRTStartup`.
+
+```
+$ $NM w_sd.obj | grep -i " g$"
+         U g                       # undefined
+$ $NM w_wd.obj | grep -i " g$"
+00000000 W g                       # weak external — NOT undefined
+
+$ $RO --symbols w_wd.obj | grep -A10 "Name: g$"
+    Name: g
+    Section: IMAGE_SYM_UNDEFINED (0)
+    StorageClass: WeakExternal (0x69)
+    AuxSymbolCount: 1
+    AuxWeakExternal {
+      Linked: .weak.g.default.use_weak_decl (17)
+      Search: Alias (0x3)
+    }
+
+$ $LINK /out:w_d1.exe ... /map:wd1.map w_dm.obj w_wd.obj w_sd.obj
+   (no output — success, either order)
+$ grep -iE "\bg\b" wd1.map
+ 0000:00000000       g                              0000000000000000  <absolute>
+ 0000:00000000       .weak.g.default.use_weak_decl  0000000000000000  <absolute>
+```
+
+### Inference
+
+**COFF does not error here, unlike ELF and Mach-O.** A COFF weak external is
+not an undefined reference: it carries an `AuxWeakExternal` record naming a
+*default* alias to bind to when no real definition is found. The link succeeds
+with `g` resolved to an absolute 0.
+
+This is the same alias mechanism that makes two weak *definitions* collide
+(§1, C4). COFF's `__attribute__((weak))` is emulation rather than a native
+binding, and it behaves differently from ELF/Mach-O weakness in both
+directions: more permissive for declarations, less permissive for definitions.
+
+For our model this is a Phase 3 concern rather than a join concern — the merged
+binding still follows `argmax strengthRank` — but it means undefined-symbol
+reporting should not assume COFF would have errored where ELF does.
+
+
+
diff --git a/docs/ssaf-linker-elf-behavior.md b/docs/ssaf-linker-elf-behavior.md
new file mode 100644
index 0000000000000..eb7ac558a560b
--- /dev/null
+++ b/docs/ssaf-linker-elf-behavior.md
@@ -0,0 +1,742 @@
+# ELF Linker Behavior — Grounding Probes
+
+Probes run against `ld.lld` (LLD 24.0.0, built from this tree) to ground the
+`LinkageRules` for `Triple::ELF`. Target: `x86_64-unknown-linux-gnu`.
+
+Tools used:
+
+- `CLANG=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/clang`
+- `LLD=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/ld.lld`
+- `RO=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/llvm-readelf`
+- `NM=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/llvm-nm`
+- `OD=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/llvm-objdump`
+
+All objects compiled with:
+
+```
+$CLANG --target=x86_64-unknown-linux-gnu -c -O0 <src> -o <obj>
+```
+
+---
+
+## 1. Binding precedence and the duplicate-definition error
+
+### Program
+
+`strong_a.c`
+```c
+int f(void) { return 1; }
+```
+
+`strong_b.c`
+```c
+int f(void) { return 2; }
+```
+
+`weak_a.c`
+```c
+__attribute__((weak)) int f(void) { return 1; }
+```
+
+`weak_b.c`
+```c
+__attribute__((weak)) int f(void) { return 2; }
+```
+
+`main.c`
+```c
+int f(void);
+int _start(void) { return f(); }
+```
+
+### Commands and output
+
+```
+$ $LLD -o out_p1 main.o strong_a.o strong_b.o
+ld.lld: error: duplicate symbol: f
+>>> defined at strong_a.c
+>>>            strong_a.o:(f)
+>>> defined at strong_b.c
+>>>            strong_b.o:(.text+0x0)
+exit=1
+
+$ $LLD -o out_p2 main.o strong_a.o weak_b.o
+exit=0
+
+$ $LLD -o out_p3 main.o weak_a.o strong_b.o
+exit=0
+
+$ $LLD -o out_p4 main.o weak_a.o weak_b.o
+exit=0
+
+$ $LLD -z muldefs -o out_p1b main.o strong_a.o strong_b.o
+exit=0
+```
+
+Which definition prevailed (`movl $0x1` = `_a`, `movl $0x2` = `_b`):
+
+```
+$ $OD -d --disassemble-symbols=f out_p2
+00000000002011b0 <f>:
+  2011b4: b8 01 00 00 00    movl $0x1, %eax      # strong_a won
+
+$ $OD -d --disassemble-symbols=f out_p3
+00000000002011c0 <f>:
+  2011c4: b8 02 00 00 00    movl $0x2, %eax      # strong_b won
+
+$ $OD -d --disassemble-symbols=f out_p4
+00000000002011b0 <f>:
+  2011b4: b8 01 00 00 00    movl $0x1, %eax      # weak_a won (first)
+
+$ $OD -d --disassemble-symbols=f out_p1b
+00000000002011b0 <f>:
+  2011b4: b8 01 00 00 00    movl $0x1, %eax      # strong_a won (first)
+```
+
+### Inference
+
+- **Conflict predicate: two non-weak definitions.** Only `strong+strong`
+  errors. This matches `Symbol::checkDuplicate` in `lld/ELF/Symbols.cpp:600`:
+  `if (!isWeak() && !other.isWeak()) reportDuplicate(...)`.
+- **Strong beats weak regardless of link order.** P2 (strong first) and P3
+  (weak first) both keep the strong body. So the winner is decided by binding
+  strength, not by position — our `incomingDataWins` must not be
+  order-sensitive when strengths differ.
+- **Ties keep the first definition.** P4 (weak+weak) and P1b (two strongs under
+  `-z muldefs`) both keep the *first* one on the command line. This grounds our
+  tie-break rule of "keep the data already linked".
+- **`-z muldefs` downgrades the error and keeps the first definition.** This is
+  exactly our `WarnOnMultipleDefinitions` flag, and it confirms the semantics
+  we chose: on conflict, ignore the incoming occurrence entirely.
+
+---
+
+## 2. Common symbols
+
+### Program
+
+`common_a.c`
+```c
+int g;
+```
+
+`common_big.c`
+```c
+long long g[4];
+```
+
+`def_g.c`
+```c
+int g = 7;
+```
+
+`weakdef_g.c`
+```c
+__attribute__((weak)) int g = 9;
+```
+
+`mainj.c`
+```c
+extern int g;
+int _start(void) { return g; }
+```
+
+Compiled with `-fcommon` so tentative definitions become real common symbols.
+
+### Commands and output
+
+Symbol classes in the objects (`C` = common, `D` = data, `V` = weak data):
+
+```
+$ $NM common_a.o   | grep " g$"
+0000000000000004 C g
+$ $NM common_big.o | grep " g$"
+0000000000000020 C g
+$ $NM def_g.o      | grep " g$"
+0000000000000000 D g
+$ $NM weakdef_g.o  | grep " g$"
+0000000000000000 V g
+```
+
+Resolution:
+
+```
+$ $LLD -o out_c1 mainj.o common_a.o common_big.o     # common(4) + common(32)
+exit=0
+$ $RO --symbols out_c1 | grep " g$"
+     5: 0000000000203220    32 OBJECT  GLOBAL DEFAULT     5 g
+
+$ $LLD -o out_c2 mainj.o common_a.o def_g.o          # common(4) + strong data(4)
+exit=0
+$ $RO --symbols out_c2 | grep " g$"
+     5: 0000000000203220     4 OBJECT  GLOBAL DEFAULT     5 g
+
+$ $LLD -o out_c2b mainj.o common_big.o def_g.o       # common(32) + strong data(4)
+exit=0
+$ $RO --symbols out_c2b | grep " g$"
+     5: 0000000000203220     4 OBJECT  GLOBAL DEFAULT     5 g
+
+$ $LLD -o out_c3 mainj.o weakdef_g.o common_big.o    # weak data(4) + common(32)
+exit=0
+$ $RO --symbols out_c3 | grep " g$"
+     5: 0000000000203230    32 OBJECT  GLOBAL DEFAULT     6 g
+```
+
+### Inference
+
+- **common + common merges with `size = max`** (C1: 4 and 32 give 32). Matches
+  `Symbol::resolve(const CommonSymbol &)` at `lld/ELF/Symbols.cpp:618-626`.
+- **A strong definition beats common regardless of size** (C2b: common of 32
+  loses to strong data of 4, final size 4). So this is a strength comparison,
+  not a size comparison — `Strong > Common`.
+- **Common beats a weak definition** (C3: final size 32, the common's). This is
+  the `isDefined() && !isWeak()` early-return at `:612` — a *weak* definition
+  does not stop the common from overwriting it. So `Common > Weak`.
+- No case here errors: two commons are legal (only `--warn-common` mentions
+  them). Our conflict predicate must not fire on commons.
+
+Net ELF strength order: **`Undefined < Weak < Common < Strong`**, which is the
+order our `EntityBinding` enum should encode.
+
+---
+
+## 3. Inline functions: how ODR is represented
+
+### Program
+
+`inl.h`
+```c
+inline int inl(void) { return 42; }
+```
+
+`inl_a.cpp`
+```c
+#include "inl.h"
+int use_a(void) { return inl(); }
+```
+
+`inl_b.cpp`
+```c
+#include "inl.h"
+int use_b(void) { return inl(); }
+```
+
+### Commands and output
+
+```
+$ $NM inl_a.o | grep -i inl
+0000000000000000 W _Z3inlv
+
+$ $RO --symbols inl_a.o | grep "_Z3inlv"
+     3: 0000000000000000     0 SECTION LOCAL  DEFAULT     5 .text._Z3inlv
+     5: 0000000000000000    11 FUNC    WEAK   DEFAULT     5 _Z3inlv
+
+$ $RO --section-groups inl_a.o
+COMDAT group section [    4] `.group' [_Z3inlv] contains 1 sections:
+   [Index]    Name
+   [    5]   .text._Z3inlv
+```
+
+### Inference
+
+**There is no `WeakODR` binding on ELF.** An `inline` function is emitted as
+`STB_WEAK` (binding) *plus* a COMDAT group (coalescing). The two properties are
+carried by separate mechanisms in the object file.
+
+This is the direct justification for splitting `EntityBinding` and
+`EntityCoalescing` into independent fields: an ODR definition and a plain
+`__attribute__((weak))` definition have *identical* binding, and differ only in
+whether the copies are guaranteed identical. Ranking `WeakODR` above `Weak`, as
+a single enum forces, models a precedence the linker does not have.
+
+---
+
+## 4. Visibility merging
+
+### Program
+
+`vis_def.c`
+```c
+int v(void) { return 1; }
+```
+
+`vis_hidden_decl.c`
+```c
+__attribute__((visibility("hidden"))) int v(void);
+int caller(void) { return v(); }
+```
+
+`vis_prot_decl.c`
+```c
+__attribute__((visibility("protected"))) int v(void);
+int caller2(void) { return v(); }
+```
+
+Compiled with `-fPIC`; linked with `-shared` so the export table is meaningful.
+
+### Commands and output
+
+```
+$ $LLD -shared -o out_v2.so vis_def.o                      # control: default only
+$ $RO --dyn-syms out_v2.so | grep " v$"
+     1: 0000000000001280    11 FUNC    GLOBAL DEFAULT     6 v
+
+$ $LLD -shared -o out_v1.so vis_def.o vis_hidden_decl.o    # default def + hidden decl
+exit=0
+$ $RO --dyn-syms out_v1.so | grep " v$"
+   (v NOT exported)
+$ $RO --symbols out_v1.so | grep " v$"
+     4: 00000000000012a0    11 FUNC    LOCAL  HIDDEN      6 v
+
+$ $LLD -shared -o out_v3.so vis_def.o vis_prot_decl.o      # default def + protected decl
+exit=0
+$ $RO --dyn-syms out_v3.so | grep " v$"
+     1: 00000000000012c0    11 FUNC    GLOBAL PROTECTED   6 v
+```
+
+### Inference
+
+- **Visibility merges to the most restrictive across *all* occurrences,
+  including ones that lose symbol resolution.** In V1 the definition has
+  default visibility and only an *undefined declaration* is hidden, yet the
+  merged symbol becomes `LOCAL HIDDEN`. This matches `Symbol::mergeProperties`
+  (`:415`) and the visibility handling at the top of each `resolve()` overload:
+  `setVisibility(v == STV_DEFAULT ? ov : std::min(v, ov))` over
+  `STV_DEFAULT=0 < STV_INTERNAL=1 < STV_HIDDEN=2 < STV_PROTECTED=3`.
+
+  Note the ELF encoding orders `PROTECTED` *above* `HIDDEN` numerically but
+  `min` is applied, and `DEFAULT` is special-cased — so the effective
+  restrictiveness order is `DEFAULT < PROTECTED < HIDDEN`, matching our
+  `EntityVisibility` numbering with `std::max`.
+
+- **This is the grounding for "Hidden blocks cross-LU resolution".** A hidden
+  symbol is removed from the dynamic symbol table entirely: it resolves within
+  the link unit and is invisible past its boundary. Our LU analogue is that a
+  `Hidden` entity must not be resolvable by a later link stage.
+
+- **`Protected` is exported but distinct from `Default`.** V3 keeps `v` in the
+  dynamic table with `PROTECTED`, meaning callers inside the DSO bind directly
+  and it cannot be interposed. This is a real, ELF-only third state — it is not
+  representable on Mach-O or COFF.
+
+---
+
+## 5. Undefined symbols
+
+### Program
+
+`undef_main.c`
+```c
+int missing(void);
+int _start(void) { return missing(); }
+```
+
+`undef_weak_main.c`
+```c
+__attribute__((weak)) int maybe_missing(void);
+int _start(void) { return maybe_missing ? maybe_missing() : 0; }
+```
+
+### Commands and output
+
+```
+$ $LLD -o out_u1 undef_main.o                                  # strong ref, executable
+ld.lld: error: undefined symbol: missing
+>>> referenced by undef_main.c
+>>>               undef_main.o:(_start)
+exit=1
+
+$ $LLD -o out_u2 undef_weak_main.o                             # weak ref, executable
+exit=0
+
+$ $LLD -shared -o out_u3.so undef_main.o                       # strong ref, shared lib
+exit=0
+
+$ $LLD --unresolved-symbols=ignore-all -o out_u4 undef_main.o
+exit=0
+
+$ $LLD --warn-unresolved-symbols -o out_u5 undef_main.o
+ld.lld: warning: undefined symbol: missing
+>>> referenced by undef_main.c
+>>>               undef_main.o:(_start)
+exit=0
+```
+
+### Inference
+
+- **An unresolved strong reference is an error when linking an executable**
+  (U1), but **not** when linking a shared library (U3), where it is expected to
+  be satisfied at load time.
+- **An unresolved *weak* reference is never an error** (U2) — it resolves to
+  address 0 and the program tests for it.
+- The policy is configurable three ways (`ReportError`/`Warn`/`Ignore`, see
+  `UnresolvedPolicy` in `lld/ELF/Config.h:115`), and ELF keeps **two separate
+  policies**: `unresolvedSymbols` and `unresolvedSymbolsInShlib` (`:468-469`).
+
+For our linker this means undefined-symbol reporting needs:
+
+1. A tri-state policy, not a bool — matching `--unresolved-symbols`.
+2. Suppression for entities whose merged binding is `Weak` (an undefined weak
+   reference is legal by design).
+3. Awareness that "is this LU a final executable or an intermediate library?"
+   changes the answer. An LU that is later combined with others is the
+   shared-library case, where unresolved references are normal.
+
+Point 3 is significant for SSAF: a single LU is almost always an intermediate
+artifact, so the default should be permissive, with the strict check applied
+only when the link unit is known to be complete.
+
+---
+
+## 6. Second pass: ordering details
+
+These probes were added to pin down the exact rank tables for `LinkageRules`,
+rather than inferring them from the LLD source.
+
+### 6.1 Weak declaration + common definition
+
+The binding join must not simply take the strongest binding across *all*
+occurrences, because a declaration's binding describes how the reference binds,
+not the definition. This probe isolates that case.
+
+`weakdecl.c`
+```c
+__attribute__((weak)) extern int g;
+int use(void) { return g; }
+```
+
+`commondef.c`
+```c
+int g;
+```
+
+`rd.c`
+```c
+extern int g; int use(void);
+int MAINSYM(void) { return g + use(); }
+```
+
+Compiled with `-fcommon -DMAINSYM=_start`.
+
+```
+$ $NM e_weakdecl.o  | grep " g$"
+                 w g                      # lowercase w = weak UNDEFINED
+$ $NM e_commondef.o | grep " g$"
+0000000000000004 C g
+
+$ $LLD -o e_out e_rd.o e_weakdecl.o e_commondef.o
+exit=0
+$ $RO --symbols e_out | grep " g$"
+     5: 0000000000203270     4 OBJECT  GLOBAL DEFAULT     5 g
+```
+
+**Inference.** The merged symbol is `GLOBAL`, not `WEAK`: the weak *declaration*
+contributed nothing to the binding. The definition alone determined it.
+
+This confirms the binding join must be scoped to defining occurrences:
+
+```
+if any occurrence is a Definition:  join over the defining occurrences only
+else:                               join over the declarations
+```
+
+Without the gate, a Mach-O-style ordering (where `Weak` outranks `Common`)
+would produce `Weak` here, wrongly describing a common definition as weak.
+
+### 6.2 Protected vs Hidden restrictiveness
+
+Section 4 established `Default` is least restrictive, but compared each of
+`Hidden` and `Protected` only against `Default`. This probe compares them
+directly, in both orders.
+
+`vp_def_prot.c`
+```c
+__attribute__((visibility("protected"))) int v(void) { return 1; }
+```
+
+`vp_decl_hidden.c`
+```c
+__attribute__((visibility("hidden"))) int v(void);
+int c1(void) { return v(); }
+```
+
+`vp_def_hidden.c`
+```c
+__attribute__((visibility("hidden"))) int v(void) { return 1; }
+```
+
+`vp_decl_prot.c`
+```c
+__attribute__((visibility("protected"))) int v(void);
+int c2(void) { return v(); }
+```
+
+```
+$ $LLD -shared -o o_vp1.so vp_def_prot.o vp_decl_hidden.o     # protected def + hidden decl
+$ $RO --symbols o_vp1.so | grep " v$"
+     4: 00000000000012a0    11 FUNC    LOCAL  HIDDEN      6 v
+
+$ $LLD -shared -o o_vp2.so vp_def_hidden.o vp_decl_prot.o     # hidden def + protected decl
+$ $RO --symbols o_vp2.so | grep " v$"
+     4: 00000000000012a0    11 FUNC    LOCAL  HIDDEN      6 v
+```
+
+**Inference.** `Hidden` wins over `Protected` in both orders, so the merge is
+commutative and the ELF restrictiveness order is:
+
+    Default < Protected < Hidden
+
+Note this is *not* the numeric order of the ELF constants
+(`STV_DEFAULT=0, STV_INTERNAL=1, STV_HIDDEN=2, STV_PROTECTED=3`) — LLD applies
+`min` with `STV_DEFAULT` special-cased, which yields the order above. The rank
+table must encode the observed behaviour, not the ELF constant values.
+
+### ELF rank tables
+
+Derived from sections 1, 2, 4 and 6:
+
+| `EntityBinding` | rank | evidence |
+|---|---|---|
+| `Undefined` | 0 | any definition beats an undefined reference (U1/U2) |
+| `Weak` | 1 | common beats weak def (C3) |
+| `Common` | 2 | strong beats common (C2b), common beats weak (C3) |
+| `Strong` | 3 | duplicate error only for strong+strong (P1) |
+
+| `EntityVisibility` | rank | evidence |
+|---|---|---|
+| `Default` | 0 | V1: hidden decl demotes default def |
+| `Protected` | 1 | VP1/VP2: loses to hidden |
+| `Hidden` | 2 | VP1/VP2: wins both orders |
+
+Conflict predicate: **both occurrences are definitions and neither is `Weak`**
+(P1 errors; P2/P3/P4 do not; commons never conflict).
+
+---
+
+## 7. Third pass: visibility on common symbols
+
+Section 4 established the visibility merge using function definitions. This
+probe confirms it applies to commons too, which are handled by a separate
+`resolve()` overload in LLD.
+
+`hc_hidden_common.c`
+```c
+__attribute__((visibility("hidden"))) int g;
+```
+
+`hc_default_common.c`
+```c
+int g;
+```
+
+Compiled with `-fcommon -fPIC`.
+
+```
+$ $NM e_hc.o | grep " g$"
+0000000000000004 C g
+
+$ $LLD -shared -o e_hc.so e_hc.o e_dc.o
+$ $RO --symbols e_hc.so | grep " g$"
+     4: 00000000000032b8     4 OBJECT  LOCAL  HIDDEN      8 g
+$ $RO --dyn-syms e_hc.so | grep " g$"
+   (NOT exported)
+```
+
+### Inference
+
+A hidden common merged with a default-visibility common yields `LOCAL HIDDEN`
+and is not exported — the same most-restrictive rule as for function
+definitions. This matches `Symbol::resolve(const CommonSymbol &)` at
+`lld/ELF/Symbols.cpp:607-611`, which applies the identical
+`setVisibility(v == STV_DEFAULT ? ov : std::min(v, ov))` step before any
+common-specific handling.
+
+So on ELF the visibility rule is uniform across all symbol kinds, and our
+single `visibilityRank` table is sufficient. (Mach-O is not uniform — see §7.2
+of the Mach-O doc.)
+
+---
+
+## 8. Fourth pass: ODR definition vs regular definition
+
+This is the case that decides whether the extractor can be target-independent.
+COFF errors here (COFF §2, probe K4); this probe establishes what ELF does.
+
+### Program
+
+`inl.h`
+```c
+inline int q(void) { return 42; }
+```
+
+`e_inl.cpp`
+```c
+#include "inl.h"
+int use_inl(void) { return q(); }
+```
+
+`e_inl_b.cpp`
+```c
+#include "inl.h"
+int use_inl_b(void) { return q(); }
+```
+
+`e_regular.cpp`
+```c
+int q(void) { return 99; }
+int use_reg(void) { return 0; }
+```
+
+`e_main.cpp`
+```c
+int use_inl(void); int use_reg(void);
+extern "C" int _start(void) { return use_inl() + use_reg(); }
+```
+
+### Commands and output
+
+```
+$ $NM e_inl.o     | grep _Z1qv
+0000000000000000 W _Z1qv                  # weak + COMDAT
+$ $NM e_regular.o | grep _Z1qv
+0000000000000000 T _Z1qv                  # strong
+
+$ $LLD -o o_ei1 e_main.o e_inl.o e_regular.o        # ODR + regular strong
+exit=0
+$ $OD -d --disassemble-symbols=_Z1qv o_ei1
+  201234: b8 63 00 00 00    movl $0x63, %eax        # 99 — the regular def won
+
+$ $LLD -o o_ei2 e_main2.o e_inl.o e_inl_b.o         # ODR + ODR
+exit=0
+```
+
+### Inference
+
+**ELF does not error on an ODR definition colliding with a regular
+definition** — the regular (strong) definition simply wins, because the inline
+was lowered to `STB_WEAK`. Two ODR definitions also link cleanly.
+
+Contrast COFF probe K4, where the identical source is a `duplicate symbol`
+error, because COFF keeps the inline definition `Strong` and relies on COMDAT
+to license duplicates.
+
+The same probe on Mach-O (`arm64-apple-macosx`, same sources) also links
+cleanly with the regular definition winning:
+
+```
+$ ld -o o_mi1 m_main.o m_inl.o m_regular.o ...
+ld exit=0
+$ $OD -d --disassemble-symbols=__Z1qv o_mi1
+100000370: 52800c60    mov w0, #0x63    ; =99
+```
+
+### Consequence for the model
+
+The three platforms disagree on this case *only because they lower `inline`
+differently*, not because they disagree about C++ semantics. The summary should
+therefore record the source-level fact — `Strong` + `ODR`, which is what COFF
+encodes directly — and let `LinkageRules` perform the platform lowering:
+
+    effectiveBinding(Strong, ODR) = Weak     on ELF and Mach-O
+    effectiveBinding(Strong, ODR) = Strong   on COFF
+
+With that, one conflict predicate per platform reproduces all four observed
+rows without the extractor needing to know the target:
+
+| case | effective bindings | predicate | observed |
+|---|---|---|---|
+| ELF: ODR + regular | `Weak` vs `Strong` | no conflict | EI1 ok |
+| ELF: ODR + ODR | `Weak` vs `Weak` | no conflict | EI2 ok |
+| COFF: ODR + regular | `Strong` vs `Strong`, not both ODR | conflict | K4 error |
+| COFF: ODR + ODR | `Strong` vs `Strong`, both ODR | no conflict | CI1 ok |
+
+---
+
+## 9. Fifth pass: two declarations with differing bindings
+
+The only join case left unprobed: a strong and a weak *declaration* of the same
+entity, with no definition anywhere in the link.
+
+### Program
+
+`strongdecl.c`
+```c
+extern int g;
+int use_strong_decl(void) { return g; }
+```
+
+`weakdecl.c`
+```c
+__attribute__((weak)) extern int g;
+int use_weak_decl(void) { return g ? 1 : 0; }
+```
+
+`dmain.c`
+```c
+int use_strong_decl(void); int use_weak_decl(void);
+int MAINSYM(void) { return use_strong_decl() + use_weak_decl(); }
+```
+
+`wonly_main.c`
+```c
+int use_weak_decl(void);
+int _start(void) { return use_weak_decl(); }
+```
+
+Compiled with `-DMAINSYM=_start`.
+
+### Commands and output
+
+```
+$ $NM e_sd.o | grep " g$"
+                 U g                       # uppercase U = undefined strong
+$ $NM e_wd.o | grep " g$"
+                 w g                       # lowercase w = undefined WEAK
+
+$ $LLD -o e_d1 e_dm.o e_wd.o e_sd.o        # weak decl FIRST
+ld.lld: error: undefined symbol: g
+>>> referenced by strongdecl.c
+>>>               e_sd.o:(use_strong_decl)
+>>> referenced by weakdecl.c
+
+$ $LLD -o e_d2 e_dm.o e_sd.o e_wd.o        # strong decl FIRST
+ld.lld: error: undefined symbol: g
+   (identical diagnostic)
+```
+
+Control — the weak declaration alone is not an error:
+
+```
+$ $LLD -o e_d3 e_wm.o e_wd.o
+exit=0
+$ $RO --symbols e_d3 | grep " g$"
+     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT   UND g
+```
+
+The merged binding, read from a shared library where unresolved references are
+permitted:
+
+```
+$ $LLD -shared -o e_d4.so e_dm.o e_sd.o e_wd.o
+$ $RO --dyn-syms e_d4.so | grep " g$"
+     1: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT   UND g
+```
+
+### Inference
+
+**Strong wins over weak among declarations, in either order.** D4 shows the
+merged undefined symbol is `GLOBAL`, not `WEAK`, and D1/D2 show the resulting
+error is order-independent. The control (D3) confirms a weak-only reference is
+legal, so the error in D1/D2 is caused by the strong reference dominating.
+
+This validates the plan's assumption that two declarations join by
+`argmax strengthRank`, with `Undefined < Weak` on every platform.
+
+It also settles the one place the result is observable: Phase 3's
+undefined-symbol reporting suppresses the diagnostic when the merged binding is
+`Weak`. Because a strong reference dominates, an entity referenced weakly by
+one TU and strongly by another is correctly reported — matching D1/D2.
+
+
+
+
diff --git a/docs/ssaf-linker-implementation-plan.md b/docs/ssaf-linker-implementation-plan.md
new file mode 100644
index 0000000000000..f524d82f16bed
--- /dev/null
+++ b/docs/ssaf-linker-implementation-plan.md
@@ -0,0 +1,607 @@
+# SSAF Entity Linker — Implementation Plan
+
+Collected from the design discussion and grounded in the probe results in
+`ssaf-linker-elf-behavior.md`, `ssaf-linker-macho-behavior.md` and
+`ssaf-linker-coff-behavior.md`.
+
+Nothing in phases 1–5 has been started. Phase 0 is already in the working tree.
+
+---
+
+## Phase 0 — already landed (working tree, uncommitted)
+
+For context, since later phases modify this code:
+
+- `WarnOnMultipleDefinitions` constructor flag (default: fail) plus
+  `clang-ssaf-linker --warn-on-multiple-definitions`. On conflict the incoming
+  occurrence is ignored entirely and the first definition stands.
+- Reconciliation split into `isConflictingDefinition`, `incomingDataWins`,
+  `mergeLinkage`, with `reportIfLinkageIsNotExternal` /
+  `reportIfDefinitionsConflict` guards.
+- Stateless pipeline: `resolve()` returns
+  `pair<EntityResolutionMap, DataSelectionMap>`; no per-link member state.
+- `EntityLinker` public surface reduced to constructor, `link()`,
+  `takeOutput()`; reconciliation rules private with `friend class TestFixture`.
+- Readability pass: `fatal()`, `lookupOrFatal()`, `insertOrFatal()`,
+  `isExternal()`, `mergeEntityData()`, `mergeSummaryData()`,
+  `checkTUNotAlreadyLinked()`, invariant asserts, section banners.
+- Lit coverage for both multiple-definition modes; `tu-2.json` corrected so the
+  shared external entity is a declaration rather than a second definition.
+
+**Loose end:** `clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-redefines-shared-ext.json`
+is untracked. It must be `git add`-ed or the two multiple-definition lit cases
+fail for everyone else.
+
+**To be reverted in Phase 1:** the explicit `= 0,1,2…` numbering added to
+`EntityBinding`, `EntityVisibility` and `EntityDefinitionKind`, and the
+`std::max` joins that depend on it. Superseded by the decision to move all
+ordering into `LinkageRules`.
+
+---
+
+## Phase 1 — split the axes, remove orderings from the enums
+
+### 1.1 `Model/EntityLinkage.h`
+
+Drop `WeakODR`; add `EntityCoalescing`; strip all numeric values and
+"ordered by…" wording. Every enum keeps `: uint8_t` for size only.
+
+```cpp
+/// Symbol scope. Enumerator order is insignificant: the linker never compares
+/// these values, and namespace resolution switches on them by name.
+enum class EntityLinkageType : uint8_t { None, Internal, External };
+
+/// Symbol strength: which definition prevails when two collide.
+///
+/// Enumerator order is insignificant. Precedence differs per platform — on
+/// Mach-O a weak definition displaces a common, on ELF and COFF the common
+/// wins — so comparisons must go through LinkageRules::strengthRank().
+enum class EntityBinding : uint8_t { Undefined, Weak, Common, Strong };
+
+/// How far a symbol escapes its link unit.
+///
+/// Enumerator order is insignificant. ELF merges to the most restrictive,
+/// Mach-O to the least, and COFF has no visibility at all; see
+/// LinkageRules::visibilityRank().
+enum class EntityVisibility : uint8_t { Default, Hidden, Protected };
+
+/// Whether every definition of the entity is required to be identical, as a
+/// COMDAT group (ELF/COFF) or .weak_definition (Mach-O) guarantees.
+///
+/// Independent of EntityBinding: an inline function is Weak+ODR on ELF and
+/// Mach-O but Strong+ODR on COFF. Modelled as an enum rather than a bool so
+/// COFF's other IMAGE_COMDAT_SELECT_* kinds can be added later.
+enum class EntityCoalescing : uint8_t { None, ODR };
+
+/// Whether this occurrence defines the entity or merely declares it.
+enum class EntityDefinitionKind : uint8_t { Declaration, Definition };
+```
+
+`EntityLinkage` gains a fifth field `Coalescing`; constructor arity 4 → 5;
+update `operator==`, `operator<<`, and add
+`FIELD(EntityLinkage, Coalescing)` to `Model/PrivateFieldNames.def`.
+
+### 1.2 `Core/ModelStringConversions.h`
+
+Remove the `WeakODR` to/from-string cases. Add
+`entityCoalescingToString` / `entityCoalescingFromString` ("None" / "ODR").
+
+### 1.3 Serialization — `JSONFormat/JSONFormatImpl.{h,cpp}`
+
+Add a **required** `"coalescing"` key alongside `"binding"`, `"visibility"`,
+`"definition"`, with an `InvalidEntityCoalescing` error message following the
+existing pattern.
+
+> **Breaking format change.** Any summary produced before this lands becomes
+> unreadable. There is no version field in the JSON schema to negotiate this.
+
+### 1.4 Extractor — `TUSummary/TUSummaryExtractor.cpp`
+
+**Target-independent.** The extractor records the *source-level* fact; the
+platform lowering happens in `LinkageRules` (§2.2).
+
+The three formats encode identical C++ semantics — `inline` means one entity,
+defined identically in every TU, duplicates permitted — and differ only in how
+they express it. COFF's encoding is the semantically faithful one: the binding
+stays `Strong` and a separate COMDAT mechanism licenses the duplicates. ELF and
+Mach-O have no general coalescing mechanism, so they *overload weak binding* to
+obtain duplicate tolerance. That is an encoding trick, not a claim that inline
+functions are interposable, so it does not belong in the summary.
+
+Normalized mapping, identical for every target:
+
+| source construct | `Binding` | `Coalescing` |
+|---|---|---|
+| ordinary definition | `Strong` | `None` |
+| `inline` / template / vtable (`GVA_*ODR`) | `Strong` | `ODR` |
+| `__attribute__((weak))` | `Weak` | `None` |
+| C tentative definition | `Common` | `None` |
+| declaration only | `Undefined` | `None` |
+
+So `mapBinding` changes only by returning `Strong` where it previously returned
+`WeakODR`, and a new `mapCoalescing(const NamedDecl *, ASTContext &)` returns
+`ODR` for the two `GVA_*ODR` linkages. `__attribute__((weak))` continues to be
+tested first, before the ODR switch.
+
+Visibility needs no target awareness either: clang's AST is already
+target-configured. The Mach-O probe showed `visibility("protected")` producing
+`-Wunsupported-visibility` and being downgraded at compile time, so
+`ND->getVisibility()` already returns a target-legal value.
+
+### 1.5 Fixtures
+
+67 JSON files under `clang/test/Analysis/Scalable/` contain `"binding"` and
+need a `"coalescing"` key added. Purely additive, scriptable; per the
+fixture-editing convention, insert in place without reordering existing keys.
+
+### 1.6 Tests
+
+`EntityLinkageTest.cpp` and `EntityLinkerTest.cpp`: 5-argument construction,
+updated stream/format assertions, updated `def`/`decl` helpers.
+
+---
+
+## Phase 2 — `LinkageRules`
+
+### 2.1 Interface — `Core/EntityLinker/LinkageRules.{h,cpp}`
+
+```cpp
+/// The platform-specific symbol resolution rules the linker emulates.
+///
+/// Ranks answer "which value wins the join", not "which value is semantically
+/// greater". They are deliberately inverted for some fields on some targets.
+class LinkageRules {
+public:
+  virtual ~LinkageRules() = default;
+
+  /// Returns the rules for \p T, dispatching on its object format.
+  static const LinkageRules &forTarget(const llvm::Triple &T);
+
+  virtual llvm::StringRef getName() const = 0;
+
+  /// Lowers a source-level (binding, coalescing) pair to the binding this
+  /// platform actually emits. ELF and Mach-O have no general coalescing
+  /// mechanism and encode ODR definitions as weak; COFF keeps them strong and
+  /// licenses duplicates via COMDAT. All ranking and conflict detection
+  /// operates on the result of this call, never on the raw binding.
+  virtual EntityBinding effectiveBinding(EntityBinding B,
+                                         EntityCoalescing C) const = 0;
+
+  virtual unsigned strengthRank(EntityBinding) const = 0;
+  virtual unsigned visibilityRank(EntityVisibility) const = 0;
+  virtual unsigned coalescingRank(EntityCoalescing) const = 0;
+  virtual unsigned definitionKindRank(EntityDefinitionKind) const = 0;
+
+  /// Not derivable from the ranks — see the COFF table below.
+  virtual bool isConflictingDefinition(const EntityLinkage &Current,
+                                       const EntityLinkage &Incoming) const = 0;
+
+  /// Normalizes \p Linkage to what this platform can represent.
+  ///
+  /// Values the platform silently drops are coerced to their platform
+  /// equivalent; values the platform's toolchain could never have produced are
+  /// a fatal error, since they can only come from a corrupted or hand-edited
+  /// summary. See §2.7.
+  virtual EntityLinkage normalize(const EntityLinkage &Linkage,
+                                  const EntityName &Name) const = 0;
+};
+```
+
+`forTarget` dispatches on `Triple::getObjectFormat()`; unsupported formats are
+a fatal error naming the triple.
+
+### 2.2 `effectiveBinding` — the platform lowering
+
+| target | `(Strong, ODR)` | everything else |
+|---|---|---|
+| ELF | `Weak` | identity |
+| Mach-O | `Weak` | identity |
+| COFF | `Strong` | identity |
+
+Evidence: ELF §3 (`inline` → `W` + COMDAT group), Mach-O §4 (`WeakDef` flag),
+COFF §2 (`T` + `IMAGE_SCN_LNK_COMDAT`, `Selection: Any`).
+
+This single virtual is what keeps the extractor target-independent, and it
+reproduces every observed ODR-collision result (ELF §8):
+
+| case | effective bindings | predicate | observed |
+|---|---|---|---|
+| ELF: ODR + regular | `Weak` vs `Strong` | no conflict | EI1 links, regular wins |
+| ELF: ODR + ODR | `Weak` vs `Weak` | no conflict | EI2 links |
+| Mach-O: ODR + regular | `Weak` vs `Strong` | no conflict | MI1 links, regular wins |
+| COFF: ODR + regular | `Strong` vs `Strong`, not both ODR | conflict | K4 errors |
+| COFF: ODR + ODR | `Strong` vs `Strong`, both ODR | no conflict | CI1 links |
+
+
+### 2.3 Rank tables (all evidence-backed)
+
+**Binding** — Mach-O inverts `Weak`/`Common`:
+
+| | ELF | Mach-O | COFF |
+|---|---|---|---|
+| `Undefined` | 0 | 0 | 0 |
+| `Weak` | 1 | **2** | 1 |
+| `Common` | 2 | **1** | 2 |
+| `Strong` | 3 | 3 | 3 |
+
+Evidence: ELF §2 C3 (common beats weak), Mach-O §3 MC3 (weak beats common,
+with a warning), COFF §6.1 K3 (common beats weak).
+
+**Visibility** — Mach-O inverts the direction entirely:
+
+| | ELF | Mach-O | COFF |
+|---|---|---|---|
+| `Default` | 0 | **1** | 0 |
+| `Protected` | 1 | n/a | 0 |
+| `Hidden` | 2 | **0** | 0 |
+
+Evidence: ELF §4 V1 + §6.2 VP1/VP2 (most restrictive wins,
+`Default < Protected < Hidden`), Mach-O §2 W1–W3 (least restrictive wins),
+COFF §7 (visibility dropped entirely, so all ranks equal).
+
+**Coalescing** — ranked so that a plain `max` yields the conservative meet:
+`ODR` survives only if *every* copy is ODR.
+
+| | all targets |
+|---|---|
+| `ODR` | 0 |
+| `None` | 1 |
+
+This looks inverted and is intentional: ranks are join-winners, and "not
+guaranteed identical" must win over "guaranteed identical".
+
+**DefinitionKind** — uniform: `Declaration` 0, `Definition` 1.
+
+### 2.4 Conflict predicates
+
+All predicates operate on `effectiveBinding(Binding, Coalescing)`, never on the
+raw binding.
+
+ELF and Mach-O:
+
+```
+both are definitions AND neither effective binding is Weak
+```
+
+An ODR definition lowers to `Weak` on these targets, so it never conflicts —
+matching ELF §8 EI1/EI2 and Mach-O §8 MI1 — without the predicate mentioning
+coalescing at all.
+
+COFF — not derivable from ranks. From the six observed rows in COFF §6:
+
+```
+both are definitions
+  AND neither is Common
+  AND NOT (both are ODR)
+  AND NOT (exactly one is Weak)
+```
+
+The weak clauses follow from COFF emulating weak symbols with an alias
+(`.weak.f.default` appears in the C4 diagnostic), so two weak definitions
+collide on the alias while one weak plus one strong does not. Note this is
+about COFF's lowering of `__attribute__((weak))`, and is unrelated to how ODR
+definitions are represented.
+
+### 2.5 Rewiring `EntityLinker`
+
+- Constructor computes `const LinkageRules &Rules = LinkageRules::forTarget(TargetTriple)`
+  and stores the reference.
+- The three reconciliation functions stay `static` but take
+  `const LinkageRules &` as their first parameter. This keeps them pure and
+  lets the unit tests sweep all three platforms. `TestFixture`'s accessors gain
+  the same parameter.
+- `mergeLinkage` becomes rank-driven, with the **definition-kind gate** the
+  probes established (ELF §6.1, Mach-O §7.1, COFF §6.2 — all three agree a
+  declaration contributes nothing to the binding):
+
+```
+Visibility     = argmax visibilityRank over both occurrences
+DefinitionKind = argmax definitionKindRank over both occurrences
+
+if both are definitions:
+    Binding    = argmax strengthRank over both
+    Coalescing = argmax coalescingRank over both
+elif exactly one is a definition:
+    Binding, Coalescing = that occurrence's
+else:  // two declarations
+    Binding    = argmax strengthRank over both
+    Coalescing = None
+```
+
+Without the gate, joining a weak *declaration* with a common *definition* on
+Mach-O would yield `Weak`, which no linker does.
+
+- `incomingDataWins` uses `strengthRank` instead of `>` on the enum.
+- `isConflictingDefinition` delegates to `Rules`.
+- The `mergeLinkage` same-linkage-type assert stays.
+
+Each `LinkageRules` subclass carries a comment with the minimal reproducer from
+the probe docs justifying its rule, per the agreed convention.
+
+### 2.6 Mach-O: order-dependent visibility on commons
+
+**Mach-O visibility on commons is order-dependent** (Mach-O §7.2):
+
+| inputs | ld-prime result |
+|---|---|
+| hidden common first, then default | private |
+| default common first, then hidden | exported |
+| both hidden | private |
+| both default | exported |
+
+The merged visibility is simply that of the **first** common. `addCommon`
+(`lld/MachO/SymbolTable.cpp:251`) carries the `isPrivateExtern` of whichever
+common it keeps; only `addDefined`'s weak-def coalescing path performs the
+`privateExtern &= isPrivateExtern` merge.
+
+**Decision: reproduce it faithfully, and warn.**
+
+An earlier draft proposed approximating this commutatively to keep
+`mergeLinkage` order-independent. That is the wrong trade. The LU is *already*
+order-dependent — `incomingDataWins` keeps the first definition on ties, which
+every platform does (ELF P4, Mach-O M4, COFF C4) — so commutativity of
+`mergeLinkage` was a local property, not a guarantee about the link unit.
+Approximating would produce `Default` where ld-prime gives private whenever the
+hidden common is linked first, which is simply a wrong answer for any analysis
+consuming the summary.
+
+Because the case indicates genuine ambiguity in the user's program, it is also
+reported:
+
+```
+warning: visibility of common symbol 'g' differs between translation units;
+         the linked result depends on link order
+```
+
+Warned by default, unlike the COFF coercion in §2.7 — two commons disagreeing
+on visibility is rare and always worth surfacing, whereas COFF visibility
+coercion would fire on every portable header.
+
+#### Interface consequence
+
+A plain `visibilityRank` cannot express this, because the rule depends on the
+*bindings*, not just the visibilities. So visibility gets a merge function with
+a rank-based default:
+
+```cpp
+  /// Merges the visibility of two occurrences. The base implementation takes
+  /// the higher visibilityRank(); MachOLinkageRules overrides it for commons.
+  virtual EntityVisibility mergeVisibility(const EntityLinkage &Current,
+                                           const EntityLinkage &Incoming) const;
+
+  /// True if this pair's merge depends on link order, so the caller can warn.
+  virtual bool isOrderDependentMerge(const EntityLinkage &Current,
+                                     const EntityLinkage &Incoming) const;
+```
+
+- ELF, COFF: base implementation, `isOrderDependentMerge` always false.
+- Mach-O: if both occurrences are `Common` definitions, return
+  `Current.Visibility`; otherwise the base implementation.
+  `isOrderDependentMerge` returns true when both are `Common` definitions and
+  their visibilities differ.
+
+`resolveEntity` calls `reportIfOrderDependentMerge(...)` alongside the existing
+`reportIfDefinitionsConflict(...)` guard.
+
+#### Test consequence
+
+`mergeLinkage` loses its blanket "commutative, associative and idempotent"
+doc-comment claim. The unit-test sweeps become per-platform, and for Mach-O the
+commutativity sweep must exclude — or better, explicitly assert the
+order-dependence of — pairs of commons with differing visibility. Idempotence
+still holds everywhere (merging a linkage with itself never hits the divergent
+case, since equal visibilities give the same answer in both orders).
+
+
+### 2.7 Inexpressible values — `normalize()`
+
+Not every `EntityLinkage` a summary can carry is representable on every target.
+The behaviour differs by case, because *why* a value is unrepresentable differs.
+
+Probed by dumping the AST for the same source across all three targets
+(`clang -Xclang -ast-dump`, ELF doc §8 workspace):
+
+| target | `Hidden` in AST | `Protected` in AST | diagnostic |
+|---|---|---|---|
+| ELF | `Hidden` | `Protected` | — |
+| Mach-O | `Hidden` | **`Default`** | `-Wunsupported-visibility` |
+| COFF | **`Hidden`** | **`Protected`** | **none** |
+
+Two distinct situations follow:
+
+**COFF `Hidden` / `Protected` → coerce to `Default`.** Clang accepts both
+silently on Windows and drops them at emission (COFF §7: a hidden common is an
+indistinguishable plain `C`). Portable code carries these attributes
+unconditionally:
+
+```c
+__attribute__((visibility("hidden"))) int helper(void) { return 1; }
+```
+
+so a faithful COFF extraction legitimately contains `Hidden`. Rejecting it
+would fail valid input, and would do so in the linker — far from the cause,
+about a value the compiler never complained about. Coercion mirrors exactly
+what the object file does. Report it only under the existing verbose flag; a
+default-on warning would fire on every portable header.
+
+**Mach-O `Protected` → fatal error.** Clang cannot produce it — it warns and
+downgrades to `Default` at compile time — so its presence in a summary means
+the summary was hand-edited or corrupted. That is precisely the case where a
+loud failure is wanted. Message should name the entity and the target, and use
+the non-prefixed style of `MultipleDefinition` rather than the
+`EntityLinkerFatalErrorPrefix` style, since it is a bad-input error rather than
+a logic bug.
+
+`normalize()` returns the adjusted linkage and is called once per occurrence in
+`resolveEntity`, before any ranking or conflict detection, so that all
+downstream rules see only platform-legal values.
+
+Note this keeps `mapVisibility` in the extractor target-independent (§1.4): the
+extractor records what the source said, and the linker normalizes for the
+target. That preserves the property that one extraction can be reinterpreted
+for a different target.
+
+### 2.8 Target triple validation
+
+**Required by this phase, not optional.** `LinkageRules::forTarget` selects one
+rule set from `Output.TargetTriple` and applies it to every linked TU. If a TU
+was extracted for a different target, the linker silently applies the wrong
+rules — and since COFF and Mach-O disagree on both the conflict predicate and
+the `Weak`/`Common` order, the result would be wrong with no diagnostic.
+
+`TUSummaryEncoding` already stores the triple and exposes `getTargetTriple()`,
+and `EntityLinker`'s constructor already documents "every linked TU must report
+the same triple". This closes the gap between that claim and the behaviour.
+
+Add to `checkTUNotAlreadyLinked`, or a sibling guard called from `link()`
+before `resolve()`:
+
+```cpp
+llvm::Error EntityLinker::checkTUTargetMatches(const llvm::Triple &TUTriple);
+```
+
+**Comparison strictness.** Exact string equality is too strict — it would
+reject `arm64-apple-macosx14.0` against `arm64-apple-macosx15.0`, which are
+identical for linkage purposes. Compare only the components that select rules
+and affect resolution: architecture, vendor, OS and object format, ignoring the
+version. This mirrors the existing key used by `MultiArchSharedLibrary` and
+`MultiArchStaticLibrary`, which sort members on
+`(getArch(), getSubArch(), getVendor(), getOS(), getEnvironment(), getObjectFormat())`.
+
+Returns `llvm::Error`, not a fatal error: it is bad input, and `link()` already
+returns errors for the analogous duplicate-TU-namespace case. Message should
+name both triples and the offending TU namespace.
+
+**Interaction with `clang-ssaf-linker`.** The tool currently hardcodes
+`llvm::Triple("arm64-apple-macosx")` when constructing the linker, with a TODO
+noting that architecture tracking is deferred. With this check in place, that
+hardcoded triple would reject every non-Mach-O summary. The tool must instead
+derive the LU triple from the first input summary and let the check validate
+the rest — a small change to `runLink`, but a required part of this phase.
+
+---
+
+## Phase 3 — finalization: undefined symbols and hidden demotion
+
+Both rules need the *complete* LU, since a later TU can change the answer — a
+hidden declaration in TU5 demotes a definition from TU1 (ELF §4 V1). So they
+run once at end of link, not per TU. Add a private `finalize()` called from
+`takeOutput()`.
+
+### 3.1 Undefined-symbol reporting
+
+For each LU entity whose merged `DefinitionKind` is `Declaration`, report under
+a tri-state policy (`Ignore` / `Warn` / `Error`), mirroring ELF's
+`--unresolved-symbols`. Suppress when the merged binding is `Weak` — an
+undefined weak reference is legal by design (ELF §5 U2).
+
+**Default: `Ignore`.** The per-target defaults would be actively wrong here. A
+real linker keys strictness on the artifact kind — ELF allows unresolved
+references in a shared library but not an executable (§5 U1 vs U3), while
+Mach-O and COFF reject them even for dylib/DLL (Mach-O §5 MU3, COFF §4 KU3). An
+SSAF link unit is almost always an *intermediate* artifact, so defaulting to
+strict would fire constantly on correct input. Expose
+`--unresolved-symbols=ignore|warn|error` and let the caller assert completeness.
+
+> **Limitation to document:** we do not track references. "Declaration
+> everywhere" is inferred from the entity's presence in the id table, which
+> means some TU mentioned it — close to, but not identical to, "referenced".
+
+### 3.2 Hidden blocks cross-LU resolution
+
+The faithful analogue of ELF turning a hidden symbol `LOCAL` and dropping it
+from `.dynsym`: for each LU entity whose merged linkage is `External` and
+merged visibility ranks as hidden on the target, rewrite its `LinkageTable`
+entry to `Internal`.
+
+Demote the **linkage type only, never the name**. Renaming would invalidate
+every `EntityId` already patched into the summary data. It is also unnecessary:
+external names are already LU-qualified, so `(f, LU1)` and `(f, LU2)` cannot
+collide.
+
+> **No consumer yet.** Nothing currently links LU summaries to each other —
+> `MultiArch*` only bundles. So this is unobservable until an LU-combining
+> stage exists. It is still worth doing now: it is the step that makes
+> `Visibility` load-bearing rather than write-only, and doing it at
+> finalization is the only correct point.
+
+---
+
+## Phase 4 — dropped-data mismatch check (on your signal)
+
+Previously parked over the multi-format `equals` question; `EntityCoalescing`
+now resolves *when* to check, and the format question still needs an answer.
+
+When a duplicate external entity's summary data is dropped, patch the dropped
+encoding into LU id space, compare it against the retained one, and report
+mismatches. Gate on **`Coalescing == ODR`**: for ODR entities all copies are
+required to be identical, so a difference is a real ODR violation or an
+extraction bug. For plain `Weak`, differing definitions are legal and the same
+check would be pure noise.
+
+Mechanics settled earlier: stash dropped encodings during `merge`, add the ones
+from the incoming TU to `PatchTargets` so they are patched uniformly, and
+compare after `patch()` completes. Never patch twice.
+
+Still open: `EntitySummaryEncoding::equals` needs to establish the concrete type
+without RTTI (`LLVM_ENABLE_RTTI=OFF`). Options previously laid out: a per-class
+type tag virtual (recommended), an unchecked `static_cast` with a documented
+precondition, or LLVM-style RTTI with a closed kind enum.
+
+---
+
+## Phase 5 — lit fixtures
+
+Three small hand-written fixtures, split by rule rather than by permutation,
+since the reconciliation unit tests already cover the algebra exhaustively:
+
+1. **`linkage-carry-through.test`** — one TU, ~6 entities, every enum value
+   appearing at least once in a realistic combination, verifying each survives
+   read → link → write.
+2. **`join-winner.test`** — two TUs, ~6 entities, one per branch of the
+   definition-kind × binding decision, each carrying a distinguishing summary
+   blob so the output proves which TU's data survived.
+3. **`join-visibility.test`** — two TUs, the visibility meets, with binding
+   pinned so the fixture stays legal.
+
+With `LinkageRules` in place these should be parameterized by target triple, so
+the same entity shapes are checked against all three rule sets — in particular
+the Mach-O binding inversion and the COFF COMDAT conflict rule.
+
+---
+
+## Suggested order
+
+1. Phase 1 (model) — everything else depends on the enum shape.
+2. Phase 2 (`LinkageRules`) — no point writing rule tables against an enum
+   that is about to change.
+3. Phase 5 fixtures for phases 1–2.
+4. Phase 3 (finalization) — independent, additive.
+5. Phase 4 (mismatch check) — on your signal.
+
+Phases 1 and 2 are one atomic change from the tests' point of view: between
+them the linker has no ordering to work with.
+
+---
+
+## Open questions blocking a clean start
+
+All resolved.
+
+- **Inexpressible values** — coerce on COFF, fatal for Mach-O `Protected`
+  (§2.7).
+- **Mach-O common visibility** — reproduce the order-dependence faithfully and
+  warn (§2.6).
+- **Target triple validation** — required, since it selects the rule set
+  (§2.8).
+- **Two declarations with differing bindings** — probed (ELF §9, Mach-O §8,
+  COFF §8). `argmax strengthRank` confirmed; ELF shows the merged undefined
+  symbol is `GLOBAL`, so a strong reference dominates a weak one in either
+  order.
+
+One finding from the last probe to carry into Phase 3: **a COFF weak
+declaration is not an undefined reference.** It is a `WeakExternal` with an
+`AuxWeakExternal` default-alias record, so COFF links successfully where ELF
+and Mach-O both error (COFF §8). Undefined-symbol reporting must not assume
+every platform would have failed where ELF does.
+
diff --git a/docs/ssaf-linker-macho-behavior.md b/docs/ssaf-linker-macho-behavior.md
new file mode 100644
index 0000000000000..a12f30cbac1c2
--- /dev/null
+++ b/docs/ssaf-linker-macho-behavior.md
@@ -0,0 +1,662 @@
+# Mach-O Linker Behavior — Grounding Probes
+
+Probes run against Apple's system linker (`ld-1221.4`, i.e. ld-prime — the real
+production linker, not LLD) to ground the `LinkageRules` for `Triple::MachO`.
+Target: `arm64-apple-macosx`.
+
+Tools used:
+
+- `CLANG=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/clang`
+- `LD=/usr/bin/ld` (`ld -v` → `PROGRAM:ld PROJECT:ld-1221.4`)
+- `NM=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/llvm-nm`
+- `RO=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/llvm-readobj`
+- `OD=/Volumes/avirals-external-drive-1/aviralg-llvm-project/bin/llvm-objdump`
+- `SDK=$(xcrun --show-sdk-path)`
+
+All objects compiled with:
+
+```
+$CLANG --target=arm64-apple-macosx -c -O0 <src> -o <obj>
+```
+
+All links use the helper:
+
+```sh
+run() {
+  ld -o "$1" "${@:2}" -syslibroot "$SDK" -lSystem -arch arm64 2>/tmp/err.txt
+  local rc=$?
+  grep -v "newer 'macOS'" /tmp/err.txt   # filter SDK-version noise
+  echo "ld exit=$rc"
+}
+```
+
+---
+
+## 1. Binding precedence and the duplicate-definition error
+
+### Program
+
+`strong_a.c`
+```c
+int f(void) { return 1; }
+```
+
+`strong_b.c`
+```c
+int f(void) { return 2; }
+```
+
+`weak_a.c`
+```c
+__attribute__((weak)) int f(void) { return 1; }
+```
+
+`weak_b.c`
+```c
+__attribute__((weak)) int f(void) { return 2; }
+```
+
+`main.c`
+```c
+int f(void);
+int main(void) { return f(); }
+```
+
+### Commands and output
+
+How weakness is encoded in the object file:
+
+```
+$ $RO --syms strong_a.o | grep -A6 "Name: _f"
+    Name: _f (1)
+    Extern
+    Type: Section (0xE)
+    Section: __text (0x1)
+    RefType: UndefinedNonLazy (0x0)
+    Flags [ (0x0)
+    ]
+
+$ $RO --syms weak_a.o | grep -A6 "Name: _f"
+    Name: _f (1)
+    Extern
+    Type: Section (0xE)
+    Section: __text (0x1)
+    RefType: UndefinedNonLazy (0x0)
+    Flags [ (0x80)
+      WeakDef (0x80)
+```
+
+Resolution:
+
+```
+$ ld -o out_m1 main.o strong_a.o strong_b.o ...
+duplicate symbol '_f' in:
+    /private/tmp/ssaf-probe/macho/strong_b.o
+    /private/tmp/ssaf-probe/macho/strong_a.o
+ld: 1 duplicate symbols
+exit=1
+
+$ ld -o out_m2 main.o strong_a.o weak_b.o ...      # strong + weak
+exit=0
+$ ld -o out_m3 main.o weak_a.o strong_b.o ...      # weak + strong
+exit=0
+$ ld -o out_m4 main.o weak_a.o weak_b.o ...        # weak + weak
+exit=0
+```
+
+Which definition prevailed (`mov w0, #0x1` = `_a`, `#0x2` = `_b`):
+
+```
+$ $OD -d --disassemble-symbols=_f out_m2
+100000348: 52800020    mov w0, #0x1     # strong_a won
+$ $OD -d --disassemble-symbols=_f out_m3
+100000348: 52800040    mov w0, #0x2     # strong_b won
+$ $OD -d --disassemble-symbols=_f out_m4
+100000430: 52800020    mov w0, #0x1     # weak_a won (first)
+```
+
+### Inference
+
+Identical to ELF on all three counts:
+
+- **Conflict predicate: two non-weak definitions.** Matches
+  `SymbolTable::addDefined` in `lld/MachO/SymbolTable.cpp:113-156`, which
+  pushes a `dupSymDiags` entry only in the `else` branch where neither the
+  existing nor the incoming definition is weak.
+- **Strong beats weak regardless of order** (M2 and M3 both keep the strong
+  body).
+- **Ties keep the first definition** (M4).
+
+Note: the ld64 `-m` flag (allow multiple definitions) is obsolete in ld-prime —
+`ld: warning: -m is obsolete` and the duplicate error still fires. So on Mach-O
+there is no supported way to downgrade the duplicate-symbol error, unlike ELF's
+`-z muldefs`. Our `WarnOnMultipleDefinitions` flag is therefore an
+SSAF-specific affordance on this platform, not an emulation of a linker option.
+
+---
+
+## 2. Visibility: `privateExtern`, and the divergence from ELF
+
+### Program
+
+`pe_hidden.c`
+```c
+__attribute__((visibility("hidden"))) int v(void) { return 1; }
+```
+
+`pe_default.c`
+```c
+int v(void) { return 1; }
+```
+
+`pe_prot.c`
+```c
+__attribute__((visibility("protected"))) int v(void) { return 1; }
+```
+
+Two weak definitions differing in visibility:
+
+`wd_hidden.c`
+```c
+__attribute__((weak, visibility("hidden"))) int w(void) { return 1; }
+int anchor_a(void) { return 0; }
+```
+
+`wd_hidden2.c`
+```c
+__attribute__((weak, visibility("hidden"))) int w(void) { return 2; }
+int anchor_b(void) { return 0; }
+```
+
+`wd_default.c`
+```c
+__attribute__((weak)) int w(void) { return 2; }
+int anchor_b(void) { return 0; }
+```
+
+`wd_main.c`
+```c
+int w(void);
+int anchor_a(void); int anchor_b(void);
+int main(void) { return w() + anchor_a() + anchor_b(); }
+```
+
+### Commands and output
+
+`protected` is rejected outright at compile time:
+
+```
+$ $CLANG --target=arm64-apple-macosx -c -O0 pe_prot.c -o pe_prot.o
+pe_prot.c:1:16: warning: target does not support 'protected' visibility;
+                         using 'default' [-Wunsupported-visibility]
+    1 | __attribute__((visibility("protected"))) int v(void) { return 1; }
+      |                ^
+```
+
+`hidden` becomes the `PrivateExtern` bit, not a visibility field:
+
+```
+$ $RO --syms pe_hidden.o | grep -A8 "Name: _v"
+    Name: _v (1)
+    PrivateExtern
+    Extern
+    ...
+
+$ $RO --syms pe_default.o | grep -A8 "Name: _v"
+    Name: _v (1)
+    Extern
+    ...
+```
+
+Coalescing two weak definitions with differing visibility:
+
+```
+$ ld -o out_w1 wd_main.o wd_hidden.o wd_default.o ...   # hidden first
+$ $NM -m out_w1 | grep " _w$"
+000000010000045c (__TEXT,__text) weak external _w
+
+$ ld -o out_w2 wd_main.o wd_default.o wd_hidden.o ...   # default first
+$ $NM -m out_w2 | grep " _w$"
+0000000100000454 (__TEXT,__text) weak external _w
+
+$ ld -o out_w3 wd_main.o wd_hidden.o wd_hidden2.o ...   # BOTH hidden
+$ $NM -m out_w3 | grep " _w$"
+000000010000036c (__TEXT,__text) non-external (was a private external) _w
+```
+
+### Inference
+
+- **Mach-O has no visibility enum — only a `PrivateExtern` bit.** There are two
+  states, not three. `Protected` is not merely unused on this target: clang
+  refuses to emit it and silently downgrades to `default`.
+
+- **Visibility merges to the LEAST restrictive, the opposite of ELF.** W1 and
+  W2 (one hidden, one default, either order) both produce an *exported* symbol;
+  only W3 (both hidden) stays private. This is
+  `defined->privateExtern &= isPrivateExtern` at
+  `lld/MachO/SymbolTable.cpp:118` — private only if *every* copy is private.
+
+  Compare ELF probe V1, where a hidden *declaration* was enough to make a
+  default-visibility definition `LOCAL HIDDEN`. The two platforms take opposite
+  directions, so this rule cannot be shared.
+
+- Order does not matter (W1 == W2), so the rule is still commutative — just
+  `min`-of-restrictiveness rather than `max`.
+
+---
+
+## 3. Common symbols
+
+### Program
+
+`common_a.c` → `int g;`, `common_big.c` → `long long g[4];`,
+`def_g.c` → `int g = 7;`, `weakdef_g.c` → `__attribute__((weak)) int g = 9;`,
+`mainj.c` → `extern int g; int main(void) { return g; }`
+
+Compiled with `-fcommon`.
+
+### Commands and output
+
+```
+$ $NM common_a.o   | grep " _g$"
+0000000000000004 C _g
+$ $NM common_big.o | grep " _g$"
+0000000000000020 C _g
+$ $NM def_g.o      | grep " _g$"
+0000000000000000 D _g
+$ $NM weakdef_g.o  | grep " _g$"
+0000000000000000 D _g
+```
+
+```
+$ run out_mc1 mainj.o common_a.o common_big.o        # common(4) + common(32)
+ld exit=0
+$ $NM -m out_mc1 | grep " _g$"
+0000000100004000 (__DATA,__common) external _g
+
+$ run out_mc2 mainj.o common_a.o def_g.o             # common(4) + strong data(4)
+ld exit=0
+$ $NM -m out_mc2 | grep " _g$"
+0000000100004000 (__DATA,__data) external _g
+
+$ run out_mc2b mainj.o common_big.o def_g.o          # common(32) + strong data(4)
+ld exit=0
+$ $NM -m out_mc2b | grep " _g$"
+0000000100004000 (__DATA,__data) external _g
+
+$ run out_mc3 mainj.o weakdef_g.o common_big.o       # weak data(4) + common(32)
+ld: warning: tentative definition of '_g' with size 32 from
+    '.../common_big.o' is being replaced by real definition of smaller
+    size 4 from '.../weakdef_g.o'
+ld exit=0
+$ $NM -m out_mc3 | grep " _g$"
+0000000100008000 (__DATA,__data) weak external _g
+```
+
+### Inference
+
+- **Two commons merge, no error** (MC1), as on ELF.
+- **A strong definition beats common regardless of size** (MC2b), as on ELF.
+- **A WEAK definition also beats common** (MC3) — and Mach-O *warns* about the
+  size shrink. **This is a divergence from ELF**, where the common won over a
+  weak definition (ELF probe C3). It matches `lld/MachO/SymbolTable.cpp:259`,
+  where `addCommon` returns early for any `isa<Defined>(s)` without consulting
+  weakness, versus ELF's `isDefined() && !isWeak()` guard at
+  `lld/ELF/Symbols.cpp:612`.
+
+So the strength order differs between platforms:
+
+- ELF: `Undefined < Weak < Common < Strong`
+- Mach-O: `Undefined < Common < Weak < Strong`
+
+A single global `EntityBinding` ordering cannot express both. The comparison
+must live in `LinkageRules`, not in the enum's numeric values.
+
+---
+
+## 4. Inline functions: how ODR is represented
+
+### Program
+
+`inl.h`
+```c
+inline int inl(void) { return 42; }
+```
+
+`inl_a.cpp` / `inl_b.cpp` — each `#include "inl.h"` and call `inl()`.
+
+### Commands and output
+
+```
+$ $NM -m inl_a.o | grep -i inlv
+0000000000000014 (__TEXT,__text) weak external __Z3inlv
+
+$ $RO --syms inl_a.o | grep -A8 "Name: __Z3inlv"
+    Name: __Z3inlv (1)
+    Extern
+    Type: Section (0xE)
+    Section: __text (0x1)
+    RefType: UndefinedNonLazy (0x0)
+    Flags [ (0x80)
+      WeakDef (0x80)
+    ]
+```
+
+### Inference
+
+**On Mach-O an ODR definition and a plain `__attribute__((weak))` definition
+are byte-for-byte identical in the symbol table** — both are just `WeakDef`
+(0x80). Compare section 1, where `weak_a.o`'s `_f` carried exactly the same
+flag.
+
+Unlike ELF, there is not even a COMDAT group to distinguish them: the ODR
+guarantee is entirely absent from the object file. This reinforces that
+`Coalescing` is *not* recoverable from a linker's view of Mach-O — it is
+front-end knowledge that SSAF must carry explicitly if it wants it, which is
+precisely the argument for a separate `EntityCoalescing` field sourced from
+clang's `GVA_*ODR` linkage rather than inferred from the binding.
+
+---
+
+## 5. Undefined symbols
+
+### Program
+
+`undef_main.c`
+```c
+int missing(void);
+int main(void) { return missing(); }
+```
+
+`undef_weak_main.c`
+```c
+__attribute__((weak_import)) int maybe_missing(void);
+int main(void) { return maybe_missing ? maybe_missing() : 0; }
+```
+
+### Commands and output
+
+```
+$ run out_mu1 undef_main.o                              # strong ref, executable
+Undefined symbols for architecture arm64:
+  "_missing", referenced from:
+      _main in undef_main.o
+ld: symbol(s) not found for architecture arm64
+ld exit=1
+
+$ run out_mu2 undef_weak_main.o                         # weak_import ref, executable
+Undefined symbols for architecture arm64:
+  "_maybe_missing", referenced from:
+      _main in undef_weak_main.o
+ld: symbol(s) not found for architecture arm64
+ld exit=1
+
+$ ld -dylib -o out_mu3.dylib undef_main.o ...           # strong ref, DYLIB
+ld exit=1
+Undefined symbols for architecture arm64:
+  "_missing", referenced from:
+      _main in undef_main.o
+
+$ ld -o out_mu4 undef_main.o -undefined dynamic_lookup ...
+ld exit=0
+```
+
+### Inference
+
+Mach-O is **stricter than ELF** in two ways that matter to us:
+
+- **A dylib with unresolved references is an error** (MU3), whereas ELF happily
+  produced `out_u3.so` (ELF probe U3). Mach-O requires every symbol to be
+  resolved at static-link time unless told otherwise; ELF defers to load time.
+- **`weak_import` does not by itself excuse an entirely absent symbol** (MU2).
+  It marks a symbol that may be missing *at runtime* when it comes from a
+  dylib; with no definition anywhere in the link, it is still an error.
+
+`-undefined dynamic_lookup` (MU4) is the escape hatch, analogous to ELF's
+`--unresolved-symbols=ignore-all`.
+
+For our linker: the "is this LU final or intermediate?" distinction that ELF
+makes via shared-vs-executable does **not** hold on Mach-O. If we want one
+policy across platforms, an explicit flag is the honest way to express it,
+rather than inferring permissiveness from the artifact kind.
+
+---
+
+## 6. Second pass: ordering details
+
+### 6.1 Weak declaration + common definition
+
+Same program as the ELF doc section 6.1 (`weakdecl.c`, `commondef.c`, `rd.c`),
+compiled with `-fcommon -DMAINSYM=main`.
+
+```
+$ $NM m_weakdecl.o  | grep " _g$"
+                 U _g                     # plain undefined
+$ $NM m_commondef.o | grep " _g$"
+0000000000000004 C _g
+
+$ ld -o m_out m_rd.o m_weakdecl.o m_commondef.o ...
+ld exit=0
+$ $NM -m m_out | grep " _g$"
+0000000100004000 (__DATA,__common) external _g
+```
+
+**Inference.** The merged symbol is the common definition, plain `external`,
+with no trace of the weak declaration. Same conclusion as ELF: the binding join
+is scoped to defining occurrences.
+
+This case matters more on Mach-O than on ELF, because Mach-O ranks `Weak` above
+`Common` (section 3). Without the definition-kind gate, joining a weak
+declaration with a common definition would yield `Weak`, which the linker
+demonstrably does not do.
+
+Note also that clang did not even mark the reference weak here — `nm` shows a
+plain `U`, not a weak undefined — so on Mach-O `__attribute__((weak))` on a
+declaration is largely inert.
+
+### Mach-O rank tables
+
+Derived from sections 1, 2, 3 and 6:
+
+| `EntityBinding` | rank | evidence |
+|---|---|---|
+| `Undefined` | 0 | any definition beats an undefined reference (MU1) |
+| `Common` | 1 | weak def replaces common, with a warning (MC3) |
+| `Weak` | 2 | strong beats weak (M2/M3); weak beats common (MC3) |
+| `Strong` | 3 | duplicate error only for strong+strong (M1) |
+
+**This inverts `Weak` and `Common` relative to ELF and COFF** — the single
+strongest argument for moving the ordering out of the enum and into
+`LinkageRules`.
+
+| `EntityVisibility` | rank | evidence |
+|---|---|---|
+| `Default` | 1 | W1/W2: hidden + default coalesce to *exported* |
+| `Hidden` | 0 | W3: private only when every copy is private |
+| `Protected` | — | not representable; clang downgrades to `Default` |
+
+**The visibility ranks are inverted relative to ELF**: here the *least*
+restrictive wins, so `Default` outranks `Hidden`. Expressing both platforms
+with one "take the greater rank" join is exactly why the ranks must be supplied
+per-target rather than baked into the enum's numeric values.
+
+Conflict predicate: **both occurrences are definitions and neither is `Weak`**
+(M1 errors; M2/M3/M4 do not) — the same predicate as ELF, unlike COFF.
+
+---
+
+## 7. Third pass: coalescing and visibility-on-commons
+
+### 7.1 Is an ODR definition distinguishable from a plain weak definition?
+
+`inl.h`
+```c
+inline int q(void) { return 42; }
+```
+
+`mo_inl.cpp`
+```c
+#include "inl.h"
+int use_inl(void) { return q(); }
+```
+
+`mo_weak.cpp`
+```c
+__attribute__((weak)) int q(void) { return 7; }
+int use_weak(void) { return 0; }
+```
+
+`mo_strong.cpp`
+```c
+int q(void) { return 99; }
+int use_strong(void) { return 0; }
+```
+
+`mo_main2.cpp` calls `use_inl() + use_weak()`; `mo_main3.cpp` calls
+`use_inl() + use_strong()`.
+
+```
+$ $NM -m mo_inl.o    | grep __Z1qv
+0000000000000014 (__TEXT,__text) weak external __Z1qv
+$ $NM -m mo_weak.o   | grep __Z1qv
+0000000000000000 (__TEXT,__text) weak external __Z1qv
+$ $NM -m mo_strong.o | grep __Z1qv
+0000000000000000 (__TEXT,__text) external __Z1qv
+```
+
+```
+$ run o_mq1 mo_main2.o mo_inl.o mo_weak.o          # ODR(=42) + weak(=7)
+ld exit=0
+$ $OD -d --disassemble-symbols=__Z1qv o_mq1
+100000458: 52800540    mov w0, #0x2a    ; =42      # inline won (first)
+
+$ run o_mq2 mo_main3.o mo_inl.o mo_strong.o        # ODR(=42) + strong(=99)
+ld exit=0
+$ $OD -d --disassemble-symbols=__Z1qv o_mq2
+100000370: 52800c60    mov w0, #0x63    ; =99      # strong won
+```
+
+### Inference
+
+**On Mach-O an ODR definition is behaviourally identical to a plain weak
+definition.** The symbol table entries are byte-identical (`weak external` for
+both), and resolution treats them the same: first-wins against another weak
+definition (MQ1), loses to a strong definition (MQ2).
+
+So Mach-O needs no `EntityCoalescing` rank at all — the field is inert for
+resolution on this platform. It remains worth carrying in the summary because
+it is front-end knowledge the object format discards, and because it gates the
+summary-mismatch check (a differing summary between two ODR copies is an ODR
+violation; between two plain weak copies it is legal).
+
+Contrast COFF, where coalescing *is* the conflict predicate.
+
+### 7.2 Visibility on common symbols — order-dependent
+
+Section 2 established that two weak *definitions* merge to the least
+restrictive visibility. Commons behave differently.
+
+`hc_hidden_common.c`
+```c
+__attribute__((visibility("hidden"))) int g;
+```
+
+`hc_default_common.c`
+```c
+int g;
+```
+
+`hc_read.c`
+```c
+extern int g;
+int MAINSYM(void) { return g; }
+```
+
+Compiled with `-fcommon -DMAINSYM=main`.
+
+```
+$ $RO --syms m_hc.o | grep -A3 "Name: _g"
+    Name: _g (1)  PrivateExtern  Extern  Type: Undef (0x0)
+
+$ ld -o m_hc_out  m_rd.o m_hc.o m_dc.o ...        # hidden common FIRST
+ld exit=0
+$ $NM -m m_hc_out | grep " _g$"
+0000000100004000 (__DATA,__common) non-external (was a private external) _g
+
+$ ld -o m_hc_out3 m_rd.o m_dc.o m_hc.o ...        # default common FIRST
+ld exit=0
+$ $NM -m m_hc_out3 | grep " _g$"
+0000000100004000 (__DATA,__common) external _g
+
+$ ld -o m_hc_out2 m_rd.o m_hc.o m_hc2.o ...       # both hidden
+0000000100004000 (__DATA,__common) non-external (was a private external) _g
+
+$ ld -o m_hc_out4 m_rd.o m_dc.o m_dc2.o ...       # both default
+0000000100004000 (__DATA,__common) external _g
+```
+
+### Inference
+
+**For common symbols, Mach-O visibility is order-dependent**: hidden-first
+yields a private symbol, default-first yields an exported one. This is the
+opposite of the weak-definition case in section 2, where `hidden + default`
+gave an exported symbol in *either* order.
+
+The rule is not a commutative merge at all — `addCommon`
+(`lld/MachO/SymbolTable.cpp:251`) simply carries the `isPrivateExtern` of
+whichever common it keeps, with no `&=` step. Only `addDefined`'s weak-def
+coalescing path performs the `privateExtern &= isPrivateExtern` merge.
+
+**Implication for our model.** We cannot faithfully reproduce this, because our
+`mergeLinkage` is deliberately commutative and order-independent — a property
+we want, since a link unit's linkage record should not depend on input order.
+Emulating Mach-O exactly would make the LU summary order-dependent for this one
+case.
+
+The honest choice is to deviate deliberately and document it: apply the
+least-restrictive rule uniformly on Mach-O (matching the weak-definition case,
+which is by far the common one) and accept that a `hidden common + default
+common` pair yields `Default` where ld-prime would give private if the hidden
+one happened to be linked first. This is a case where being order-independent
+is worth more to a summary format than bug-compatibility with a link-order
+artifact.
+
+> **Superseded.** The implementation plan (§2.6) now reproduces this faithfully
+> and warns, rather than approximating commutatively. The LU is already
+> order-dependent via `incomingDataWins`'s first-wins tie-break, so
+> commutativity of `mergeLinkage` was never a guarantee about the link unit.
+
+---
+
+## 8. Fifth pass: two declarations with differing bindings
+
+Same program as ELF §9 (`strongdecl.c`, `weakdecl.c`, `dmain.c`), compiled with
+`-DMAINSYM=main`.
+
+```
+$ $NM m_sd.o | grep " _g$"
+                 U _g
+$ $NM m_wd.o | grep " _g$"
+                 U _g               # NOT marked weak
+
+$ ld -o m_d1 m_dm.o m_wd.o m_sd.o ...
+ld exit=1
+Undefined symbols for architecture arm64:
+  "_g", referenced from:
+      _use_weak_decl in m_wd.o
+      _use_strong_decl in m_sd.o
+```
+
+### Inference
+
+Both declarations produce a plain `U` — clang does not mark a weak *declaration*
+weak on Mach-O at all, corroborating the same observation in §6.1. There is
+consequently nothing to join: the merged binding is `Undefined` either way, and
+the link fails because the symbol has no definition.
+
+Mach-O agrees with ELF on the outcome (error) but for a different reason: on
+ELF the strong reference dominates a genuinely-weak one, whereas here neither
+reference was weak to begin with.
+
+
+



More information about the cfe-commits mailing list