[clang] [compiler-rt] [llvm] [Clang][SanitizerCoverage] Document trace-args and trace-ret (PR #227614)

Yunseong Kim via cfe-commits cfe-commits at lists.llvm.org
Thu Oct 22 02:15:39 PDT 2026


https://github.com/yskzalloc updated https://github.com/llvm/llvm-project/pull/227614

>From 5bf51adf06357975e722e6cc7e658e6d359e443d Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 23 Aug 2026 12:27:09 +0200
Subject: [PATCH 1/4] [SanitizerCoverage] Add trace-args and trace-ret
 instrumentation

Add two SanitizerCoverage modes that report the values flowing in and out
of instrumented functions, selected through SanitizerCoverageOptions
(TraceArgs/TraceRet) and, for opt, the -sanitizer-coverage-trace-args and
-sanitizer-coverage-trace-ret fields of InstrumentationOptions.td.
trace-args emits one call per IR argument of an instrumented function on
entry, trace-ret one before every return:

  void __sanitizer_cov_trace_args(u32 arg_idx, u32 size, u64 val,
                                  u64 *offsets, u32 num_fields);
  void __sanitizer_cov_trace_ret (u32 size, u64 val, u64 *offsets,
                                  u32 num_fields);

The consumers are the Linux kernel's KCOV dataflow subsystem and libFuzzer
value profiling, which use the values as coverage: a call reaching a
function with an argument value never seen there before is new coverage,
which drives a fuzzer towards the state a bug needs rather than merely
towards the code containing it.

Both modes instrument function boundaries rather than blocks, so they
stand on their own: the module pass no longer bails out on SCK_None when
either is requested, and neither one triggers the implicit trace-pc-guard
default. Without this, `opt -passes=sancov-module
-sanitizer-coverage-trace-args` produced a cleanly linking but entirely
uninstrumented module. Block coverage alongside them has to be asked for
explicitly.

The IR decides, debug information does not
------------------------------------------

The design rule is:

  Debug information may enrich the interpretation of an observation,
  but must never determine which instrumentation is emitted.

What is observed, how many calls are emitted, their operands, and which
object tables exist are decided by the LLVM IR alone. The pass consults
no DILocalVariable, DICompositeType, #dbg_value or #dbg_declare record,
inline frame or source parameter number. Debug information supplies a
location for the inserted calls - the function's own opening line, as
the verifier requires of a call in a function with a subprogram - and
nothing else, so a build with -g emits exactly the same calls, with the
same operands and the same machine code, as a build without it.

Observations
------------

`arg_idx` is the position of the argument in the IR parameter list: the
ABI's view of the signature, not the source-language parameter number.

- A struct-return (sret) pointer is where the return value goes, which
  trace-ret reports, and the Swift error slot may only be loaded, stored
  or passed on; neither is observed, but both keep their positions, so
  `f(ptr sret %0, i64 %1)` reports %1 with arg_idx 1.
- Integers, pointers, floating-point values and vectors are reported by
  value (num_fields == 0): the low `size` bytes, zero-extended. A pointer
  is the address it holds, with the width of its address space; a
  floating-point value or a vector is its bit pattern; a value wider than
  64 bits is its low 64 bits.
- A first-class aggregate has no address, so each element is reported by
  a call of its own, all under the aggregate's arg_idx. AArch64 passes
  `struct mix { double d; long l; }` as `[2 x i64] %0`: two calls with
  arg_idx 0. x86-64 passes the same struct as `double %0, i64 %1`: two
  IR arguments.
- An argument the IR marks as the address of an object in memory -
  byval, byref, inalloca, preallocated - and the sret buffer of an
  indirect return are reported as that object (num_fields != 0): `val`
  is its address and `offsets` points at a constant table of
  {byte offset, byte size} pairs, one per element of its IR type, nested
  structs included, so a consumer can read them out of it. The table is
  a private unnamed_addr constant, one per IR type. No pair is wider
  than 8 bytes: an element that is, such as an array, is described in
  8-byte pieces, so a runtime that reads every pair into a 64-bit word
  reads all of the object. That matters most for rustc, which types
  every object it passes or returns in memory as an opaque byte array -
  sret([32 x i8]) for a struct of four u64 - and of which a single
  32-byte pair would have yielded only the first 8 bytes.
- `size == 0` means there was nothing to report: a void return, a value
  with no fixed-size bit pattern, such as a scalable vector, or an object
  the function does not use.

Pointee snapshots
-----------------

Only an explicit IR dereferenceability contract causes the
instrumentation runtime to read behind a pointer. A pointer argument the
IR says is dereferenceable(N) or dereferenceable_or_null(N) on entry,
and a pointer return value the
function's return attributes say is at the return, are reported with
what they point to: `val` stays the pointer, `size` is the number of
bytes, min(N, -sanitizer-coverage-trace-pointee-bytes), 64 by default,
and `offsets` covers them in 8-byte pieces - the table of [size x i8].
Those are the only points at which the IR guarantees the bytes may be
read, and nothing but the IR attribute bounds the read. The attribute is
a readable range, not a type: the bytes are a raw, bounded snapshot,
which a consumer cuts into the members of the pointee with the binary's
debug information. Debug information never decides whether, or how much,
is read, and no readable range is inferred from accesses, object sizes,
callers or what must execute. A dereferenceable_or_null pointer may be null, which the runtime
has to check; a pointer the function does not use is reported by value
only, as callers may pass poison for it, and so is one in a non-default
address space, which the runtime may not be able to read through.
-sanitizer-coverage-trace-pointee-bytes=0 reports every pointer by
value.

Which pointers carry the attribute is up to the front end. clang gives
C++ references, `this` and a struct the ABI passes by address (AArch64,
over 16 bytes) dereferenceable(sizeof(T)), and a C array parameter of
static size (`T p[static 1]`) its size; a plain C pointer has none.
rustc gives &T and &mut T arguments dereferenceable(size_of::<T>()) and
Option<&T> dereferenceable_or_null, but not Box<T>, raw pointers, slice
data, & of a !Freeze or &mut of a !Unpin type, nor any return. On an x86-64
Linux kernel build, at the point the pass runs, none of the 23810
pointer arguments of 557 C objects (kernel/, mm/, fs/, net/core, lib/,
...) carries the attribute, and 318 of the 756 of rust/kernel.o and the
Rust binder driver do (42%); on arm64, 21 of 22606 C pointer arguments
of a comparable set do, structs over 16 bytes that AArch64 passes by
reference, which are read like x86-64's byval objects. These are shares
of pointer arguments whose pointee is read at all, not of arguments or
structs reconstructed in full.

A value that may be poison is frozen before it is reported, so that the
callback receives bits: the padding of a struct coerced into an integer,
or an argument the function does not use, which dead argument
elimination passes as poison in the callers it can see. The latter is
still reported - other callers pass real values - but an unused object
is not handed to the runtime to read, as its pointer may be poison.
Freezing also keeps MemorySanitizer from reporting the instrumentation's
own use of such a value.

The arguments are those of IR functions. An inlined callee is no IR
function any more, so its parameters are not observed.

trace-ret reports every return, a first-class aggregate return per
element. A return forwarding a musttail call is left alone: the call has
to stay adjacent to the return, so there is nowhere to put the trace
call.

Call sites
----------

Neither callback is told which function it speaks for. A runtime takes
its own return address, as the trace-cmp callbacks already do, so every
observation is a call of its own, and none is ever a tail call: a
trace-ret call before `ret void` is in tail position, and as a tail call
it would hand the callback the caller's return address.

No memory of its own
--------------------

Reporting the value rather than an address to read it from is what keeps
both modes target-independent. The pass never creates memory to report a
value from, so nothing escapes to the stack and no frame is realigned on
the instrumentation's account. Spilling instead would not work
everywhere: the slot escapes, because its address is handed to the
callback, so (K)ASan gives it a redzone and 32-byte alignment; that
realigns the frame, which makes the backend reserve a base pointer. On
x86-64 the base pointer is RBX, and a function whose inline assembly also
claims RBX - a cpuid with an "=b" operand, an rdtsc clobbering it - then
fails to compile with "Interference usage of base pointer/frame pointer".

Tests
-----

- trace-args.ll and trace-ret.ll: every kind of argument and return, the
  IR positions around sret and swifterror, aggregates, objects and their
  shared tables, nested structs and elements and opaque byte arrays wider
  than 8 bytes, pointee snapshots of dereferenceable and
  dereferenceable_or_null arguments and returns, bounded and disabled,
  and the pointers not snapshotted (no attribute, unused, another address
  space), vectors, address spaces, musttail and unwinding returns, unused
  arguments and frozen values, and the calls never being tail calls.
- trace-args-debug-invariance.ll: instrumenting and then stripping debug
  information gives exactly the module that stripping first and
  instrumenting then gives, for a module whose debug records disagree
  with its IR argument list in every way a debug-information-driven
  design would trip over, and
  for what clang -O2 -g emits for C and C++ argument shapes on x86-64 and
  AArch64, C++ references and `this` with their pointee snapshots
  included.
- trace-args-debug-invariance-codegen-{x86_64,aarch64}.ll: the same
  inputs compiled with and without debug information give a byte-identical
  .text, with the expected number of trace calls.
- trace-args-no-coverage-level.ll and trace-no-spill.ll: the modes
  working without a coverage level, and no configuration emitting an
  alloca.

Clang driver support (-fsanitize-coverage=trace-args,trace-ret) and the
compiler-rt runtime follow in separate patches.

Assisted-by: Kiro CLI (Claude Opus 5.5)
Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 .../llvm/Transforms/Utils/Instrumentation.h   |   2 +
 .../Instrumentation/InstrumentationOptions.td |   6 +
 .../Instrumentation/SanitizerCoverage.cpp     | 428 ++++++++++++++-
 .../Inputs/trace-args-cases-aarch64.ll        | 498 ++++++++++++++++++
 .../Inputs/trace-args-cases-x86_64.ll         | 475 +++++++++++++++++
 ...e-args-debug-invariance-codegen-aarch64.ll |  36 ++
 ...ce-args-debug-invariance-codegen-x86_64.ll |  36 ++
 .../trace-args-debug-invariance.ll            | 153 ++++++
 .../trace-args-no-coverage-level.ll           |  75 +++
 .../SanitizerCoverage/trace-args.ll           | 230 ++++++++
 .../SanitizerCoverage/trace-no-spill.ll       |  67 +++
 .../SanitizerCoverage/trace-ret.ll            | 148 ++++++
 12 files changed, 2152 insertions(+), 2 deletions(-)
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/Inputs/trace-args-cases-aarch64.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/Inputs/trace-args-cases-x86_64.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance-codegen-aarch64.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance-codegen-x86_64.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-coverage-level.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-no-spill.ll
 create mode 100644 llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll

diff --git a/llvm/include/llvm/Transforms/Utils/Instrumentation.h b/llvm/include/llvm/Transforms/Utils/Instrumentation.h
index 9945ef5665e9be..eed744341fa45d 100644
--- a/llvm/include/llvm/Transforms/Utils/Instrumentation.h
+++ b/llvm/include/llvm/Transforms/Utils/Instrumentation.h
@@ -161,6 +161,8 @@ struct SanitizerCoverageOptions {
   bool StackDepth = false;
   bool TraceLoads = false;
   bool TraceStores = false;
+  bool TraceArgs = false;
+  bool TraceRet = false;
   bool CollectControlFlow = false;
   bool GatedCallbacks = false;
   int StackDepthCallbackMin = 0;
diff --git a/llvm/lib/Transforms/Instrumentation/InstrumentationOptions.td b/llvm/lib/Transforms/Instrumentation/InstrumentationOptions.td
index 44a2c46ed3ec43..d569a0c52d286a 100644
--- a/llvm/lib/Transforms/Instrumentation/InstrumentationOptions.td
+++ b/llvm/lib/Transforms/Instrumentation/InstrumentationOptions.td
@@ -750,6 +750,8 @@ defm : BoolField<"sanitizer-coverage-stack-depth", "false",
   "Max stack depth tracing">;
 defm : ValueField<"sanitizer-coverage-stack-depth-callback-min", "int", "0",
   "Max stack depth tracing should use callback and only when stack depth more than specified">;
+defm : BoolField<"sanitizer-coverage-trace-args", "false",
+  "Tracing of function arguments">;
 defm : BoolField<"sanitizer-coverage-trace-compares", "false",
   "Tracing of CMP and similar instructions">;
 defm : BoolField<"sanitizer-coverage-trace-divs", "false",
@@ -764,6 +766,10 @@ defm : BoolField<"sanitizer-coverage-trace-pc-entry-exit", "false",
   "PC tracing with separate entry/exit callbacks">;
 defm : BoolField<"sanitizer-coverage-trace-pc-guard", "false",
   "PC tracing with a guard">;
+defm : ValueField<"sanitizer-coverage-trace-pointee-bytes", "unsigned", "64",
+  "With trace-args and trace-ret, read at most this many bytes behind a pointer the IR says is dereferenceable (0: report the pointer only)">;
+defm : BoolField<"sanitizer-coverage-trace-ret", "false",
+  "Tracing of return values">;
 defm : BoolField<"sanitizer-coverage-trace-stores", "false",
   "Tracing of store instructions">;
 
diff --git a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
index 88f39f904d803f..364b69e0ed1a57 100644
--- a/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
+++ b/llvm/lib/Transforms/Instrumentation/SanitizerCoverage.cpp
@@ -13,12 +13,15 @@
 #include "llvm/Transforms/Instrumentation/SanitizerCoverage.h"
 #include "InstrumentationOptions.h"
 #include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/SmallBitVector.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/Analysis/GlobalsModRef.h"
 #include "llvm/Analysis/PostDominators.h"
+#include "llvm/Analysis/ValueTracking.h"
 #include "llvm/IR/Constant.h"
 #include "llvm/IR/Constants.h"
 #include "llvm/IR/DataLayout.h"
+#include "llvm/IR/DebugInfoMetadata.h"
 #include "llvm/IR/Dominators.h"
 #include "llvm/IR/EHPersonalities.h"
 #include "llvm/IR/Function.h"
@@ -31,6 +34,7 @@
 #include "llvm/IR/Module.h"
 #include "llvm/IR/Type.h"
 #include "llvm/IR/ValueSymbolTable.h"
+#include "llvm/Support/MathExtras.h"
 #include "llvm/Support/SpecialCaseList.h"
 #include "llvm/Support/VirtualFileSystem.h"
 #include "llvm/TargetParser/Triple.h"
@@ -46,6 +50,8 @@ const char SanCovTracePCIndirName[] = "__sanitizer_cov_trace_pc_indir";
 const char SanCovTracePCName[] = "__sanitizer_cov_trace_pc";
 const char SanCovTracePCEntryName[] = "__sanitizer_cov_trace_pc_entry";
 const char SanCovTracePCExitName[] = "__sanitizer_cov_trace_pc_exit";
+const char SanCovTraceArgsName[] = "__sanitizer_cov_trace_args";
+const char SanCovTraceRetName[] = "__sanitizer_cov_trace_ret";
 const char SanCovTraceCmp1[] = "__sanitizer_cov_trace_cmp1";
 const char SanCovTraceCmp2[] = "__sanitizer_cov_trace_cmp2";
 const char SanCovTraceCmp4[] = "__sanitizer_cov_trace_cmp4";
@@ -140,10 +146,13 @@ SanitizerCoverageOptions overrideFromCL(const InstrumentationOptions &Opts,
                Opts.sanitizer_coverage_stack_depth_callback_min);
   Options.TraceLoads |= Opts.sanitizer_coverage_trace_loads;
   Options.TraceStores |= Opts.sanitizer_coverage_trace_stores;
+  Options.TraceArgs |= Opts.sanitizer_coverage_trace_args;
+  Options.TraceRet |= Opts.sanitizer_coverage_trace_ret;
   Options.GatedCallbacks |= Opts.sanitizer_coverage_gated_trace_callbacks;
   if (!Options.TracePCGuard && !Options.TracePC && !Options.TracePCEntryExit &&
       !Options.Inline8bitCounters && !Options.StackDepth &&
-      !Options.InlineBoolFlag && !Options.TraceLoads && !Options.TraceStores)
+      !Options.InlineBoolFlag && !Options.TraceLoads && !Options.TraceStores &&
+      !Options.TraceArgs && !Options.TraceRet)
     Options.TracePCGuard = true; // TracePCGuard is default.
   Options.CollectControlFlow |= Opts.sanitizer_coverage_control_flow;
   return Options;
@@ -180,6 +189,8 @@ class ModuleSanitizerCoverage {
   void InjectTraceForLoadsAndStores(Function &F, ArrayRef<LoadInst *> Loads,
                                     ArrayRef<StoreInst *> Stores);
   void InjectTraceForExits(Function &F);
+  void InjectTraceForArgs(Function &F);
+  void InjectTraceForRet(Function &F);
   void InjectTraceForSwitch(Function &F,
                             ArrayRef<Instruction *> SwitchTraceTargets,
                             Value *&FunctionGateCmp);
@@ -205,6 +216,37 @@ class ModuleSanitizerCoverage {
   std::string getSectionStart(const std::string &Section) const;
   std::string getSectionEnd(const std::string &Section) const;
 
+  /// The `offsets` and `num_fields` arguments of the trace-args/trace-ret
+  /// callbacks for an object in memory: a constant table of {byte offset,
+  /// byte size} pairs, none wider than 8 bytes, covering the elements of its
+  /// IR type, and the store size of that type. Table is null for an object of
+  /// no fixed size, in which case NumFields and ObjectSize are 0.
+  struct FieldOffsets {
+    Constant *Table = nullptr;
+    unsigned NumFields = 0;
+    uint64_t ObjectSize = 0;
+  };
+  FieldOffsets getObjectOffsets(Type *Ty);
+  /// Create one trace-args/trace-ret call, which is never a tail call.
+  void emitTraceCall(IRBuilderBase &IRB, FunctionCallee Callee,
+                     ArrayRef<Value *> Args);
+  /// Emit the trace-args/trace-ret call for \p V through \p IRB: one call per
+  /// element of a first-class aggregate, and one for anything else.
+  void traceValue(IRBuilderBase &IRB, FunctionCallee Callee,
+                  ArrayRef<Value *> Prefix, Value *V);
+  void traceObject(IRBuilderBase &IRB, FunctionCallee Callee,
+                   ArrayRef<Value *> Prefix, Value *Addr, Type *Ty);
+  /// Report pointer \p Ptr together with the first \p Bytes bytes it points
+  /// to, which the IR says are dereferenceable.
+  void tracePointee(IRBuilderBase &IRB, FunctionCallee Callee,
+                    ArrayRef<Value *> Prefix, Value *Ptr, uint64_t Bytes);
+  /// How many bytes behind \p Ptr to report, given that the IR says
+  /// \p Dereferenceable of them may be read: none if \p Ptr is not a pointer
+  /// in the default address space.
+  uint64_t getPointeeBytes(Value *Ptr, uint64_t Dereferenceable) const;
+  /// The most bytes a trace call reads behind a dereferenceable pointer.
+  uint64_t TracePointeeBytes = 0;
+
   const InstrumentationOptions &Opts;
   Module &M;
   DomTreeCallback DTCallback;
@@ -214,6 +256,7 @@ class ModuleSanitizerCoverage {
   FunctionCallee SanCovTracePCIndir;
   FunctionCallee SanCovTracePC, SanCovTracePCGuard;
   FunctionCallee SanCovTracePCEntry, SanCovTracePCExit;
+  FunctionCallee SanCovTraceArgsFunc, SanCovTraceRetFunc;
   std::array<FunctionCallee, 4> SanCovTraceCmpFunction;
   std::array<FunctionCallee, 4> SanCovTraceConstCmpFunction;
   std::array<FunctionCallee, 5> SanCovLoadFunction;
@@ -237,6 +280,11 @@ class ModuleSanitizerCoverage {
   SmallVector<GlobalValue *, 20> GlobalsToAppendToUsed;
   SmallVector<GlobalValue *, 20> GlobalsToAppendToCompilerUsed;
 
+  /// Object tables are shared by every object of the same IR type: a type
+  /// passed by hundreds of functions must not emit hundreds of identical
+  /// tables.
+  DenseMap<Type *, FieldOffsets> ObjectOffsetsCache;
+
   SanitizerCoverageOptions Options;
 
   const SpecialCaseList *Allowlist;
@@ -341,7 +389,16 @@ Function *ModuleSanitizerCoverage::CreateInitCallsForSections(
 }
 
 bool ModuleSanitizerCoverage::instrumentModule() {
-  if (Options.CoverageType == SanitizerCoverageOptions::SCK_None)
+  // Argument and return value tracing instrument function boundaries, not
+  // blocks or edges, so they are complete on their own and do not need a
+  // coverage type. Everything the block path emits is guarded by its own
+  // option, so letting these through with SCK_None adds no block
+  // instrumentation. Without this, `opt -passes=sancov-module
+  // -sanitizer-coverage-trace-args` silently produces an uninstrumented
+  // module: the driver supplies a level when it parses
+  // -fsanitize-coverage=, but a direct pass invocation has none.
+  if (Options.CoverageType == SanitizerCoverageOptions::SCK_None &&
+      !Options.TraceArgs && !Options.TraceRet)
     return false;
   if (Allowlist &&
       !Allowlist->inSection("coverage", "src", M.getSourceFileName()))
@@ -353,6 +410,7 @@ bool ModuleSanitizerCoverage::instrumentModule() {
   DL = &M.getDataLayout();
   CurModule = &M;
   TargetTriple = M.getTargetTriple();
+  TracePointeeBytes = Opts.sanitizer_coverage_trace_pointee_bytes;
   FunctionGuardArray = nullptr;
   Function8bitCounterArray = nullptr;
   FunctionBoolArray = nullptr;
@@ -459,6 +517,17 @@ bool ModuleSanitizerCoverage::instrumentModule() {
   SanCovTracePCGuard =
       M.getOrInsertFunction(SanCovTracePCGuardName, VoidTy, PtrTy);
 
+  // See the "Argument and return value tracing" section below for the meaning
+  // of the arguments.
+  // void __sanitizer_cov_trace_args(u32 arg_idx, u32 size, u64 val,
+  //                                 u64 *offsets, u32 num_fields)
+  SanCovTraceArgsFunc = M.getOrInsertFunction(
+      SanCovTraceArgsName, VoidTy, Int32Ty, Int32Ty, Int64Ty, PtrTy, Int32Ty);
+  // void __sanitizer_cov_trace_ret(u32 size, u64 val,
+  //                                u64 *offsets, u32 num_fields)
+  SanCovTraceRetFunc = M.getOrInsertFunction(SanCovTraceRetName, VoidTy,
+                                             Int32Ty, Int64Ty, PtrTy, Int32Ty);
+
   SanCovStackDepthCallback =
       M.getOrInsertFunction(SanCovStackDepthCallbackName, VoidTy);
 
@@ -684,6 +753,12 @@ void ModuleSanitizerCoverage::instrumentFunction(Function &F) {
 
   if (Options.TracePCEntryExit)
     InjectTraceForExits(F);
+
+  if (Options.TraceArgs)
+    InjectTraceForArgs(F);
+
+  if (Options.TraceRet)
+    InjectTraceForRet(F);
 }
 
 GlobalVariable *ModuleSanitizerCoverage::CreateFunctionLocalArrayInSection(
@@ -1183,3 +1258,352 @@ void ModuleSanitizerCoverage::createFunctionControlFlow(Function &F) {
       ConstantArray::get(ArrayType::get(PtrTy, CFs.size()), CFs));
   FunctionCFsArray->setConstant(true);
 }
+
+//===----------------------------------------------------------------------===//
+// Argument and return value tracing.
+//===----------------------------------------------------------------------===//
+//
+// -sanitizer-coverage-trace-args reports the IR arguments of an instrumented
+// function on entry, -sanitizer-coverage-trace-ret the value of every return:
+//
+//   void __sanitizer_cov_trace_args(u32 arg_idx, u32 size, u64 val,
+//                                   u64 *offsets, u32 num_fields);
+//   void __sanitizer_cov_trace_ret (u32 size, u64 val,
+//                                   u64 *offsets, u32 num_fields);
+//
+// The instrumentation is decided by the IR alone. Debug information is never
+// consulted, so a build with -g emits exactly the same calls, with the same
+// operands, as one without it: debug information may enrich how an observation
+// is interpreted, offline, but never decides what is observed.
+//
+// An observation is identified by its call site, which the runtime reads as its
+// own return address, together with `arg_idx`. `arg_idx` is the zero-based
+// position of the argument in the IR function's parameter list - the ABI's
+// view of it, not the source-language parameter number. A struct-return
+// pointer and a Swift error slot are not observed, but keep their positions,
+// so the observed indices then have gaps. One source parameter may be several
+// IR arguments (a struct split across registers), and one IR argument several
+// observations (the elements of a first-class aggregate, which share its
+// `arg_idx`). A consumer that wants the source-level parameter maps the call
+// site and IR position back through the debug information of the binary.
+//
+// What `val` holds depends on `num_fields`:
+//
+// - `num_fields == 0`: `val` is the value itself, the low `size` bytes of it,
+//   zero-extended. Integers, pointers, floating-point values and vectors are
+//   reported this way: a pointer as the address it holds, and a floating-point
+//   value or a vector as its bit pattern. A value wider than 64 bits is
+//   reported by its low 64 bits.
+//
+// - `num_fields != 0`: `val` is the address of `size` bytes that may be read
+//   at the call, and `offsets` points at a constant table of `num_fields`
+//   {byte offset, byte size} pairs, none wider than 8 bytes, that say which of
+//   them to read. That is
+//
+//   - an object the ABI placed in memory - a byval, inalloca, preallocated or
+//     byref argument, or the buffer of an indirect (sret) return - which the
+//     table describes by the elements of its IR type, nested structs
+//     included, and anything wider than 8 bytes, such as an array or the
+//     opaque [N x i8] some front ends type every such object as, in 8-byte
+//     pieces;
+//
+//   - or a pointer argument, or a pointer return value, that the IR says is
+//     dereferenceable(N) or dereferenceable_or_null(N): `val` is the pointer
+//     and `size` the first min(N, -sanitizer-coverage-trace-pointee-bytes)
+//     bytes it points to, which the table covers in 8-byte pieces. Nothing but
+//     the IR attribute bounds the read - not the pointee's source type, which
+//     the IR does not have - so the bytes are a raw snapshot, which a consumer
+//     cuts into the pointee's members with the debug information of the
+//     binary. A dereferenceable_or_null pointer may be null, which the runtime
+//     checks for; a pointer with no such attribute is reported by value
+//     alone.
+//
+// `size == 0` means there was nothing to report - a value of a type with no
+// fixed-size bit pattern, such as a scalable vector, a void return, or an
+// object the function does not use, which its callers may pass a poison
+// pointer for. A pointer the function does not use is reported by value, its
+// pointee is not read.
+//
+// The pass never creates memory to report a value from, so no stack slot
+// escapes and no frame is realigned on its account.
+
+ModuleSanitizerCoverage::FieldOffsets
+ModuleSanitizerCoverage::getObjectOffsets(Type *Ty) {
+  if (!Ty || !Ty->isSized() || DL->getTypeStoreSize(Ty).isScalable())
+    return {};
+  if (auto It = ObjectOffsetsCache.find(Ty); It != ObjectOffsetsCache.end())
+    return It->second;
+
+  // A struct is described by its elements, recursively; anything else as one
+  // field, or in 8-byte pieces if it is wider, so that a runtime reading every
+  // field into a 64-bit word reads all of the object.
+  SmallVector<Constant *, 16> Pairs;
+  auto AddField = [&](uint64_t Offset, uint64_t Bytes) {
+    for (uint64_t Piece = 0; Piece < Bytes; Piece += sizeof(uint64_t)) {
+      Pairs.push_back(ConstantInt::get(Int64Ty, Offset + Piece));
+      Pairs.push_back(ConstantInt::get(
+          Int64Ty, std::min<uint64_t>(Bytes - Piece, sizeof(uint64_t))));
+    }
+  };
+  auto AddType = [&](auto &Self, Type *T, uint64_t Offset) -> void {
+    if (auto *STy = dyn_cast<StructType>(T)) {
+      const StructLayout *Layout = DL->getStructLayout(STy);
+      for (unsigned I = 0, E = STy->getNumElements(); I != E; ++I)
+        Self(Self, STy->getElementType(I),
+             Offset + Layout->getElementOffset(I).getFixedValue());
+      return;
+    }
+    AddField(Offset, DL->getTypeStoreSize(T).getFixedValue());
+  };
+  uint64_t Size = DL->getTypeStoreSize(Ty).getFixedValue();
+  AddType(AddType, Ty, 0);
+
+  FieldOffsets Offsets;
+  if (!Pairs.empty()) {
+    ArrayType *TableTy = ArrayType::get(Int64Ty, Pairs.size());
+    auto *Table = new GlobalVariable(
+        M, TableTy, /*isConstant=*/true, GlobalVariable::PrivateLinkage,
+        ConstantArray::get(TableTy, Pairs), "__sancov_offsets_");
+    Table->setUnnamedAddr(GlobalValue::UnnamedAddr::Global);
+    Offsets = {Table, static_cast<unsigned>(Pairs.size() / 2), Size};
+  }
+  ObjectOffsetsCache[Ty] = Offsets;
+  return Offsets;
+}
+
+/// Widen \p V to the 64-bit value the callbacks take: a pointer is reported as
+/// the address it holds, a floating-point value or a vector as its bit pattern,
+/// and a value wider than 64 bits by its low 64 bits. Returns the value
+/// together with the number of bytes of it that are meaningful, or
+/// {nullptr, 0} for a type that has no fixed-size bit pattern, such as a
+/// scalable vector.
+static std::pair<Value *, uint64_t> getReportedValue(IRBuilderBase &IRB,
+                                                     Value *V) {
+  const DataLayout &DL = IRB.GetInsertBlock()->getDataLayout();
+  Type *Ty = V->getType();
+  IntegerType *Int64Ty = IRB.getInt64Ty();
+
+  // The callback receives bits, not poison: an argument callers pass nothing
+  // for (dead argument elimination passes poison), or the padding of a struct
+  // coerced into an integer, is whatever it happens to be. This also keeps
+  // MemorySanitizer from reporting the instrumentation's own use of it.
+  if (!isGuaranteedNotToBeUndefOrPoison(V))
+    V = IRB.CreateFreeze(V);
+
+  // ptrtoint widens or narrows to the requested type, so this also holds for a
+  // target, or an address space, whose pointers are narrower than 64 bits.
+  if (Ty->isPointerTy())
+    return {IRB.CreatePtrToInt(V, Int64Ty),
+            std::min<uint64_t>(DL.getPointerSize(Ty->getPointerAddressSpace()),
+                               sizeof(uint64_t))};
+
+  // A vector of pointers has no bit pattern of its own; its addresses do.
+  if (auto *VTy = dyn_cast<FixedVectorType>(Ty);
+      VTy && VTy->getElementType()->isPointerTy()) {
+    V = IRB.CreatePtrToInt(
+        V, VectorType::get(DL.getIntPtrType(VTy->getElementType()),
+                           VTy->getElementCount()));
+    Ty = V->getType();
+  }
+  if (Ty->isFloatingPointTy() || isa<FixedVectorType>(Ty)) {
+    TypeSize Bits = Ty->getPrimitiveSizeInBits();
+    if (Bits.isScalable() || !Bits.getFixedValue())
+      return {nullptr, 0};
+    V = IRB.CreateBitCast(V, IRB.getIntNTy(Bits.getFixedValue()));
+    Ty = V->getType();
+  }
+  if (!Ty->isIntegerTy())
+    return {nullptr, 0};
+
+  uint64_t Size = divideCeil(Ty->getIntegerBitWidth(), 8);
+  if (Size > sizeof(uint64_t))
+    return {IRB.CreateTrunc(V, Int64Ty), sizeof(uint64_t)};
+  return {IRB.CreateZExt(V, Int64Ty), Size};
+}
+
+void ModuleSanitizerCoverage::emitTraceCall(IRBuilderBase &IRB,
+                                            FunctionCallee Callee,
+                                            ArrayRef<Value *> Args) {
+  CallInst *CI = IRB.CreateCall(Callee, Args);
+  // An observation is identified by its call site, which the runtime reads as
+  // its own return address. A tail call would hand the callback the caller's
+  // return address instead, and a trace-ret call of a void function is in tail
+  // position.
+  CI->setTailCallKind(CallInst::TCK_NoTail);
+}
+
+void ModuleSanitizerCoverage::traceValue(IRBuilderBase &IRB,
+                                         FunctionCallee Callee,
+                                         ArrayRef<Value *> Prefix, Value *V) {
+  auto Emit = [&](std::pair<Value *, uint64_t> Reported) {
+    SmallVector<Value *, 5> Args(Prefix);
+    Args.push_back(ConstantInt::get(Int32Ty, Reported.second));
+    Args.push_back(Reported.first ? Reported.first
+                                  : ConstantInt::get(Int64Ty, 0));
+    Args.push_back(ConstantPointerNull::get(PtrTy));
+    Args.push_back(ConstantInt::get(Int32Ty, 0));
+    emitTraceCall(IRB, Callee, Args);
+  };
+
+  // A first-class aggregate lives in registers and has no address, so each of
+  // its elements is reported on its own.
+  Type *Ty = V->getType();
+  unsigned NumElements = Ty->isStructTy()  ? Ty->getStructNumElements()
+                         : Ty->isArrayTy() ? Ty->getArrayNumElements()
+                                           : 0;
+  if (!NumElements) {
+    Emit(getReportedValue(IRB, V));
+    return;
+  }
+  for (unsigned I = 0; I != NumElements; ++I)
+    Emit(getReportedValue(IRB, IRB.CreateExtractValue(V, I)));
+}
+
+void ModuleSanitizerCoverage::traceObject(IRBuilderBase &IRB,
+                                          FunctionCallee Callee,
+                                          ArrayRef<Value *> Prefix, Value *Addr,
+                                          Type *Ty) {
+  FieldOffsets Offsets = getObjectOffsets(Ty);
+  SmallVector<Value *, 5> Args(Prefix);
+  Args.push_back(ConstantInt::get(Int32Ty, Offsets.ObjectSize));
+  Args.push_back(Offsets.Table ? IRB.CreatePtrToInt(Addr, Int64Ty)
+                               : ConstantInt::get(Int64Ty, 0));
+  Args.push_back(Offsets.Table ? Offsets.Table
+                               : ConstantPointerNull::get(PtrTy));
+  Args.push_back(ConstantInt::get(Int32Ty, Offsets.NumFields));
+  emitTraceCall(IRB, Callee, Args);
+}
+
+uint64_t
+ModuleSanitizerCoverage::getPointeeBytes(Value *Ptr,
+                                         uint64_t Dereferenceable) const {
+  // A pointer of another address space may not be readable through the
+  // generic address the runtime gets.
+  if (!Ptr->getType()->isPointerTy() ||
+      Ptr->getType()->getPointerAddressSpace() != 0)
+    return 0;
+  return std::min(Dereferenceable, TracePointeeBytes);
+}
+
+void ModuleSanitizerCoverage::tracePointee(IRBuilderBase &IRB,
+                                           FunctionCallee Callee,
+                                           ArrayRef<Value *> Prefix, Value *Ptr,
+                                           uint64_t Bytes) {
+  // The IR says nothing about the pointee but how many bytes of it may be
+  // read, so they are read as bytes: the table of a byte array that long.
+  FieldOffsets Offsets =
+      getObjectOffsets(ArrayType::get(IRB.getInt8Ty(), Bytes));
+  if (!isGuaranteedNotToBeUndefOrPoison(Ptr))
+    Ptr = IRB.CreateFreeze(Ptr);
+  SmallVector<Value *, 5> Args(Prefix);
+  Args.push_back(ConstantInt::get(Int32Ty, Offsets.ObjectSize));
+  Args.push_back(IRB.CreatePtrToInt(Ptr, Int64Ty));
+  Args.push_back(Offsets.Table);
+  Args.push_back(ConstantInt::get(Int32Ty, Offsets.NumFields));
+  emitTraceCall(IRB, Callee, Args);
+}
+
+void ModuleSanitizerCoverage::InjectTraceForArgs(Function &F) {
+  BasicBlock &EntryBB = F.getEntryBlock();
+  BasicBlock::iterator IP = EntryBB.getFirstInsertionPt();
+  // A function whose entry block admits no call cannot be reported at all.
+  if (IP == EntryBB.end())
+    return;
+
+  // InstrumentationIRBuilder gives the calls a !dbg location, which the
+  // verifier requires of a call in a function with debug information once it
+  // can be inlined. The location is the function's own opening line rather
+  // than the one of the instruction the calls precede, which may belong to an
+  // inlined callee: the arguments are the function's own. Only the location
+  // depends on debug information, never the calls.
+  InstrumentationIRBuilder IRB(&*IP);
+  if (DISubprogram *SP = F.getSubprogram())
+    IRB.SetCurrentDebugLocation(
+        DILocation::get(SP->getContext(), SP->getScopeLine(), 0, SP));
+
+  // Whether the function uses each argument, before the calls use them all.
+  // Callers may pass anything for one it does not: a value is still reported,
+  // but an object is not read.
+  SmallBitVector Unused(F.arg_size());
+  for (Argument &Arg : F.args())
+    Unused[Arg.getArgNo()] = Arg.use_empty();
+  for (Argument &Arg : F.args()) {
+    // A struct-return pointer is where the return value goes, which trace-ret
+    // reports; the error slot of a Swift throwing function may only be loaded,
+    // stored, or passed on as a swifterror argument. Neither is observed, and
+    // neither renumbers the arguments that are: arg_idx stays the IR position.
+    if (Arg.hasStructRetAttr() || Arg.hasSwiftErrorAttr())
+      continue;
+    Value *Prefix[] = {ConstantInt::get(Int32Ty, Arg.getArgNo())};
+    // An argument the ABI says points at a copy of the parameter in memory is
+    // that object, not a value.
+    // An object the function does not use may be passed as a poison pointer,
+    // by dead argument elimination in its callers: it is not handed to the
+    // runtime to read.
+    if (Type *MemTy = Arg.getPointeeInMemoryValueType()) {
+      traceObject(IRB, SanCovTraceArgsFunc, Prefix,
+                  Unused[Arg.getArgNo()] ? nullptr : &Arg,
+                  Unused[Arg.getArgNo()] ? nullptr : MemTy);
+    } else if (uint64_t Bytes =
+                   !Arg.getType()->isPointerTy() || Unused[Arg.getArgNo()]
+                       ? 0
+                       : getPointeeBytes(
+                             &Arg,
+                             std::max(Arg.getDereferenceableBytes(),
+                                      Arg.getDereferenceableOrNullBytes()))) {
+      // What the pointer points to, as far as the IR says it may be read: at
+      // the top of the entry block nothing can have freed it yet.
+      tracePointee(IRB, SanCovTraceArgsFunc, Prefix, &Arg, Bytes);
+    } else {
+      traceValue(IRB, SanCovTraceArgsFunc, Prefix, &Arg);
+    }
+  }
+}
+
+void ModuleSanitizerCoverage::InjectTraceForRet(Function &F) {
+  // An indirect return leaves an IR function returning void and writes the
+  // returned object through a hidden struct-return pointer; that buffer is
+  // what is reported, so an indirect return is not dropped.
+  Argument *SRetArg = nullptr;
+  for (Argument &Arg : F.args())
+    if (Arg.hasStructRetAttr()) {
+      SRetArg = &Arg;
+      break;
+    }
+
+  // A function that never writes its sret buffer may be passed a poison one.
+  bool SRetUnused = SRetArg && SRetArg->use_empty();
+  // A returned pointer the IR says may be read from is reported with what it
+  // points to: the return attribute holds where the function returns.
+  uint64_t RetPointeeBytes =
+      F.getReturnType()->isPointerTy() && !SRetArg
+          ? std::max(F.getAttributes().getRetDereferenceableBytes(),
+                     F.getAttributes().getRetDereferenceableOrNullBytes())
+          : 0;
+  for (BasicBlock &BB : F) {
+    // Only a return carries a value to report; unwinding leaves the function
+    // without one.
+    auto *RI = dyn_cast<ReturnInst>(BB.getTerminator());
+    if (!RI)
+      continue;
+    // A musttail call has to stay adjacent to the return that forwards it, so
+    // there is nowhere to put the call.
+    if (auto *CI = dyn_cast_or_null<CallInst>(RI->getPrevNode());
+        CI && CI->isMustTailCall())
+      continue;
+
+    InstrumentationIRBuilder IRB(RI);
+    if (SRetArg) {
+      traceObject(IRB, SanCovTraceRetFunc, {}, SRetUnused ? nullptr : SRetArg,
+                  SRetUnused ? nullptr : SRetArg->getParamStructRetType());
+    } else if (Value *RetVal = RI->getReturnValue()) {
+      if (uint64_t Bytes =
+              RetPointeeBytes ? getPointeeBytes(RetVal, RetPointeeBytes) : 0)
+        tracePointee(IRB, SanCovTraceRetFunc, {}, RetVal, Bytes);
+      else
+        traceValue(IRB, SanCovTraceRetFunc, {}, RetVal);
+    } else {
+      traceObject(IRB, SanCovTraceRetFunc, {}, nullptr, nullptr); // void
+    }
+  }
+}
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/Inputs/trace-args-cases-aarch64.ll b/llvm/test/Instrumentation/SanitizerCoverage/Inputs/trace-args-cases-aarch64.ll
new file mode 100644
index 00000000000000..f762c376d6fb79
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/Inputs/trace-args-cases-aarch64.ll
@@ -0,0 +1,498 @@
+; A set of C and C++ argument shapes, as clang -O2 -g emits them
+; for aarch64-linux-gnu: cases.c and cases.cpp, linked into one module.
+source_filename = "llvm-link"
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32"
+target triple = "aarch64-unknown-linux-gnu"
+
+%struct.big = type { i64, i64, i64, i64 }
+
+define dso_local i64 @c01_scalar(i32 noundef %a, i64 noundef %b) local_unnamed_addr !dbg !30 {
+entry:
+    #dbg_value(i32 %a, !35, !DIExpression(), !37)
+    #dbg_value(i64 %b, !36, !DIExpression(), !37)
+  %conv = sext i32 %a to i64, !dbg !38
+  %add = add nsw i64 %b, %conv, !dbg !39
+  ret i64 %add, !dbg !40
+}
+
+define dso_local noundef i64 @c02_pointer(ptr noundef %p) local_unnamed_addr !dbg !41 {
+entry:
+    #dbg_value(ptr %p, !47, !DIExpression(), !48)
+  %0 = ptrtoint ptr %p to i64, !dbg !49
+  ret i64 %0, !dbg !50
+}
+
+define dso_local noundef double @c03_fp(float noundef %f, double noundef %d) local_unnamed_addr !dbg !51 {
+entry:
+    #dbg_value(float %f, !57, !DIExpression(), !59)
+    #dbg_value(double %d, !58, !DIExpression(), !59)
+  %conv = fpext float %f to double, !dbg !60
+  %add = fadd double %d, %conv, !dbg !61
+  ret double %add, !dbg !62
+}
+
+define dso_local i32 @c04_small(i64 %p.coerce) local_unnamed_addr !dbg !63 {
+entry:
+  %p.sroa.0.0.extract.trunc = trunc i64 %p.coerce to i32
+  %p.sroa.2.0.extract.shift = lshr i64 %p.coerce, 32
+  %p.sroa.2.0.extract.trunc = trunc nuw i64 %p.sroa.2.0.extract.shift to i32
+    #dbg_value(i32 %p.sroa.0.0.extract.trunc, !71, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !72)
+    #dbg_value(i32 %p.sroa.2.0.extract.trunc, !71, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !72)
+  %add = add nsw i32 %p.sroa.2.0.extract.trunc, %p.sroa.0.0.extract.trunc, !dbg !73
+  ret i32 %add, !dbg !74
+}
+
+define dso_local double @c05_split([2 x i64] %m.coerce) local_unnamed_addr !dbg !75 {
+entry:
+  %m.coerce.fca.0.extract = extractvalue [2 x i64] %m.coerce, 0
+  %0 = bitcast i64 %m.coerce.fca.0.extract to double
+  %m.coerce.fca.1.extract = extractvalue [2 x i64] %m.coerce, 1
+    #dbg_value(double %0, !83, !DIExpression(DW_OP_LLVM_fragment, 0, 64), !84)
+    #dbg_value(i64 %m.coerce.fca.1.extract, !83, !DIExpression(DW_OP_LLVM_fragment, 64, 64), !84)
+  %conv = sitofp i64 %m.coerce.fca.1.extract to double, !dbg !85
+  %add = fadd double %0, %conv, !dbg !86
+  ret double %add, !dbg !87
+}
+
+define dso_local float @c05_hfa([3 x float] alignstack(8) %h.coerce) local_unnamed_addr !dbg !88 {
+entry:
+  %h.coerce.fca.0.extract = extractvalue [3 x float] %h.coerce, 0
+  %h.coerce.fca.1.extract = extractvalue [3 x float] %h.coerce, 1
+  %h.coerce.fca.2.extract = extractvalue [3 x float] %h.coerce, 2
+    #dbg_value(float %h.coerce.fca.0.extract, !97, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !98)
+    #dbg_value(float %h.coerce.fca.1.extract, !97, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !98)
+    #dbg_value(float %h.coerce.fca.2.extract, !97, !DIExpression(DW_OP_LLVM_fragment, 64, 32), !98)
+  %add = fadd float %h.coerce.fca.0.extract, %h.coerce.fca.1.extract, !dbg !99
+  %add1 = fadd float %h.coerce.fca.2.extract, %add, !dbg !100
+  ret float %add1, !dbg !101
+}
+
+define dso_local i64 @c06_big(ptr nofreeobj noundef readonly align 8 captures(none) dead_on_return dereferenceable(32) %b) local_unnamed_addr !dbg !102 {
+entry:
+    #dbg_value(ptr %b, !112, !DIExpression(DW_OP_deref), !113)
+  %0 = load i64, ptr %b, align 8, !dbg !114, !tbaa !115
+  %d = getelementptr inbounds nuw i8, ptr %b, i64 24, !dbg !118
+  %1 = load i64, ptr %d, align 8, !dbg !118, !tbaa !119
+  %add = add nsw i64 %1, %0, !dbg !120
+  ret i64 %add, !dbg !121
+}
+
+define dso_local void @c07_sret(ptr dead_on_unwind noalias nofree writable writeonly sret(%struct.big) align 8 captures(none) initializes((0, 32)) %agg.result, i64 noundef %x) local_unnamed_addr !dbg !122 {
+entry:
+    #dbg_value(i64 %x, !126, !DIExpression(), !128)
+    #dbg_declare(ptr %agg.result, !127, !DIExpression(), !129)
+  store i64 %x, ptr %agg.result, align 8, !dbg !130, !tbaa !115
+  %b = getelementptr inbounds nuw i8, ptr %agg.result, i64 8, !dbg !131
+  %add = add nsw i64 %x, 1, !dbg !132
+  store i64 %add, ptr %b, align 8, !dbg !130, !tbaa !133
+  %c = getelementptr inbounds nuw i8, ptr %agg.result, i64 16, !dbg !131
+  %add1 = add nsw i64 %x, 2, !dbg !134
+  store i64 %add1, ptr %c, align 8, !dbg !130, !tbaa !135
+  %d = getelementptr inbounds nuw i8, ptr %agg.result, i64 24, !dbg !131
+  %add2 = add nsw i64 %x, 3, !dbg !136
+  store i64 %add2, ptr %d, align 8, !dbg !130, !tbaa !119
+  ret void, !dbg !137
+}
+
+define dso_local i64 @c08_struct_ptr(ptr nofree noundef readonly captures(none) %f) local_unnamed_addr !dbg !138 {
+entry:
+    #dbg_value(ptr %f, !149, !DIExpression(), !150)
+  %0 = load i32, ptr %f, align 8, !dbg !151, !tbaa !152
+  %conv = sext i32 %0 to i64, !dbg !154
+  %b = getelementptr inbounds nuw i8, ptr %f, i64 8, !dbg !155
+  %1 = load i64, ptr %b, align 8, !dbg !155, !tbaa !156
+  %add = add nsw i64 %1, %conv, !dbg !157
+  %c = getelementptr inbounds nuw i8, ptr %f, i64 16, !dbg !158
+  %2 = load i8, ptr %c, align 8, !dbg !158, !tbaa !159
+  %conv1 = zext i8 %2 to i64, !dbg !160
+  %add2 = add nsw i64 %add, %conv1, !dbg !161
+  ret i64 %add2, !dbg !162
+}
+
+define dso_local i64 @c09_stack(i64 noundef %a0, i64 noundef %a1, i64 noundef %a2, i64 noundef %a3, i64 noundef %a4, i64 noundef %a5, i64 noundef %a6, i64 noundef %a7, i64 noundef %a8, i64 noundef %a9) local_unnamed_addr !dbg !163 {
+entry:
+    #dbg_value(i64 %a0, !167, !DIExpression(), !177)
+    #dbg_value(i64 %a1, !168, !DIExpression(), !177)
+    #dbg_value(i64 %a2, !169, !DIExpression(), !177)
+    #dbg_value(i64 %a3, !170, !DIExpression(), !177)
+    #dbg_value(i64 %a4, !171, !DIExpression(), !177)
+    #dbg_value(i64 %a5, !172, !DIExpression(), !177)
+    #dbg_value(i64 %a6, !173, !DIExpression(), !177)
+    #dbg_value(i64 %a7, !174, !DIExpression(), !177)
+    #dbg_value(i64 %a8, !175, !DIExpression(), !177)
+    #dbg_value(i64 %a9, !176, !DIExpression(), !177)
+  %add = add nsw i64 %a1, %a0, !dbg !178
+  %add1 = add nsw i64 %add, %a2, !dbg !179
+  %add2 = add nsw i64 %add1, %a3, !dbg !180
+  %add3 = add nsw i64 %add2, %a4, !dbg !181
+  %add4 = add nsw i64 %add3, %a5, !dbg !182
+  %add5 = add nsw i64 %add4, %a6, !dbg !183
+  %add6 = add nsw i64 %add5, %a7, !dbg !184
+  %add7 = add nsw i64 %add6, %a8, !dbg !185
+  %add8 = add nsw i64 %add7, %a9, !dbg !186
+  ret i64 %add8, !dbg !187
+}
+
+define dso_local noundef double @c10_mixed(i32 noundef %i, double noundef %d, i64 noundef %l, float noundef %f) local_unnamed_addr !dbg !188 {
+entry:
+    #dbg_value(i32 %i, !192, !DIExpression(), !196)
+    #dbg_value(double %d, !193, !DIExpression(), !196)
+    #dbg_value(i64 %l, !194, !DIExpression(), !196)
+    #dbg_value(float %f, !195, !DIExpression(), !196)
+  %conv = sitofp i32 %i to double, !dbg !197
+  %add = fadd double %d, %conv, !dbg !198
+  %conv1 = sitofp i64 %l to double, !dbg !199
+  %add2 = fadd double %add, %conv1, !dbg !200
+  %conv3 = fpext float %f to double, !dbg !201
+  %add4 = fadd double %add2, %conv3, !dbg !202
+  ret double %add4, !dbg !203
+}
+
+define dso_local range(i64 -9223372036854775807, -9223372036854775808) i64 @c12_outer(ptr nofree noundef readonly captures(none) %p) local_unnamed_addr !dbg !204 {
+entry:
+    #dbg_value(ptr %p, !206, !DIExpression(), !207)
+    #dbg_value(ptr %p, !208, !DIExpression(), !211)
+  %0 = load i32, ptr %p, align 8, !dbg !213, !tbaa !152
+  %mul.i = mul nsw i32 %0, 3, !dbg !214
+  %conv.i = sext i32 %mul.i to i64, !dbg !215
+  %b.i = getelementptr inbounds nuw i8, ptr %p, i64 8, !dbg !216
+  %1 = load i64, ptr %b.i, align 8, !dbg !216, !tbaa !156
+  %add.i = add i64 %1, 1, !dbg !217
+  %add = add i64 %add.i, %conv.i, !dbg !218
+  ret i64 %add, !dbg !219
+}
+
+define dso_local i64 @c13_after_split([2 x i64] %m.coerce, i32 noundef %z) local_unnamed_addr !dbg !220 {
+entry:
+  %m.coerce.fca.1.extract = extractvalue [2 x i64] %m.coerce, 1
+    #dbg_value(i64 poison, !224, !DIExpression(DW_OP_LLVM_fragment, 0, 64), !226)
+    #dbg_value(i64 %m.coerce.fca.1.extract, !224, !DIExpression(DW_OP_LLVM_fragment, 64, 64), !226)
+    #dbg_value(i32 %z, !225, !DIExpression(), !226)
+  %conv = sext i32 %z to i64, !dbg !227
+  %add = add nsw i64 %m.coerce.fca.1.extract, %conv, !dbg !228
+  ret i64 %add, !dbg !229
+}
+
+define dso_local noundef i64 @_Z8c11_crefRK4foo2(ptr nofree noundef nonnull readonly align 8 captures(none) dereferenceable(24) %r) local_unnamed_addr !dbg !230 {
+entry:
+    #dbg_value(ptr %r, !241, !DIExpression(), !242)
+  %0 = load i32, ptr %r, align 8, !dbg !243, !tbaa !244
+  %conv = sext i32 %0 to i64, !dbg !247
+  %b = getelementptr inbounds nuw i8, ptr %r, i64 8, !dbg !248
+  %1 = load i64, ptr %b, align 8, !dbg !248, !tbaa !249
+  %add = add nsw i64 %1, %conv, !dbg !250
+  ret i64 %add, !dbg !251
+}
+
+define dso_local noundef range(i64 -2147483648, 2147483648) i64 @_ZNK1C5scaleEi(ptr nofree noundef nonnull readonly align 4 captures(none) dereferenceable(4) %this, i32 noundef %f) local_unnamed_addr !dbg !252 {
+entry:
+    #dbg_value(ptr %this, !262, !DIExpression(), !265)
+    #dbg_value(i32 %f, !264, !DIExpression(), !265)
+  %0 = load i32, ptr %this, align 4, !dbg !266, !tbaa !267
+  %mul = mul nsw i32 %0, %f, !dbg !269
+  %conv = sext i32 %mul to i64, !dbg !270
+  ret i64 %conv, !dbg !271
+}
+
+define dso_local noundef i64 @_Z14c14_nontrivial2NTi(ptr nofreeobj noundef readonly align 8 captures(none) dead_on_return dereferenceable(16) %n, i32 noundef %z) local_unnamed_addr !dbg !272 {
+entry:
+    #dbg_value(ptr %n, !277, !DIExpression(DW_OP_deref), !279)
+    #dbg_value(i32 %z, !278, !DIExpression(), !279)
+  %0 = load i64, ptr %n, align 8, !dbg !280, !tbaa !281
+  %arrayidx2 = getelementptr inbounds nuw i8, ptr %n, i64 8, !dbg !282
+  %1 = load i64, ptr %arrayidx2, align 8, !dbg !282, !tbaa !281
+  %conv = sext i32 %z to i64, !dbg !283
+  %add = add i64 %0, %conv, !dbg !284
+  %add3 = add i64 %add, %1, !dbg !285
+  ret i64 %add3, !dbg !286
+}
+
+!llvm.dbg.cu = !{!0, !4}
+!llvm.module.flags = !{!17, !18, !19, !20, !21, !22, !23, !24, !25, !26, !27, !28, !29}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, retainedTypes: !2, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "cases.c", directory: "/")
+!2 = !{!3}
+!3 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!4 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !5, isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!5 = !DIFile(filename: "cases.cpp", directory: "/")
+!7 = !{!8, !9, i64 0}
+!8 = !{!"__libc_errno", !9, i64 0}
+!9 = !{!"int", !10, i64 0}
+!10 = !{!"omnipotent char", !11, i64 0}
+!11 = !{!"Simple C/C++ TBAA"}
+!12 = !{!13, !14, i64 0}
+!13 = !{!"__libc_errno", !14, i64 0}
+!14 = !{!"int", !15, i64 0}
+!15 = !{!"omnipotent char", !16, i64 0}
+!16 = !{!"Simple C++ TBAA"}
+!17 = !{i32 7, !"Dwarf Version", i32 5}
+!18 = !{i32 2, !"Debug Info Version", i32 3}
+!19 = !{i32 1, !"ptrauth-elf-got", i32 0}
+!20 = !{i32 1, !"ptrauth-init-fini", i32 0}
+!21 = !{i32 1, !"ptrauth-init-fini-address-discrimination", i32 0}
+!22 = !{i32 1, !"ptrauth-sign-personality", i32 0}
+!23 = !{i32 1, !"aarch64-elf-pauthabi-platform", i32 268435458}
+!24 = !{i32 1, !"aarch64-elf-pauthabi-version", i32 0}
+!25 = !{i32 8, !"PIC Level", i32 2}
+!26 = !{i32 7, !"PIE Level", i32 2}
+!27 = !{i32 7, !"uwtable", i32 2}
+!28 = !{i32 7, !"frame-pointer", i32 4}
+!29 = !{i32 7, !"debug-info-assignment-tracking", i1 true}
+!30 = distinct !DISubprogram(name: "c01_scalar", scope: !1, file: !1, line: 11, type: !31, scopeLine: 11, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !34, keyInstructions: true)
+!31 = !DISubroutineType(types: !32)
+!32 = !{!3, !33, !3}
+!33 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!34 = !{!35, !36}
+!35 = !DILocalVariable(name: "a", arg: 1, scope: !30, file: !1, line: 11, type: !33)
+!36 = !DILocalVariable(name: "b", arg: 2, scope: !30, file: !1, line: 11, type: !3)
+!37 = !DILocation(line: 0, scope: !30)
+!38 = !DILocation(line: 11, column: 44, scope: !30)
+!39 = !DILocation(line: 11, column: 46, scope: !30, atomGroup: 1, atomRank: 2)
+!40 = !DILocation(line: 11, column: 37, scope: !30, atomGroup: 1, atomRank: 1)
+!41 = distinct !DISubprogram(name: "c02_pointer", scope: !1, file: !1, line: 12, type: !42, scopeLine: 12, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !46, keyInstructions: true)
+!42 = !DISubroutineType(types: !43)
+!43 = !{!3, !44}
+!44 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !45, size: 64)
+!45 = !DIDerivedType(tag: DW_TAG_const_type, baseType: null)
+!46 = !{!47}
+!47 = !DILocalVariable(name: "p", arg: 1, scope: !41, file: !1, line: 12, type: !44)
+!48 = !DILocation(line: 0, scope: !41)
+!49 = !DILocation(line: 12, column: 45, scope: !41, atomGroup: 1, atomRank: 2)
+!50 = !DILocation(line: 12, column: 38, scope: !41, atomGroup: 1, atomRank: 1)
+!51 = distinct !DISubprogram(name: "c03_fp", scope: !1, file: !1, line: 13, type: !52, scopeLine: 13, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !56, keyInstructions: true)
+!52 = !DISubroutineType(types: !53)
+!53 = !{!54, !55, !54}
+!54 = !DIBasicType(name: "double", size: 64, encoding: DW_ATE_float)
+!55 = !DIBasicType(name: "float", size: 32, encoding: DW_ATE_float)
+!56 = !{!57, !58}
+!57 = !DILocalVariable(name: "f", arg: 1, scope: !51, file: !1, line: 13, type: !55)
+!58 = !DILocalVariable(name: "d", arg: 2, scope: !51, file: !1, line: 13, type: !54)
+!59 = !DILocation(line: 0, scope: !51)
+!60 = !DILocation(line: 13, column: 46, scope: !51)
+!61 = !DILocation(line: 13, column: 48, scope: !51, atomGroup: 1, atomRank: 2)
+!62 = !DILocation(line: 13, column: 39, scope: !51, atomGroup: 1, atomRank: 1)
+!63 = distinct !DISubprogram(name: "c04_small", scope: !1, file: !1, line: 14, type: !64, scopeLine: 14, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !70, keyInstructions: true)
+!64 = !DISubroutineType(types: !65)
+!65 = !{!33, !66}
+!66 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "pair", file: !1, line: 6, size: 64, elements: !67)
+!67 = !{!68, !69}
+!68 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !66, file: !1, line: 6, baseType: !33, size: 32)
+!69 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !66, file: !1, line: 6, baseType: !33, size: 32, offset: 32)
+!70 = !{!71}
+!71 = !DILocalVariable(name: "p", arg: 1, scope: !63, file: !1, line: 14, type: !66)
+!72 = !DILocation(line: 0, scope: !63)
+!73 = !DILocation(line: 14, column: 46, scope: !63, atomGroup: 1, atomRank: 2)
+!74 = !DILocation(line: 14, column: 35, scope: !63, atomGroup: 1, atomRank: 1)
+!75 = distinct !DISubprogram(name: "c05_split", scope: !1, file: !1, line: 15, type: !76, scopeLine: 15, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !82, keyInstructions: true)
+!76 = !DISubroutineType(types: !77)
+!77 = !{!54, !78}
+!78 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "mix", file: !1, line: 7, size: 128, elements: !79)
+!79 = !{!80, !81}
+!80 = !DIDerivedType(tag: DW_TAG_member, name: "d", scope: !78, file: !1, line: 7, baseType: !54, size: 64)
+!81 = !DIDerivedType(tag: DW_TAG_member, name: "l", scope: !78, file: !1, line: 7, baseType: !3, size: 64, offset: 64)
+!82 = !{!83}
+!83 = !DILocalVariable(name: "m", arg: 1, scope: !75, file: !1, line: 15, type: !78)
+!84 = !DILocation(line: 0, scope: !75)
+!85 = !DILocation(line: 15, column: 50, scope: !75)
+!86 = !DILocation(line: 15, column: 48, scope: !75, atomGroup: 1, atomRank: 2)
+!87 = !DILocation(line: 15, column: 37, scope: !75, atomGroup: 1, atomRank: 1)
+!88 = distinct !DISubprogram(name: "c05_hfa", scope: !1, file: !1, line: 16, type: !89, scopeLine: 16, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !96, keyInstructions: true)
+!89 = !DISubroutineType(types: !90)
+!90 = !{!55, !91}
+!91 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "hfa", file: !1, line: 8, size: 96, elements: !92)
+!92 = !{!93, !94, !95}
+!93 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !91, file: !1, line: 8, baseType: !55, size: 32)
+!94 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !91, file: !1, line: 8, baseType: !55, size: 32, offset: 32)
+!95 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !91, file: !1, line: 8, baseType: !55, size: 32, offset: 64)
+!96 = !{!97}
+!97 = !DILocalVariable(name: "h", arg: 1, scope: !88, file: !1, line: 16, type: !91)
+!98 = !DILocation(line: 0, scope: !88)
+!99 = !DILocation(line: 16, column: 45, scope: !88)
+!100 = !DILocation(line: 16, column: 51, scope: !88, atomGroup: 1, atomRank: 2)
+!101 = !DILocation(line: 16, column: 34, scope: !88, atomGroup: 1, atomRank: 1)
+!102 = distinct !DISubprogram(name: "c06_big", scope: !1, file: !1, line: 17, type: !103, scopeLine: 17, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !111, keyInstructions: true)
+!103 = !DISubroutineType(types: !104)
+!104 = !{!3, !105}
+!105 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "big", file: !1, line: 9, size: 256, elements: !106)
+!106 = !{!107, !108, !109, !110}
+!107 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !105, file: !1, line: 9, baseType: !3, size: 64)
+!108 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !105, file: !1, line: 9, baseType: !3, size: 64, offset: 64)
+!109 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !105, file: !1, line: 9, baseType: !3, size: 64, offset: 128)
+!110 = !DIDerivedType(tag: DW_TAG_member, name: "d", scope: !105, file: !1, line: 9, baseType: !3, size: 64, offset: 192)
+!111 = !{!112}
+!112 = !DILocalVariable(name: "b", arg: 1, scope: !102, file: !1, line: 17, type: !105)
+!113 = !DILocation(line: 0, scope: !102)
+!114 = !DILocation(line: 17, column: 42, scope: !102)
+!115 = !{!116, !117, i64 0}
+!116 = !{!"big", !117, i64 0, !117, i64 8, !117, i64 16, !117, i64 24}
+!117 = !{!"long", !10, i64 0}
+!118 = !DILocation(line: 17, column: 48, scope: !102)
+!119 = !{!116, !117, i64 24}
+!120 = !DILocation(line: 17, column: 44, scope: !102, atomGroup: 1, atomRank: 2)
+!121 = !DILocation(line: 17, column: 33, scope: !102, atomGroup: 1, atomRank: 1)
+!122 = distinct !DISubprogram(name: "c07_sret", scope: !1, file: !1, line: 18, type: !123, scopeLine: 18, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !125, keyInstructions: true)
+!123 = !DISubroutineType(types: !124)
+!124 = !{!105, !3}
+!125 = !{!126, !127}
+!126 = !DILocalVariable(name: "x", arg: 1, scope: !122, file: !1, line: 18, type: !3)
+!127 = !DILocalVariable(name: "r", scope: !122, file: !1, line: 18, type: !105)
+!128 = !DILocation(line: 0, scope: !122)
+!129 = !DILocation(line: 18, column: 45, scope: !122)
+!130 = !DILocation(line: 18, column: 49, scope: !122, atomGroup: 1, atomRank: 1)
+!131 = !DILocation(line: 18, column: 49, scope: !122)
+!132 = !DILocation(line: 18, column: 55, scope: !122, atomGroup: 1, atomRank: 2)
+!133 = !{!116, !117, i64 8}
+!134 = !DILocation(line: 18, column: 62, scope: !122, atomGroup: 1, atomRank: 2)
+!135 = !{!116, !117, i64 16}
+!136 = !DILocation(line: 18, column: 69, scope: !122, atomGroup: 1, atomRank: 2)
+!137 = !DILocation(line: 18, column: 75, scope: !122, atomGroup: 2, atomRank: 1)
+!138 = distinct !DISubprogram(name: "c08_struct_ptr", scope: !1, file: !1, line: 19, type: !139, scopeLine: 19, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !148, keyInstructions: true)
+!139 = !DISubroutineType(types: !140)
+!140 = !{!3, !141}
+!141 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !142, size: 64)
+!142 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "foo", file: !1, line: 5, size: 192, elements: !143)
+!143 = !{!144, !145, !146}
+!144 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !142, file: !1, line: 5, baseType: !33, size: 32)
+!145 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !142, file: !1, line: 5, baseType: !3, size: 64, offset: 64)
+!146 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !142, file: !1, line: 5, baseType: !147, size: 8, offset: 128)
+!147 = !DIBasicType(name: "char", size: 8, encoding: DW_ATE_unsigned_char)
+!148 = !{!149}
+!149 = !DILocalVariable(name: "f", arg: 1, scope: !138, file: !1, line: 19, type: !141)
+!150 = !DILocation(line: 0, scope: !138)
+!151 = !DILocation(line: 19, column: 51, scope: !138)
+!152 = !{!153, !9, i64 0}
+!153 = !{!"foo", !9, i64 0, !117, i64 8, !10, i64 16}
+!154 = !DILocation(line: 19, column: 48, scope: !138)
+!155 = !DILocation(line: 19, column: 58, scope: !138)
+!156 = !{!153, !117, i64 8}
+!157 = !DILocation(line: 19, column: 53, scope: !138)
+!158 = !DILocation(line: 19, column: 65, scope: !138)
+!159 = !{!153, !10, i64 16}
+!160 = !DILocation(line: 19, column: 62, scope: !138)
+!161 = !DILocation(line: 19, column: 60, scope: !138, atomGroup: 1, atomRank: 2)
+!162 = !DILocation(line: 19, column: 41, scope: !138, atomGroup: 1, atomRank: 1)
+!163 = distinct !DISubprogram(name: "c09_stack", scope: !1, file: !1, line: 20, type: !164, scopeLine: 21, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !166, keyInstructions: true)
+!164 = !DISubroutineType(types: !165)
+!165 = !{!3, !3, !3, !3, !3, !3, !3, !3, !3, !3, !3}
+!166 = !{!167, !168, !169, !170, !171, !172, !173, !174, !175, !176}
+!167 = !DILocalVariable(name: "a0", arg: 1, scope: !163, file: !1, line: 20, type: !3)
+!168 = !DILocalVariable(name: "a1", arg: 2, scope: !163, file: !1, line: 20, type: !3)
+!169 = !DILocalVariable(name: "a2", arg: 3, scope: !163, file: !1, line: 20, type: !3)
+!170 = !DILocalVariable(name: "a3", arg: 4, scope: !163, file: !1, line: 20, type: !3)
+!171 = !DILocalVariable(name: "a4", arg: 5, scope: !163, file: !1, line: 20, type: !3)
+!172 = !DILocalVariable(name: "a5", arg: 6, scope: !163, file: !1, line: 20, type: !3)
+!173 = !DILocalVariable(name: "a6", arg: 7, scope: !163, file: !1, line: 21, type: !3)
+!174 = !DILocalVariable(name: "a7", arg: 8, scope: !163, file: !1, line: 21, type: !3)
+!175 = !DILocalVariable(name: "a8", arg: 9, scope: !163, file: !1, line: 21, type: !3)
+!176 = !DILocalVariable(name: "a9", arg: 10, scope: !163, file: !1, line: 21, type: !3)
+!177 = !DILocation(line: 0, scope: !163)
+!178 = !DILocation(line: 22, column: 13, scope: !163)
+!179 = !DILocation(line: 22, column: 18, scope: !163)
+!180 = !DILocation(line: 22, column: 23, scope: !163)
+!181 = !DILocation(line: 22, column: 28, scope: !163)
+!182 = !DILocation(line: 22, column: 33, scope: !163)
+!183 = !DILocation(line: 22, column: 38, scope: !163)
+!184 = !DILocation(line: 22, column: 43, scope: !163)
+!185 = !DILocation(line: 22, column: 48, scope: !163)
+!186 = !DILocation(line: 22, column: 53, scope: !163, atomGroup: 1, atomRank: 2)
+!187 = !DILocation(line: 22, column: 3, scope: !163, atomGroup: 1, atomRank: 1)
+!188 = distinct !DISubprogram(name: "c10_mixed", scope: !1, file: !1, line: 24, type: !189, scopeLine: 24, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !191, keyInstructions: true)
+!189 = !DISubroutineType(types: !190)
+!190 = !{!54, !33, !54, !3, !55}
+!191 = !{!192, !193, !194, !195}
+!192 = !DILocalVariable(name: "i", arg: 1, scope: !188, file: !1, line: 24, type: !33)
+!193 = !DILocalVariable(name: "d", arg: 2, scope: !188, file: !1, line: 24, type: !54)
+!194 = !DILocalVariable(name: "l", arg: 3, scope: !188, file: !1, line: 24, type: !3)
+!195 = !DILocalVariable(name: "f", arg: 4, scope: !188, file: !1, line: 24, type: !55)
+!196 = !DILocation(line: 0, scope: !188)
+!197 = !DILocation(line: 24, column: 64, scope: !188)
+!198 = !DILocation(line: 24, column: 66, scope: !188)
+!199 = !DILocation(line: 24, column: 72, scope: !188)
+!200 = !DILocation(line: 24, column: 70, scope: !188)
+!201 = !DILocation(line: 24, column: 76, scope: !188)
+!202 = !DILocation(line: 24, column: 74, scope: !188, atomGroup: 1, atomRank: 2)
+!203 = !DILocation(line: 24, column: 57, scope: !188, atomGroup: 1, atomRank: 1)
+!204 = distinct !DISubprogram(name: "c12_outer", scope: !1, file: !1, line: 27, type: !139, scopeLine: 27, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !205, keyInstructions: true)
+!205 = !{!206}
+!206 = !DILocalVariable(name: "p", arg: 1, scope: !204, file: !1, line: 27, type: !141)
+!207 = !DILocation(line: 0, scope: !204)
+!208 = !DILocalVariable(name: "p", arg: 1, scope: !209, file: !1, line: 26, type: !141)
+!209 = distinct !DISubprogram(name: "inner", scope: !1, file: !1, line: 26, type: !139, scopeLine: 26, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !210, keyInstructions: true)
+!210 = !{!208}
+!211 = distinct !DILocation(line: 0, scope: !209, inlinedAt: !212)
+!212 = distinct !DILocation(line: 27, column: 43, scope: !204)
+!213 = distinct !DILocation(line: 26, column: 53, scope: !209, inlinedAt: !212)
+!214 = distinct !DILocation(line: 26, column: 55, scope: !209, inlinedAt: !212)
+!215 = distinct !DILocation(line: 26, column: 50, scope: !209, inlinedAt: !212)
+!216 = distinct !DILocation(line: 26, column: 64, scope: !209, inlinedAt: !212)
+!217 = distinct !DILocation(line: 26, column: 59, scope: !209, inlinedAt: !212, atomGroup: 1, atomRank: 2)
+!218 = !DILocation(line: 27, column: 52, scope: !204, atomGroup: 1, atomRank: 2)
+!219 = !DILocation(line: 27, column: 36, scope: !204, atomGroup: 1, atomRank: 1)
+!220 = distinct !DISubprogram(name: "c13_after_split", scope: !1, file: !1, line: 30, type: !221, scopeLine: 30, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !223, keyInstructions: true)
+!221 = !DISubroutineType(types: !222)
+!222 = !{!3, !78, !33}
+!223 = !{!224, !225}
+!224 = !DILocalVariable(name: "m", arg: 1, scope: !220, file: !1, line: 30, type: !78)
+!225 = !DILocalVariable(name: "z", arg: 2, scope: !220, file: !1, line: 30, type: !33)
+!226 = !DILocation(line: 0, scope: !220)
+!227 = !DILocation(line: 30, column: 61, scope: !220)
+!228 = !DILocation(line: 30, column: 59, scope: !220, atomGroup: 1, atomRank: 2)
+!229 = !DILocation(line: 30, column: 48, scope: !220, atomGroup: 1, atomRank: 1)
+!230 = distinct !DISubprogram(name: "c11_cref", linkageName: "_Z8c11_crefRK4foo2", scope: !5, file: !5, line: 3, type: !231, scopeLine: 3, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !4, retainedNodes: !240, keyInstructions: true)
+!231 = !DISubroutineType(types: !232)
+!232 = !{!3, !233}
+!233 = !DIDerivedType(tag: DW_TAG_reference_type, baseType: !234, size: 64)
+!234 = !DIDerivedType(tag: DW_TAG_const_type, baseType: !235)
+!235 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "foo2", file: !5, line: 2, size: 192, flags: DIFlagTypePassByValue, elements: !236, identifier: "_ZTS4foo2")
+!236 = !{!237, !238, !239}
+!237 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !235, file: !5, line: 2, baseType: !33, size: 32)
+!238 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !235, file: !5, line: 2, baseType: !3, size: 64, offset: 64)
+!239 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !235, file: !5, line: 2, baseType: !147, size: 8, offset: 128)
+!240 = !{!241}
+!241 = !DILocalVariable(name: "r", arg: 1, scope: !230, file: !5, line: 3, type: !233)
+!242 = !DILocation(line: 0, scope: !230)
+!243 = !DILocation(line: 3, column: 44, scope: !230)
+!244 = !{!245, !14, i64 0}
+!245 = !{!"_ZTS4foo2", !14, i64 0, !246, i64 8, !15, i64 16}
+!246 = !{!"long", !15, i64 0}
+!247 = !DILocation(line: 3, column: 42, scope: !230)
+!248 = !DILocation(line: 3, column: 50, scope: !230)
+!249 = !{!245, !246, i64 8}
+!250 = !DILocation(line: 3, column: 46, scope: !230, atomGroup: 1, atomRank: 2)
+!251 = !DILocation(line: 3, column: 35, scope: !230, atomGroup: 1, atomRank: 1)
+!252 = distinct !DISubprogram(name: "scale", linkageName: "_ZNK1C5scaleEi", scope: !253, file: !5, line: 5, type: !257, scopeLine: 5, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !4, declaration: !256, retainedNodes: !261, keyInstructions: true)
+!253 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "C", file: !5, line: 4, size: 32, flags: DIFlagTypePassByValue, elements: !254, identifier: "_ZTS1C")
+!254 = !{!255, !256}
+!255 = !DIDerivedType(tag: DW_TAG_member, name: "base", scope: !253, file: !5, line: 4, baseType: !33, size: 32)
+!256 = !DISubprogram(name: "scale", linkageName: "_ZNK1C5scaleEi", scope: !253, file: !5, line: 4, type: !257, scopeLine: 4, flags: DIFlagPrototyped, spFlags: DISPFlagOptimized)
+!257 = !DISubroutineType(types: !258)
+!258 = !{!3, !259, !33}
+!259 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !260, size: 64, flags: DIFlagArtificial | DIFlagObjectPointer)
+!260 = !DIDerivedType(tag: DW_TAG_const_type, baseType: !253)
+!261 = !{!262, !264}
+!262 = !DILocalVariable(name: "this", arg: 1, scope: !252, type: !263, flags: DIFlagArtificial | DIFlagObjectPointer)
+!263 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !260, size: 64)
+!264 = !DILocalVariable(name: "f", arg: 2, scope: !252, file: !5, line: 5, type: !33)
+!265 = !DILocation(line: 0, scope: !252)
+!266 = !DILocation(line: 5, column: 40, scope: !252)
+!267 = !{!268, !14, i64 0}
+!268 = !{!"_ZTS1C", !14, i64 0}
+!269 = !DILocation(line: 5, column: 45, scope: !252, atomGroup: 1, atomRank: 3)
+!270 = !DILocation(line: 5, column: 40, scope: !252, atomGroup: 1, atomRank: 2)
+!271 = !DILocation(line: 5, column: 33, scope: !252, atomGroup: 1, atomRank: 1)
+!272 = distinct !DISubprogram(name: "c14_nontrivial", linkageName: "_Z14c14_nontrivial2NTi", scope: !5, file: !5, line: 9, type: !273, scopeLine: 9, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !4, retainedNodes: !276, keyInstructions: true)
+!273 = !DISubroutineType(types: !274)
+!274 = !{!3, !275, !33}
+!275 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "NT", file: !5, line: 8, size: 128, flags: DIFlagFwdDecl | DIFlagNonTrivial, identifier: "_ZTS2NT")
+!276 = !{!277, !278}
+!277 = !DILocalVariable(name: "n", arg: 1, scope: !272, file: !5, line: 9, type: !275)
+!278 = !DILocalVariable(name: "z", arg: 2, scope: !272, file: !5, line: 9, type: !33)
+!279 = !DILocation(line: 0, scope: !272)
+!280 = !DILocation(line: 9, column: 46, scope: !272)
+!281 = !{!246, !246, i64 0}
+!282 = !DILocation(line: 9, column: 55, scope: !272)
+!283 = !DILocation(line: 9, column: 64, scope: !272)
+!284 = !DILocation(line: 9, column: 53, scope: !272)
+!285 = !DILocation(line: 9, column: 62, scope: !272, atomGroup: 1, atomRank: 2)
+!286 = !DILocation(line: 9, column: 39, scope: !272, atomGroup: 1, atomRank: 1)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/Inputs/trace-args-cases-x86_64.ll b/llvm/test/Instrumentation/SanitizerCoverage/Inputs/trace-args-cases-x86_64.ll
new file mode 100644
index 00000000000000..3f593c7c8eebfa
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/Inputs/trace-args-cases-x86_64.ll
@@ -0,0 +1,475 @@
+; A set of C and C++ argument shapes, as clang -O2 -g emits them
+; for x86_64-linux-gnu: cases.c and cases.cpp, linked into one module.
+source_filename = "llvm-link"
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.big = type { i64, i64, i64, i64 }
+
+define dso_local i64 @c01_scalar(i32 noundef %a, i64 noundef %b) local_unnamed_addr !dbg !23 {
+entry:
+    #dbg_value(i32 %a, !28, !DIExpression(), !30)
+    #dbg_value(i64 %b, !29, !DIExpression(), !30)
+  %conv = sext i32 %a to i64, !dbg !31
+  %add = add nsw i64 %b, %conv, !dbg !32
+  ret i64 %add, !dbg !33
+}
+
+define dso_local noundef i64 @c02_pointer(ptr noundef %p) local_unnamed_addr !dbg !34 {
+entry:
+    #dbg_value(ptr %p, !40, !DIExpression(), !41)
+  %0 = ptrtoint ptr %p to i64, !dbg !42
+  ret i64 %0, !dbg !43
+}
+
+define dso_local noundef double @c03_fp(float noundef %f, double noundef %d) local_unnamed_addr !dbg !44 {
+entry:
+    #dbg_value(float %f, !50, !DIExpression(), !52)
+    #dbg_value(double %d, !51, !DIExpression(), !52)
+  %conv = fpext float %f to double, !dbg !53
+  %add = fadd double %d, %conv, !dbg !54
+  ret double %add, !dbg !55
+}
+
+define dso_local i32 @c04_small(i64 %p.coerce) local_unnamed_addr !dbg !56 {
+entry:
+  %p.sroa.0.0.extract.trunc = trunc i64 %p.coerce to i32
+  %p.sroa.2.0.extract.shift = lshr i64 %p.coerce, 32
+  %p.sroa.2.0.extract.trunc = trunc nuw i64 %p.sroa.2.0.extract.shift to i32
+    #dbg_value(i32 %p.sroa.0.0.extract.trunc, !64, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !65)
+    #dbg_value(i32 %p.sroa.2.0.extract.trunc, !64, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !65)
+  %add = add nsw i32 %p.sroa.2.0.extract.trunc, %p.sroa.0.0.extract.trunc, !dbg !66
+  ret i32 %add, !dbg !67
+}
+
+define dso_local double @c05_split(double %m.coerce0, i64 %m.coerce1) local_unnamed_addr !dbg !68 {
+entry:
+    #dbg_value(double %m.coerce0, !76, !DIExpression(DW_OP_LLVM_fragment, 0, 64), !77)
+    #dbg_value(i64 %m.coerce1, !76, !DIExpression(DW_OP_LLVM_fragment, 64, 64), !77)
+  %conv = sitofp i64 %m.coerce1 to double, !dbg !78
+  %add = fadd double %m.coerce0, %conv, !dbg !79
+  ret double %add, !dbg !80
+}
+
+define dso_local float @c05_hfa(<2 x float> %h.coerce0, float %h.coerce1) local_unnamed_addr !dbg !81 {
+entry:
+    #dbg_value(<2 x float> %h.coerce0, !90, !DIExpression(DW_OP_LLVM_fragment, 0, 64), !91)
+    #dbg_value(float %h.coerce1, !90, !DIExpression(DW_OP_LLVM_fragment, 64, 32), !91)
+  %shift = shufflevector <2 x float> %h.coerce0, <2 x float> poison, <2 x i32> <i32 1, i32 poison>, !dbg !92
+  %foldExtExtBinop = fadd <2 x float> %h.coerce0, %shift, !dbg !92
+  %add = extractelement <2 x float> %foldExtExtBinop, i64 0, !dbg !92
+  %add1 = fadd float %h.coerce1, %add, !dbg !93
+  ret float %add1, !dbg !94
+}
+
+define dso_local i64 @c06_big(ptr nofree noundef readonly byval(%struct.big) align 8 captures(none) %b) local_unnamed_addr !dbg !95 {
+entry:
+    #dbg_declare(ptr %b, !105, !DIExpression(), !106)
+  %0 = load i64, ptr %b, align 8, !dbg !107, !tbaa !108
+  %d = getelementptr inbounds nuw i8, ptr %b, i64 24, !dbg !111
+  %1 = load i64, ptr %d, align 8, !dbg !111, !tbaa !112
+  %add = add nsw i64 %1, %0, !dbg !113
+  ret i64 %add, !dbg !114
+}
+
+define dso_local void @c07_sret(ptr dead_on_unwind noalias nofree writable writeonly sret(%struct.big) align 8 captures(none) initializes((0, 32)) %agg.result, i64 noundef %x) local_unnamed_addr !dbg !115 {
+entry:
+    #dbg_value(i64 %x, !119, !DIExpression(), !121)
+    #dbg_declare(ptr %agg.result, !120, !DIExpression(), !122)
+  %0 = insertelement <4 x i64> poison, i64 %x, i64 0, !dbg !123
+  %1 = shufflevector <4 x i64> %0, <4 x i64> poison, <4 x i32> zeroinitializer, !dbg !123
+  %2 = add nsw <4 x i64> %1, <i64 0, i64 1, i64 2, i64 3>, !dbg !123
+  store <4 x i64> %2, ptr %agg.result, align 8, !dbg !124, !tbaa !125
+  ret void, !dbg !126
+}
+
+define dso_local i64 @c08_struct_ptr(ptr nofree noundef readonly captures(none) %f) local_unnamed_addr !dbg !127 {
+entry:
+    #dbg_value(ptr %f, !138, !DIExpression(), !139)
+  %0 = load i32, ptr %f, align 8, !dbg !140, !tbaa !141
+  %conv = sext i32 %0 to i64, !dbg !143
+  %b = getelementptr inbounds nuw i8, ptr %f, i64 8, !dbg !144
+  %1 = load i64, ptr %b, align 8, !dbg !144, !tbaa !145
+  %add = add nsw i64 %1, %conv, !dbg !146
+  %c = getelementptr inbounds nuw i8, ptr %f, i64 16, !dbg !147
+  %2 = load i8, ptr %c, align 8, !dbg !147, !tbaa !148
+  %conv1 = sext i8 %2 to i64, !dbg !149
+  %add2 = add nsw i64 %add, %conv1, !dbg !150
+  ret i64 %add2, !dbg !151
+}
+
+define dso_local i64 @c09_stack(i64 noundef %a0, i64 noundef %a1, i64 noundef %a2, i64 noundef %a3, i64 noundef %a4, i64 noundef %a5, i64 noundef %a6, i64 noundef %a7, i64 noundef %a8, i64 noundef %a9) local_unnamed_addr !dbg !152 {
+entry:
+    #dbg_value(i64 %a0, !156, !DIExpression(), !166)
+    #dbg_value(i64 %a1, !157, !DIExpression(), !166)
+    #dbg_value(i64 %a2, !158, !DIExpression(), !166)
+    #dbg_value(i64 %a3, !159, !DIExpression(), !166)
+    #dbg_value(i64 %a4, !160, !DIExpression(), !166)
+    #dbg_value(i64 %a5, !161, !DIExpression(), !166)
+    #dbg_value(i64 %a6, !162, !DIExpression(), !166)
+    #dbg_value(i64 %a7, !163, !DIExpression(), !166)
+    #dbg_value(i64 %a8, !164, !DIExpression(), !166)
+    #dbg_value(i64 %a9, !165, !DIExpression(), !166)
+  %add = add nsw i64 %a1, %a0, !dbg !167
+  %add1 = add nsw i64 %add, %a2, !dbg !168
+  %add2 = add nsw i64 %add1, %a3, !dbg !169
+  %add3 = add nsw i64 %add2, %a4, !dbg !170
+  %add4 = add nsw i64 %add3, %a5, !dbg !171
+  %add5 = add nsw i64 %add4, %a6, !dbg !172
+  %add6 = add nsw i64 %add5, %a7, !dbg !173
+  %add7 = add nsw i64 %add6, %a8, !dbg !174
+  %add8 = add nsw i64 %add7, %a9, !dbg !175
+  ret i64 %add8, !dbg !176
+}
+
+define dso_local noundef double @c10_mixed(i32 noundef %i, double noundef %d, i64 noundef %l, float noundef %f) local_unnamed_addr !dbg !177 {
+entry:
+    #dbg_value(i32 %i, !181, !DIExpression(), !185)
+    #dbg_value(double %d, !182, !DIExpression(), !185)
+    #dbg_value(i64 %l, !183, !DIExpression(), !185)
+    #dbg_value(float %f, !184, !DIExpression(), !185)
+  %conv = sitofp i32 %i to double, !dbg !186
+  %add = fadd double %d, %conv, !dbg !187
+  %conv1 = sitofp i64 %l to double, !dbg !188
+  %add2 = fadd double %add, %conv1, !dbg !189
+  %conv3 = fpext float %f to double, !dbg !190
+  %add4 = fadd double %add2, %conv3, !dbg !191
+  ret double %add4, !dbg !192
+}
+
+define dso_local range(i64 -9223372036854775807, -9223372036854775808) i64 @c12_outer(ptr nofree noundef readonly captures(none) %p) local_unnamed_addr !dbg !193 {
+entry:
+    #dbg_value(ptr %p, !195, !DIExpression(), !196)
+    #dbg_value(ptr %p, !197, !DIExpression(), !200)
+  %0 = load i32, ptr %p, align 8, !dbg !202, !tbaa !141
+  %mul.i = mul nsw i32 %0, 3, !dbg !203
+  %conv.i = sext i32 %mul.i to i64, !dbg !204
+  %b.i = getelementptr inbounds nuw i8, ptr %p, i64 8, !dbg !205
+  %1 = load i64, ptr %b.i, align 8, !dbg !205, !tbaa !145
+  %add.i = add i64 %1, 1, !dbg !206
+  %add = add i64 %add.i, %conv.i, !dbg !207
+  ret i64 %add, !dbg !208
+}
+
+define dso_local i64 @c13_after_split(double %m.coerce0, i64 %m.coerce1, i32 noundef %z) local_unnamed_addr !dbg !209 {
+entry:
+    #dbg_value(double %m.coerce0, !213, !DIExpression(DW_OP_LLVM_fragment, 0, 64), !215)
+    #dbg_value(i64 %m.coerce1, !213, !DIExpression(DW_OP_LLVM_fragment, 64, 64), !215)
+    #dbg_value(i32 %z, !214, !DIExpression(), !215)
+  %conv = sext i32 %z to i64, !dbg !216
+  %add = add nsw i64 %m.coerce1, %conv, !dbg !217
+  ret i64 %add, !dbg !218
+}
+
+define dso_local noundef i64 @_Z8c11_crefRK4foo2(ptr nofree noundef nonnull readonly align 8 captures(none) dereferenceable(24) %r) local_unnamed_addr !dbg !219 {
+entry:
+    #dbg_value(ptr %r, !230, !DIExpression(), !231)
+  %0 = load i32, ptr %r, align 8, !dbg !232, !tbaa !233
+  %conv = sext i32 %0 to i64, !dbg !236
+  %b = getelementptr inbounds nuw i8, ptr %r, i64 8, !dbg !237
+  %1 = load i64, ptr %b, align 8, !dbg !237, !tbaa !238
+  %add = add nsw i64 %1, %conv, !dbg !239
+  ret i64 %add, !dbg !240
+}
+
+define dso_local noundef range(i64 -2147483648, 2147483648) i64 @_ZNK1C5scaleEi(ptr nofree noundef nonnull readonly align 4 captures(none) dereferenceable(4) %this, i32 noundef %f) local_unnamed_addr align 2 !dbg !241 {
+entry:
+    #dbg_value(ptr %this, !251, !DIExpression(), !254)
+    #dbg_value(i32 %f, !253, !DIExpression(), !254)
+  %0 = load i32, ptr %this, align 4, !dbg !255, !tbaa !256
+  %mul = mul nsw i32 %0, %f, !dbg !258
+  %conv = sext i32 %mul to i64, !dbg !259
+  ret i64 %conv, !dbg !260
+}
+
+define dso_local noundef i64 @_Z14c14_nontrivial2NTi(ptr nofreeobj noundef readonly align 8 captures(none) dead_on_return dereferenceable(16) %n, i32 noundef %z) local_unnamed_addr !dbg !261 {
+entry:
+    #dbg_value(ptr %n, !266, !DIExpression(DW_OP_deref), !268)
+    #dbg_value(i32 %z, !267, !DIExpression(), !268)
+  %0 = load i64, ptr %n, align 8, !dbg !269, !tbaa !270
+  %arrayidx2 = getelementptr inbounds nuw i8, ptr %n, i64 8, !dbg !271
+  %1 = load i64, ptr %arrayidx2, align 8, !dbg !271, !tbaa !270
+  %conv = sext i32 %z to i64, !dbg !272
+  %add = add i64 %0, %conv, !dbg !273
+  %add3 = add i64 %add, %1, !dbg !274
+  ret i64 %add3, !dbg !275
+}
+
+!llvm.dbg.cu = !{!0, !4}
+!llvm.module.flags = !{!17, !18, !19, !20, !21, !22}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, retainedTypes: !2, splitDebugInlining: false, nameTableKind: None)
+!1 = !DIFile(filename: "cases.c", directory: "/")
+!2 = !{!3}
+!3 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!4 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus_14, file: !5, isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
+!5 = !DIFile(filename: "cases.cpp", directory: "/")
+!7 = !{!8, !9, i64 0}
+!8 = !{!"__libc_errno", !9, i64 0}
+!9 = !{!"int", !10, i64 0}
+!10 = !{!"omnipotent char", !11, i64 0}
+!11 = !{!"Simple C/C++ TBAA"}
+!12 = !{!13, !14, i64 0}
+!13 = !{!"__libc_errno", !14, i64 0}
+!14 = !{!"int", !15, i64 0}
+!15 = !{!"omnipotent char", !16, i64 0}
+!16 = !{!"Simple C++ TBAA"}
+!17 = !{i32 7, !"Dwarf Version", i32 5}
+!18 = !{i32 2, !"Debug Info Version", i32 3}
+!19 = !{i32 8, !"PIC Level", i32 2}
+!20 = !{i32 7, !"PIE Level", i32 2}
+!21 = !{i32 7, !"uwtable", i32 2}
+!22 = !{i32 7, !"debug-info-assignment-tracking", i1 true}
+!23 = distinct !DISubprogram(name: "c01_scalar", scope: !1, file: !1, line: 11, type: !24, scopeLine: 11, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !27, keyInstructions: true)
+!24 = !DISubroutineType(types: !25)
+!25 = !{!3, !26, !3}
+!26 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!27 = !{!28, !29}
+!28 = !DILocalVariable(name: "a", arg: 1, scope: !23, file: !1, line: 11, type: !26)
+!29 = !DILocalVariable(name: "b", arg: 2, scope: !23, file: !1, line: 11, type: !3)
+!30 = !DILocation(line: 0, scope: !23)
+!31 = !DILocation(line: 11, column: 44, scope: !23)
+!32 = !DILocation(line: 11, column: 46, scope: !23, atomGroup: 1, atomRank: 2)
+!33 = !DILocation(line: 11, column: 37, scope: !23, atomGroup: 1, atomRank: 1)
+!34 = distinct !DISubprogram(name: "c02_pointer", scope: !1, file: !1, line: 12, type: !35, scopeLine: 12, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !39, keyInstructions: true)
+!35 = !DISubroutineType(types: !36)
+!36 = !{!3, !37}
+!37 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !38, size: 64)
+!38 = !DIDerivedType(tag: DW_TAG_const_type, baseType: null)
+!39 = !{!40}
+!40 = !DILocalVariable(name: "p", arg: 1, scope: !34, file: !1, line: 12, type: !37)
+!41 = !DILocation(line: 0, scope: !34)
+!42 = !DILocation(line: 12, column: 45, scope: !34, atomGroup: 1, atomRank: 2)
+!43 = !DILocation(line: 12, column: 38, scope: !34, atomGroup: 1, atomRank: 1)
+!44 = distinct !DISubprogram(name: "c03_fp", scope: !1, file: !1, line: 13, type: !45, scopeLine: 13, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !49, keyInstructions: true)
+!45 = !DISubroutineType(types: !46)
+!46 = !{!47, !48, !47}
+!47 = !DIBasicType(name: "double", size: 64, encoding: DW_ATE_float)
+!48 = !DIBasicType(name: "float", size: 32, encoding: DW_ATE_float)
+!49 = !{!50, !51}
+!50 = !DILocalVariable(name: "f", arg: 1, scope: !44, file: !1, line: 13, type: !48)
+!51 = !DILocalVariable(name: "d", arg: 2, scope: !44, file: !1, line: 13, type: !47)
+!52 = !DILocation(line: 0, scope: !44)
+!53 = !DILocation(line: 13, column: 46, scope: !44)
+!54 = !DILocation(line: 13, column: 48, scope: !44, atomGroup: 1, atomRank: 2)
+!55 = !DILocation(line: 13, column: 39, scope: !44, atomGroup: 1, atomRank: 1)
+!56 = distinct !DISubprogram(name: "c04_small", scope: !1, file: !1, line: 14, type: !57, scopeLine: 14, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !63, keyInstructions: true)
+!57 = !DISubroutineType(types: !58)
+!58 = !{!26, !59}
+!59 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "pair", file: !1, line: 6, size: 64, elements: !60)
+!60 = !{!61, !62}
+!61 = !DIDerivedType(tag: DW_TAG_member, name: "x", scope: !59, file: !1, line: 6, baseType: !26, size: 32)
+!62 = !DIDerivedType(tag: DW_TAG_member, name: "y", scope: !59, file: !1, line: 6, baseType: !26, size: 32, offset: 32)
+!63 = !{!64}
+!64 = !DILocalVariable(name: "p", arg: 1, scope: !56, file: !1, line: 14, type: !59)
+!65 = !DILocation(line: 0, scope: !56)
+!66 = !DILocation(line: 14, column: 46, scope: !56, atomGroup: 1, atomRank: 2)
+!67 = !DILocation(line: 14, column: 35, scope: !56, atomGroup: 1, atomRank: 1)
+!68 = distinct !DISubprogram(name: "c05_split", scope: !1, file: !1, line: 15, type: !69, scopeLine: 15, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !75, keyInstructions: true)
+!69 = !DISubroutineType(types: !70)
+!70 = !{!47, !71}
+!71 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "mix", file: !1, line: 7, size: 128, elements: !72)
+!72 = !{!73, !74}
+!73 = !DIDerivedType(tag: DW_TAG_member, name: "d", scope: !71, file: !1, line: 7, baseType: !47, size: 64)
+!74 = !DIDerivedType(tag: DW_TAG_member, name: "l", scope: !71, file: !1, line: 7, baseType: !3, size: 64, offset: 64)
+!75 = !{!76}
+!76 = !DILocalVariable(name: "m", arg: 1, scope: !68, file: !1, line: 15, type: !71)
+!77 = !DILocation(line: 0, scope: !68)
+!78 = !DILocation(line: 15, column: 50, scope: !68)
+!79 = !DILocation(line: 15, column: 48, scope: !68, atomGroup: 1, atomRank: 2)
+!80 = !DILocation(line: 15, column: 37, scope: !68, atomGroup: 1, atomRank: 1)
+!81 = distinct !DISubprogram(name: "c05_hfa", scope: !1, file: !1, line: 16, type: !82, scopeLine: 16, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !89, keyInstructions: true)
+!82 = !DISubroutineType(types: !83)
+!83 = !{!48, !84}
+!84 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "hfa", file: !1, line: 8, size: 96, elements: !85)
+!85 = !{!86, !87, !88}
+!86 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !84, file: !1, line: 8, baseType: !48, size: 32)
+!87 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !84, file: !1, line: 8, baseType: !48, size: 32, offset: 32)
+!88 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !84, file: !1, line: 8, baseType: !48, size: 32, offset: 64)
+!89 = !{!90}
+!90 = !DILocalVariable(name: "h", arg: 1, scope: !81, file: !1, line: 16, type: !84)
+!91 = !DILocation(line: 0, scope: !81)
+!92 = !DILocation(line: 16, column: 45, scope: !81)
+!93 = !DILocation(line: 16, column: 51, scope: !81, atomGroup: 1, atomRank: 2)
+!94 = !DILocation(line: 16, column: 34, scope: !81, atomGroup: 1, atomRank: 1)
+!95 = distinct !DISubprogram(name: "c06_big", scope: !1, file: !1, line: 17, type: !96, scopeLine: 17, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !104, keyInstructions: true)
+!96 = !DISubroutineType(types: !97)
+!97 = !{!3, !98}
+!98 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "big", file: !1, line: 9, size: 256, elements: !99)
+!99 = !{!100, !101, !102, !103}
+!100 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !98, file: !1, line: 9, baseType: !3, size: 64)
+!101 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !98, file: !1, line: 9, baseType: !3, size: 64, offset: 64)
+!102 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !98, file: !1, line: 9, baseType: !3, size: 64, offset: 128)
+!103 = !DIDerivedType(tag: DW_TAG_member, name: "d", scope: !98, file: !1, line: 9, baseType: !3, size: 64, offset: 192)
+!104 = !{!105}
+!105 = !DILocalVariable(name: "b", arg: 1, scope: !95, file: !1, line: 17, type: !98)
+!106 = !DILocation(line: 17, column: 28, scope: !95)
+!107 = !DILocation(line: 17, column: 42, scope: !95)
+!108 = !{!109, !110, i64 0}
+!109 = !{!"big", !110, i64 0, !110, i64 8, !110, i64 16, !110, i64 24}
+!110 = !{!"long", !10, i64 0}
+!111 = !DILocation(line: 17, column: 48, scope: !95)
+!112 = !{!109, !110, i64 24}
+!113 = !DILocation(line: 17, column: 44, scope: !95, atomGroup: 1, atomRank: 2)
+!114 = !DILocation(line: 17, column: 33, scope: !95, atomGroup: 1, atomRank: 1)
+!115 = distinct !DISubprogram(name: "c07_sret", scope: !1, file: !1, line: 18, type: !116, scopeLine: 18, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !118, keyInstructions: true)
+!116 = !DISubroutineType(types: !117)
+!117 = !{!98, !3}
+!118 = !{!119, !120}
+!119 = !DILocalVariable(name: "x", arg: 1, scope: !115, file: !1, line: 18, type: !3)
+!120 = !DILocalVariable(name: "r", scope: !115, file: !1, line: 18, type: !98)
+!121 = !DILocation(line: 0, scope: !115)
+!122 = !DILocation(line: 18, column: 45, scope: !115)
+!123 = !DILocation(line: 18, column: 55, scope: !115, atomGroup: 1, atomRank: 2)
+!124 = !DILocation(line: 18, column: 49, scope: !115, atomGroup: 1, atomRank: 1)
+!125 = !{!110, !110, i64 0}
+!126 = !DILocation(line: 18, column: 75, scope: !115, atomGroup: 2, atomRank: 1)
+!127 = distinct !DISubprogram(name: "c08_struct_ptr", scope: !1, file: !1, line: 19, type: !128, scopeLine: 19, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !137, keyInstructions: true)
+!128 = !DISubroutineType(types: !129)
+!129 = !{!3, !130}
+!130 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !131, size: 64)
+!131 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "foo", file: !1, line: 5, size: 192, elements: !132)
+!132 = !{!133, !134, !135}
+!133 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !131, file: !1, line: 5, baseType: !26, size: 32)
+!134 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !131, file: !1, line: 5, baseType: !3, size: 64, offset: 64)
+!135 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !131, file: !1, line: 5, baseType: !136, size: 8, offset: 128)
+!136 = !DIBasicType(name: "char", size: 8, encoding: DW_ATE_signed_char)
+!137 = !{!138}
+!138 = !DILocalVariable(name: "f", arg: 1, scope: !127, file: !1, line: 19, type: !130)
+!139 = !DILocation(line: 0, scope: !127)
+!140 = !DILocation(line: 19, column: 51, scope: !127)
+!141 = !{!142, !9, i64 0}
+!142 = !{!"foo", !9, i64 0, !110, i64 8, !10, i64 16}
+!143 = !DILocation(line: 19, column: 48, scope: !127)
+!144 = !DILocation(line: 19, column: 58, scope: !127)
+!145 = !{!142, !110, i64 8}
+!146 = !DILocation(line: 19, column: 53, scope: !127)
+!147 = !DILocation(line: 19, column: 65, scope: !127)
+!148 = !{!142, !10, i64 16}
+!149 = !DILocation(line: 19, column: 62, scope: !127)
+!150 = !DILocation(line: 19, column: 60, scope: !127, atomGroup: 1, atomRank: 2)
+!151 = !DILocation(line: 19, column: 41, scope: !127, atomGroup: 1, atomRank: 1)
+!152 = distinct !DISubprogram(name: "c09_stack", scope: !1, file: !1, line: 20, type: !153, scopeLine: 21, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !155, keyInstructions: true)
+!153 = !DISubroutineType(types: !154)
+!154 = !{!3, !3, !3, !3, !3, !3, !3, !3, !3, !3, !3}
+!155 = !{!156, !157, !158, !159, !160, !161, !162, !163, !164, !165}
+!156 = !DILocalVariable(name: "a0", arg: 1, scope: !152, file: !1, line: 20, type: !3)
+!157 = !DILocalVariable(name: "a1", arg: 2, scope: !152, file: !1, line: 20, type: !3)
+!158 = !DILocalVariable(name: "a2", arg: 3, scope: !152, file: !1, line: 20, type: !3)
+!159 = !DILocalVariable(name: "a3", arg: 4, scope: !152, file: !1, line: 20, type: !3)
+!160 = !DILocalVariable(name: "a4", arg: 5, scope: !152, file: !1, line: 20, type: !3)
+!161 = !DILocalVariable(name: "a5", arg: 6, scope: !152, file: !1, line: 20, type: !3)
+!162 = !DILocalVariable(name: "a6", arg: 7, scope: !152, file: !1, line: 21, type: !3)
+!163 = !DILocalVariable(name: "a7", arg: 8, scope: !152, file: !1, line: 21, type: !3)
+!164 = !DILocalVariable(name: "a8", arg: 9, scope: !152, file: !1, line: 21, type: !3)
+!165 = !DILocalVariable(name: "a9", arg: 10, scope: !152, file: !1, line: 21, type: !3)
+!166 = !DILocation(line: 0, scope: !152)
+!167 = !DILocation(line: 22, column: 13, scope: !152)
+!168 = !DILocation(line: 22, column: 18, scope: !152)
+!169 = !DILocation(line: 22, column: 23, scope: !152)
+!170 = !DILocation(line: 22, column: 28, scope: !152)
+!171 = !DILocation(line: 22, column: 33, scope: !152)
+!172 = !DILocation(line: 22, column: 38, scope: !152)
+!173 = !DILocation(line: 22, column: 43, scope: !152)
+!174 = !DILocation(line: 22, column: 48, scope: !152)
+!175 = !DILocation(line: 22, column: 53, scope: !152, atomGroup: 1, atomRank: 2)
+!176 = !DILocation(line: 22, column: 3, scope: !152, atomGroup: 1, atomRank: 1)
+!177 = distinct !DISubprogram(name: "c10_mixed", scope: !1, file: !1, line: 24, type: !178, scopeLine: 24, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !180, keyInstructions: true)
+!178 = !DISubroutineType(types: !179)
+!179 = !{!47, !26, !47, !3, !48}
+!180 = !{!181, !182, !183, !184}
+!181 = !DILocalVariable(name: "i", arg: 1, scope: !177, file: !1, line: 24, type: !26)
+!182 = !DILocalVariable(name: "d", arg: 2, scope: !177, file: !1, line: 24, type: !47)
+!183 = !DILocalVariable(name: "l", arg: 3, scope: !177, file: !1, line: 24, type: !3)
+!184 = !DILocalVariable(name: "f", arg: 4, scope: !177, file: !1, line: 24, type: !48)
+!185 = !DILocation(line: 0, scope: !177)
+!186 = !DILocation(line: 24, column: 64, scope: !177)
+!187 = !DILocation(line: 24, column: 66, scope: !177)
+!188 = !DILocation(line: 24, column: 72, scope: !177)
+!189 = !DILocation(line: 24, column: 70, scope: !177)
+!190 = !DILocation(line: 24, column: 76, scope: !177)
+!191 = !DILocation(line: 24, column: 74, scope: !177, atomGroup: 1, atomRank: 2)
+!192 = !DILocation(line: 24, column: 57, scope: !177, atomGroup: 1, atomRank: 1)
+!193 = distinct !DISubprogram(name: "c12_outer", scope: !1, file: !1, line: 27, type: !128, scopeLine: 27, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !194, keyInstructions: true)
+!194 = !{!195}
+!195 = !DILocalVariable(name: "p", arg: 1, scope: !193, file: !1, line: 27, type: !130)
+!196 = !DILocation(line: 0, scope: !193)
+!197 = !DILocalVariable(name: "p", arg: 1, scope: !198, file: !1, line: 26, type: !130)
+!198 = distinct !DISubprogram(name: "inner", scope: !1, file: !1, line: 26, type: !128, scopeLine: 26, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !199, keyInstructions: true)
+!199 = !{!197}
+!200 = distinct !DILocation(line: 0, scope: !198, inlinedAt: !201)
+!201 = distinct !DILocation(line: 27, column: 43, scope: !193)
+!202 = distinct !DILocation(line: 26, column: 53, scope: !198, inlinedAt: !201)
+!203 = distinct !DILocation(line: 26, column: 55, scope: !198, inlinedAt: !201)
+!204 = distinct !DILocation(line: 26, column: 50, scope: !198, inlinedAt: !201)
+!205 = distinct !DILocation(line: 26, column: 64, scope: !198, inlinedAt: !201)
+!206 = distinct !DILocation(line: 26, column: 59, scope: !198, inlinedAt: !201, atomGroup: 1, atomRank: 2)
+!207 = !DILocation(line: 27, column: 52, scope: !193, atomGroup: 1, atomRank: 2)
+!208 = !DILocation(line: 27, column: 36, scope: !193, atomGroup: 1, atomRank: 1)
+!209 = distinct !DISubprogram(name: "c13_after_split", scope: !1, file: !1, line: 30, type: !210, scopeLine: 30, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !212, keyInstructions: true)
+!210 = !DISubroutineType(types: !211)
+!211 = !{!3, !71, !26}
+!212 = !{!213, !214}
+!213 = !DILocalVariable(name: "m", arg: 1, scope: !209, file: !1, line: 30, type: !71)
+!214 = !DILocalVariable(name: "z", arg: 2, scope: !209, file: !1, line: 30, type: !26)
+!215 = !DILocation(line: 0, scope: !209)
+!216 = !DILocation(line: 30, column: 61, scope: !209)
+!217 = !DILocation(line: 30, column: 59, scope: !209, atomGroup: 1, atomRank: 2)
+!218 = !DILocation(line: 30, column: 48, scope: !209, atomGroup: 1, atomRank: 1)
+!219 = distinct !DISubprogram(name: "c11_cref", linkageName: "_Z8c11_crefRK4foo2", scope: !5, file: !5, line: 3, type: !220, scopeLine: 3, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !4, retainedNodes: !229, keyInstructions: true)
+!220 = !DISubroutineType(types: !221)
+!221 = !{!3, !222}
+!222 = !DIDerivedType(tag: DW_TAG_reference_type, baseType: !223, size: 64)
+!223 = !DIDerivedType(tag: DW_TAG_const_type, baseType: !224)
+!224 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "foo2", file: !5, line: 2, size: 192, flags: DIFlagTypePassByValue, elements: !225, identifier: "_ZTS4foo2")
+!225 = !{!226, !227, !228}
+!226 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !224, file: !5, line: 2, baseType: !26, size: 32)
+!227 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !224, file: !5, line: 2, baseType: !3, size: 64, offset: 64)
+!228 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !224, file: !5, line: 2, baseType: !136, size: 8, offset: 128)
+!229 = !{!230}
+!230 = !DILocalVariable(name: "r", arg: 1, scope: !219, file: !5, line: 3, type: !222)
+!231 = !DILocation(line: 0, scope: !219)
+!232 = !DILocation(line: 3, column: 44, scope: !219)
+!233 = !{!234, !14, i64 0}
+!234 = !{!"_ZTS4foo2", !14, i64 0, !235, i64 8, !15, i64 16}
+!235 = !{!"long", !15, i64 0}
+!236 = !DILocation(line: 3, column: 42, scope: !219)
+!237 = !DILocation(line: 3, column: 50, scope: !219)
+!238 = !{!234, !235, i64 8}
+!239 = !DILocation(line: 3, column: 46, scope: !219, atomGroup: 1, atomRank: 2)
+!240 = !DILocation(line: 3, column: 35, scope: !219, atomGroup: 1, atomRank: 1)
+!241 = distinct !DISubprogram(name: "scale", linkageName: "_ZNK1C5scaleEi", scope: !242, file: !5, line: 5, type: !246, scopeLine: 5, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !4, declaration: !245, retainedNodes: !250, keyInstructions: true)
+!242 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "C", file: !5, line: 4, size: 32, flags: DIFlagTypePassByValue, elements: !243, identifier: "_ZTS1C")
+!243 = !{!244, !245}
+!244 = !DIDerivedType(tag: DW_TAG_member, name: "base", scope: !242, file: !5, line: 4, baseType: !26, size: 32)
+!245 = !DISubprogram(name: "scale", linkageName: "_ZNK1C5scaleEi", scope: !242, file: !5, line: 4, type: !246, scopeLine: 4, flags: DIFlagPrototyped, spFlags: DISPFlagOptimized)
+!246 = !DISubroutineType(types: !247)
+!247 = !{!3, !248, !26}
+!248 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !249, size: 64, flags: DIFlagArtificial | DIFlagObjectPointer)
+!249 = !DIDerivedType(tag: DW_TAG_const_type, baseType: !242)
+!250 = !{!251, !253}
+!251 = !DILocalVariable(name: "this", arg: 1, scope: !241, type: !252, flags: DIFlagArtificial | DIFlagObjectPointer)
+!252 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !249, size: 64)
+!253 = !DILocalVariable(name: "f", arg: 2, scope: !241, file: !5, line: 5, type: !26)
+!254 = !DILocation(line: 0, scope: !241)
+!255 = !DILocation(line: 5, column: 40, scope: !241)
+!256 = !{!257, !14, i64 0}
+!257 = !{!"_ZTS1C", !14, i64 0}
+!258 = !DILocation(line: 5, column: 45, scope: !241, atomGroup: 1, atomRank: 3)
+!259 = !DILocation(line: 5, column: 40, scope: !241, atomGroup: 1, atomRank: 2)
+!260 = !DILocation(line: 5, column: 33, scope: !241, atomGroup: 1, atomRank: 1)
+!261 = distinct !DISubprogram(name: "c14_nontrivial", linkageName: "_Z14c14_nontrivial2NTi", scope: !5, file: !5, line: 9, type: !262, scopeLine: 9, flags: DIFlagPrototyped | DIFlagAllCallsDescribed, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !4, retainedNodes: !265, keyInstructions: true)
+!262 = !DISubroutineType(types: !263)
+!263 = !{!3, !264, !26}
+!264 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "NT", file: !5, line: 8, size: 128, flags: DIFlagFwdDecl | DIFlagNonTrivial, identifier: "_ZTS2NT")
+!265 = !{!266, !267}
+!266 = !DILocalVariable(name: "n", arg: 1, scope: !261, file: !5, line: 9, type: !264)
+!267 = !DILocalVariable(name: "z", arg: 2, scope: !261, file: !5, line: 9, type: !26)
+!268 = !DILocation(line: 0, scope: !261)
+!269 = !DILocation(line: 9, column: 46, scope: !261)
+!270 = !{!235, !235, i64 0}
+!271 = !DILocation(line: 9, column: 55, scope: !261)
+!272 = !DILocation(line: 9, column: 64, scope: !261)
+!273 = !DILocation(line: 9, column: 53, scope: !261)
+!274 = !DILocation(line: 9, column: 62, scope: !261, atomGroup: 1, atomRank: 2)
+!275 = !DILocation(line: 9, column: 39, scope: !261, atomGroup: 1, atomRank: 1)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance-codegen-aarch64.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance-codegen-aarch64.ll
new file mode 100644
index 00000000000000..f582b04dd52495
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance-codegen-aarch64.ll
@@ -0,0 +1,36 @@
+; Debug information must not change the machine code of trace-args/trace-ret
+; (AArch64 AAPCS64). The input is what clang -O2 -g emits for the argument shapes of
+; Inputs/trace-args-cases-aarch64.ll; stripping its debug information first stands in
+; for the same source built without -g. Code generation runs as clang runs it
+; at -O2 -g, with call-site information.
+;
+; REQUIRES: aarch64-registered-target
+;
+; RUN: opt -passes='module(sancov-module)' -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret \
+; RUN:   %S/Inputs/trace-args-cases-aarch64.ll -o %t.g.bc
+; RUN: opt -strip-debug %S/Inputs/trace-args-cases-aarch64.ll \
+; RUN:   | opt -passes='module(sancov-module)' -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret -o %t.nog.bc
+; RUN: llc -O2 -filetype=obj -emit-call-site-info %t.g.bc -o %t.g.o
+; RUN: llc -O2 -filetype=obj -emit-call-site-info %t.nog.bc -o %t.nog.o
+;
+; The debug build has debug information, the other has none.
+; RUN: llvm-readobj --sections %t.g.o | FileCheck %s --check-prefix=G
+; RUN: llvm-readobj --sections %t.nog.o | FileCheck %s --check-prefix=NOG
+; G: Name: .debug_info
+; NOG-NOT: Name: .debug_
+;
+; .text is byte-identical.
+; RUN: llvm-objcopy --dump-section=.text=%t.g.text %t.g.o
+; RUN: llvm-objcopy --dump-section=.text=%t.nog.text %t.nog.o
+; RUN: cmp %t.g.text %t.nog.text
+;
+; And it does trace: the same calls in both, one per IR argument or
+; first-class aggregate element, and one per return.
+; RUN: llvm-objdump -d -r %t.g.o | FileCheck %s --check-prefix=ARGS
+; RUN: llvm-objdump -d -r %t.nog.o | FileCheck %s --check-prefix=ARGS
+; RUN: llvm-objdump -d -r %t.g.o | FileCheck %s --check-prefix=RET
+; RUN: llvm-objdump -d -r %t.nog.o | FileCheck %s --check-prefix=RET
+; ARGS-COUNT-37: R_{{[A-Z0-9_]+}} __sanitizer_cov_trace_args
+; ARGS-NOT: __sanitizer_cov_trace_args
+; RET-COUNT-16: R_{{[A-Z0-9_]+}} __sanitizer_cov_trace_ret
+; RET-NOT: __sanitizer_cov_trace_ret
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance-codegen-x86_64.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance-codegen-x86_64.ll
new file mode 100644
index 00000000000000..4d8b55758bee30
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance-codegen-x86_64.ll
@@ -0,0 +1,36 @@
+; Debug information must not change the machine code of trace-args/trace-ret
+; (x86-64 SysV). The input is what clang -O2 -g emits for the argument shapes of
+; Inputs/trace-args-cases-x86_64.ll; stripping its debug information first stands in
+; for the same source built without -g. Code generation runs as clang runs it
+; at -O2 -g, with call-site information.
+;
+; REQUIRES: x86-registered-target
+;
+; RUN: opt -passes='module(sancov-module)' -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret \
+; RUN:   %S/Inputs/trace-args-cases-x86_64.ll -o %t.g.bc
+; RUN: opt -strip-debug %S/Inputs/trace-args-cases-x86_64.ll \
+; RUN:   | opt -passes='module(sancov-module)' -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret -o %t.nog.bc
+; RUN: llc -O2 -filetype=obj -emit-call-site-info %t.g.bc -o %t.g.o
+; RUN: llc -O2 -filetype=obj -emit-call-site-info %t.nog.bc -o %t.nog.o
+;
+; The debug build has debug information, the other has none.
+; RUN: llvm-readobj --sections %t.g.o | FileCheck %s --check-prefix=G
+; RUN: llvm-readobj --sections %t.nog.o | FileCheck %s --check-prefix=NOG
+; G: Name: .debug_info
+; NOG-NOT: Name: .debug_
+;
+; .text is byte-identical.
+; RUN: llvm-objcopy --dump-section=.text=%t.g.text %t.g.o
+; RUN: llvm-objcopy --dump-section=.text=%t.nog.text %t.nog.o
+; RUN: cmp %t.g.text %t.nog.text
+;
+; And it does trace: the same calls in both, one per IR argument or
+; first-class aggregate element, and one per return.
+; RUN: llvm-objdump -d -r %t.g.o | FileCheck %s --check-prefix=ARGS
+; RUN: llvm-objdump -d -r %t.nog.o | FileCheck %s --check-prefix=ARGS
+; RUN: llvm-objdump -d -r %t.g.o | FileCheck %s --check-prefix=RET
+; RUN: llvm-objdump -d -r %t.nog.o | FileCheck %s --check-prefix=RET
+; ARGS-COUNT-36: R_{{[A-Z0-9_]+}} __sanitizer_cov_trace_args
+; ARGS-NOT: __sanitizer_cov_trace_args
+; RET-COUNT-16: R_{{[A-Z0-9_]+}} __sanitizer_cov_trace_ret
+; RET-NOT: __sanitizer_cov_trace_ret
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll
new file mode 100644
index 00000000000000..5bd665c7ae5301
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-debug-invariance.ll
@@ -0,0 +1,153 @@
+; Debug information may enrich how an observation is interpreted, but must never
+; decide what is observed: instrumenting this module and then stripping its debug
+; information gives exactly the module that stripping first and instrumenting
+; then gives.
+;
+; The input carries everything that could tempt the pass: source-level
+; parameter records that disagree with the IR argument list (an sret pointer, a
+; struct coerced into one register and split into two fragments, a first-class
+; aggregate one of whose fragments was optimized out), a struct pointer with a
+; known field layout, a #dbg_declare of a byval argument, an unoptimized
+; function that stores its arguments to declared allocas, and an inlined callee
+; with parameters of its own. The clang -O2 inputs below add C++ references,
+; `this` and an object passed indirectly, all dereferenceable(N) pointers whose
+; pointees are read as far as the IR attribute says, which debug information
+; must not change either.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-pc-guard -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret \
+; RUN:   | opt -strip-debug -S -o %t.instrumented-then-stripped.ll
+; RUN: opt < %s -strip-debug \
+; RUN:   | opt -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-pc-guard -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret -S -o %t.stripped-then-instrumented.ll
+; RUN: diff %t.instrumented-then-stripped.ll %t.stripped-then-instrumented.ll
+
+; The same holds for what clang -O2 -g emits for the argument shapes of
+; Inputs/trace-args-cases-*.ll, on both x86-64 and AArch64, whose ABIs lower the
+; same source parameters to different IR arguments.
+; RUN: opt < %S/Inputs/trace-args-cases-x86_64.ll -passes='module(sancov-module)' -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret \
+; RUN:   | opt -strip-debug -S -o %t.x86_64.instrumented-then-stripped.ll
+; RUN: opt < %S/Inputs/trace-args-cases-x86_64.ll -strip-debug \
+; RUN:   | opt -passes='module(sancov-module)' -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret -S -o %t.x86_64.stripped-then-instrumented.ll
+; RUN: diff %t.x86_64.instrumented-then-stripped.ll %t.x86_64.stripped-then-instrumented.ll
+; RUN: opt < %S/Inputs/trace-args-cases-aarch64.ll -passes='module(sancov-module)' -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret \
+; RUN:   | opt -strip-debug -S -o %t.aarch64.instrumented-then-stripped.ll
+; RUN: opt < %S/Inputs/trace-args-cases-aarch64.ll -strip-debug \
+; RUN:   | opt -passes='module(sancov-module)' -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret -S -o %t.aarch64.stripped-then-instrumented.ll
+; RUN: diff %t.aarch64.instrumented-then-stripped.ll %t.aarch64.stripped-then-instrumented.ll
+
+; Debug information still gives the calls a location: the function's own
+; opening line, even where the entry block starts with an inlined callee.
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-trace-args -S | FileCheck %s
+; CHECK-LABEL: define i32 @caller(
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(i32 1
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 8, i64 %n, ptr null, i32 0), !dbg ![[ENTRY:[0-9]+]]
+; CHECK-DAG: ![[ENTRY]] = !DILocation(line: 10, scope: ![[CALLER:[0-9]+]])
+; CHECK-DAG: ![[CALLER]] = distinct !DISubprogram(name: "caller"
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.S = type { i64, i64, i64 }
+
+; struct S make(struct pair p, struct S *q) with struct pair { int a, b; }
+define void @make(ptr sret(%struct.S) %r, i64 noundef %p, ptr noundef %q) !dbg !10 {
+entry:
+  %a = trunc i64 %p to i32
+  %hi = lshr i64 %p, 32
+  %b = trunc i64 %hi to i32
+    #dbg_value(i32 %a, !13, !DIExpression(DW_OP_LLVM_fragment, 0, 32), !15)
+    #dbg_value(i32 %b, !13, !DIExpression(DW_OP_LLVM_fragment, 32, 32), !15)
+    #dbg_value(ptr %q, !14, !DIExpression(), !15)
+  store i64 %p, ptr %r, !dbg !15
+  ret void, !dbg !15
+}
+
+; long use(struct S s), passed in memory and described by a #dbg_declare.
+define i64 @use(ptr byval(%struct.S) %s) !dbg !20 {
+entry:
+    #dbg_declare(ptr %s, !21, !DIExpression(), !22)
+  %v = load i64, ptr %s, !dbg !22
+  ret i64 %v, !dbg !22
+}
+
+; long caller(long n) { return leaf(n * 2); } with leaf(long m) inlined.
+define i32 @caller(i64 noundef %n) !dbg !30 {
+entry:
+  %m = shl i64 %n, 1, !dbg !34
+    #dbg_value(i64 %n, !31, !DIExpression(), !33)
+    #dbg_value(i64 %m, !35, !DIExpression(), !34)
+  %t = trunc i64 %m to i32, !dbg !34
+  ret i32 %t, !dbg !33
+}
+
+; long split(struct mix m, int z), with m passed as [2 x i64] (AArch64).
+define i64 @split([2 x i64] noundef %m, i32 noundef %z) !dbg !50 {
+entry:
+  %l = extractvalue [2 x i64] %m, 1
+    #dbg_value(i64 poison, !51, !DIExpression(DW_OP_LLVM_fragment, 0, 64), !53)
+    #dbg_value(i64 %l, !51, !DIExpression(DW_OP_LLVM_fragment, 64, 64), !53)
+    #dbg_value(i32 %z, !52, !DIExpression(), !53)
+  %zl = sext i32 %z to i64
+  %r = add i64 %l, %zl
+  ret i64 %r, !dbg !53
+}
+
+; The same at -O0, where the arguments are stored to declared allocas.
+define i64 @split_o0([2 x i64] noundef %m, i32 noundef %z) !dbg !60 {
+entry:
+  %m.addr = alloca [2 x i64], align 8
+  %z.addr = alloca i32, align 4
+  store [2 x i64] %m, ptr %m.addr, align 8
+    #dbg_declare(ptr %m.addr, !61, !DIExpression(), !63)
+  store i32 %z, ptr %z.addr, align 4
+    #dbg_declare(ptr %z.addr, !62, !DIExpression(), !63)
+  %p = getelementptr inbounds i8, ptr %m.addr, i64 8
+  %l = load i64, ptr %p, align 8, !dbg !63
+  ret i64 %l, !dbg !63
+}
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, isOptimized: true, emissionKind: FullDebug)
+!1 = !DIFile(filename: "invariance.c", directory: "/")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!6 = !DICompositeType(tag: DW_TAG_structure_type, name: "S", file: !1, size: 192, elements: !7)
+!7 = !{!40, !41, !42}
+!8 = !DICompositeType(tag: DW_TAG_structure_type, name: "pair", file: !1, size: 64, elements: !{})
+!9 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !6, size: 64)
+!10 = distinct !DISubprogram(name: "make", scope: !1, file: !1, line: 1, type: !11, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!11 = !DISubroutineType(types: !12)
+!12 = !{!6, !8, !9}
+!13 = !DILocalVariable(name: "p", arg: 1, scope: !10, file: !1, line: 1, type: !8)
+!14 = !DILocalVariable(name: "q", arg: 2, scope: !10, file: !1, line: 1, type: !9)
+!15 = !DILocation(line: 1, column: 1, scope: !10)
+!20 = distinct !DISubprogram(name: "use", scope: !1, file: !1, line: 5, type: !23, scopeLine: 5, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!21 = !DILocalVariable(name: "s", arg: 1, scope: !20, file: !1, line: 5, type: !6)
+!22 = !DILocation(line: 5, column: 1, scope: !20)
+!23 = !DISubroutineType(types: !24)
+!24 = !{!5, !6}
+!30 = distinct !DISubprogram(name: "caller", scope: !1, file: !1, line: 10, type: !36, scopeLine: 10, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!31 = !DILocalVariable(name: "n", arg: 1, scope: !30, file: !1, line: 10, type: !5)
+!32 = distinct !DISubprogram(name: "leaf", scope: !1, file: !1, line: 8, type: !36, scopeLine: 8, spFlags: DISPFlagLocalToUnit | DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!33 = !DILocation(line: 10, column: 1, scope: !30)
+!34 = !DILocation(line: 8, column: 1, scope: !32, inlinedAt: !33)
+!35 = !DILocalVariable(name: "m", arg: 1, scope: !32, file: !1, line: 8, type: !5)
+!36 = !DISubroutineType(types: !37)
+!37 = !{!5, !5}
+!40 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !6, file: !1, baseType: !5, size: 64)
+!41 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !6, file: !1, baseType: !5, size: 64, offset: 64)
+!42 = !DIDerivedType(tag: DW_TAG_member, name: "c", scope: !6, file: !1, baseType: !5, size: 64, offset: 128)
+!43 = !DICompositeType(tag: DW_TAG_structure_type, name: "mix", file: !1, size: 128, elements: !{})
+!44 = !DISubroutineType(types: !45)
+!45 = !{!5, !43, !4}
+!50 = distinct !DISubprogram(name: "split", scope: !1, file: !1, line: 20, type: !44, scopeLine: 20, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0)
+!51 = !DILocalVariable(name: "m", arg: 1, scope: !50, file: !1, line: 20, type: !43)
+!52 = !DILocalVariable(name: "z", arg: 2, scope: !50, file: !1, line: 20, type: !4)
+!53 = !DILocation(line: 20, column: 1, scope: !50)
+!60 = distinct !DISubprogram(name: "split_o0", scope: !1, file: !1, line: 30, type: !44, scopeLine: 30, spFlags: DISPFlagDefinition, unit: !0)
+!61 = !DILocalVariable(name: "m", arg: 1, scope: !60, file: !1, line: 30, type: !43)
+!62 = !DILocalVariable(name: "z", arg: 2, scope: !60, file: !1, line: 30, type: !4)
+!63 = !DILocation(line: 30, column: 1, scope: !60)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-coverage-level.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-coverage-level.ll
new file mode 100644
index 00000000000000..0bb04bc0dda80b
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args-no-coverage-level.ll
@@ -0,0 +1,75 @@
+; Argument and return value tracing instrument function boundaries rather than
+; blocks, so they stand on their own and do not need a coverage level. A direct
+; pass invocation has no level (the clang driver supplies one when it parses
+; -fsanitize-coverage=), and the module pass used to bail out on SCK_None
+; before reaching them -- producing a cleanly linking but entirely
+; uninstrumented module.
+;
+; Without a level, the boundary callbacks must still be emitted, and no block
+; or edge instrumentation may appear.
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret -S | FileCheck %s
+;
+; Asking for no mode at all still instruments nothing.
+; RUN: opt < %s -passes='module(sancov-module)' -S | FileCheck %s --check-prefix=NONE
+;
+; Block coverage requested alongside the boundary modes still works. Note that
+; a level alone is not enough: trace-args/trace-ret count as a requested mode,
+; so they suppress the implicit trace-pc-guard default and it has to be asked
+; for explicitly.
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-pc-guard -sanitizer-coverage-trace-args -S | FileCheck %s --check-prefix=WITHLEVEL
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.S = type { i32, i64 }
+
+; int f(struct S *s, int x)
+define i32 @f(ptr %0, i32 %1) !dbg !8 {
+entry:
+  %s = ptrtoint ptr %0 to i64
+    #dbg_value(ptr %0, !16, !DIExpression(), !19)
+    #dbg_value(i32 %1, !17, !DIExpression(), !19)
+  %a = load i32, ptr %0, align 8, !dbg !19
+  %sum = add nsw i32 %a, %1, !dbg !19
+  ret i32 %sum, !dbg !19
+}
+
+; Both arguments and the return are reported by value.
+; CHECK-LABEL: define i32 @f(
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 8, i64 %{{[0-9]+}}, ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 1, i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_ret(i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+
+; No block instrumentation came along with it.
+; CHECK-NOT: call void @__sanitizer_cov_trace_pc
+; CHECK-NOT: @__sancov_gen_
+; CHECK-NOT: __sancov_guards
+
+; NONE-NOT: __sanitizer_cov
+; NONE-NOT: __sancov
+
+; WITHLEVEL-LABEL: define i32 @f(
+; WITHLEVEL: call void @__sanitizer_cov_trace_args(
+; WITHLEVEL: call void @__sanitizer_cov_trace_pc_guard
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!2, !3}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, emissionKind: FullDebug)
+!1 = !DIFile(filename: "a.c", directory: "/")
+!2 = !{i32 7, !"Dwarf Version", i32 5}
+!3 = !{i32 2, !"Debug Info Version", i32 3}
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DIBasicType(name: "long", size: 64, encoding: DW_ATE_signed)
+!6 = !DIDerivedType(tag: DW_TAG_member, name: "a", scope: !10, file: !1, baseType: !4, size: 32)
+!7 = !DIDerivedType(tag: DW_TAG_member, name: "b", scope: !10, file: !1, baseType: !5, size: 64, offset: 64)
+!8 = distinct !DISubprogram(name: "f", scope: !1, file: !1, line: 2, type: !13, scopeLine: 2, unit: !0, retainedNodes: !18)
+!9 = !{!6, !7}
+!10 = distinct !DICompositeType(tag: DW_TAG_structure_type, name: "S", file: !1, line: 1, size: 128, elements: !9)
+!11 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: !10, size: 64)
+!13 = !DISubroutineType(types: !14)
+!14 = !{!4, !11, !4}
+!16 = !DILocalVariable(name: "s", arg: 1, scope: !8, file: !1, line: 2, type: !11)
+!17 = !DILocalVariable(name: "x", arg: 2, scope: !8, file: !1, line: 2, type: !4)
+!18 = !{!16, !17}
+!19 = !DILocation(line: 2, column: 1, scope: !8)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
new file mode 100644
index 00000000000000..609b0f1a65962b
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-args.ll
@@ -0,0 +1,230 @@
+; Argument tracing: one __sanitizer_cov_trace_args call per IR argument, in the
+; entry block, decided by the IR alone. arg_idx is the position of the argument
+; in the IR parameter list; a struct-return pointer and a Swift error slot are
+; not observed but keep their positions.
+;
+; A value that fits in a register is reported as the value; an argument the ABI
+; says points at a copy of the parameter in memory is reported as that object,
+; with a table of the elements of its IR type, none wider than 8 bytes. A
+; pointer the IR says is dereferenceable is reported with a raw snapshot of the
+; bytes it points to, as many as the attribute says and
+; -sanitizer-coverage-trace-pointee-bytes allows.
+;
+; An argument the function does not use is passed whatever its callers pass -
+; poison, after dead argument elimination - so an object is not read through
+; it; the functions below use theirs, and @unused checks the rule.
+;
+; opt runs the verifier, so a passing run also proves the emitted IR is
+; well-formed.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-trace-args -S | FileCheck %s
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-trace-args \
+; RUN:   -sanitizer-coverage-trace-pointee-bytes=0 -S | FileCheck %s --check-prefix=NOPTEE
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+declare void @use(...)
+
+%struct.T = type { i32, i8, i64 }
+
+; One table per IR type, shared by every object of that type, and one for an
+; object that is not a struct.
+; CHECK: @__sancov_offsets_ = private unnamed_addr constant [6 x i64] [i64 0, i64 4, i64 4, i64 1, i64 8, i64 8]
+; CHECK: @__sancov_offsets_.1 = private unnamed_addr constant [2 x i64] [i64 0, i64 8]
+; A pointee snapshot is a byte array: 16, 64 and 12 bytes, in 8-byte pieces.
+; CHECK: @__sancov_offsets_.2 = private unnamed_addr constant [4 x i64] [i64 0, i64 8, i64 8, i64 8]
+; CHECK: @__sancov_offsets_.3 = private unnamed_addr constant [16 x i64] [i64 0, i64 8, i64 8, i64 8, i64 16, i64 8, i64 24, i64 8, i64 32, i64 8, i64 40, i64 8, i64 48, i64 8, i64 56, i64 8]
+; CHECK: @__sancov_offsets_.4 = private unnamed_addr constant [4 x i64] [i64 0, i64 8, i64 8, i64 4]
+; An opaque [32 x i8] object, in 8-byte pieces; a struct by the elements of its
+; nested structs, and an element wider than 8 bytes in pieces.
+; CHECK: @__sancov_offsets_.5 = private unnamed_addr constant [8 x i64] [i64 0, i64 8, i64 8, i64 8, i64 16, i64 8, i64 24, i64 8]
+; CHECK: @__sancov_offsets_.6 = private unnamed_addr constant [10 x i64] [i64 0, i64 4, i64 8, i64 1, i64 16, i64 8, i64 24, i64 8, i64 32, i64 4]
+; CHECK: @__sancov_offsets_.7 = private unnamed_addr constant [6 x i64] [i64 0, i64 8, i64 8, i64 8, i64 16, i64 1]
+; CHECK-NOT: @__sancov_offsets_
+
+; Scalars and pointers are reported as their value, zero-extended to 64 bits.
+define void @scalars(i1 noundef %b, i8 noundef %c, i32 noundef %i, i64 noundef %l, ptr noundef %p) {
+entry:
+  call void (...) @use(i1 %b, i8 %c, i32 %i, i64 %l, ptr %p)
+  ret void
+}
+; CHECK-LABEL: define void @scalars(
+; CHECK: %[[B:[0-9]+]] = zext i1 %b to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 1, i64 %[[B]], ptr null, i32 0)
+; CHECK: %[[C:[0-9]+]] = zext i8 %c to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 1, i32 1, i64 %[[C]], ptr null, i32 0)
+; CHECK: %[[I:[0-9]+]] = zext i32 %i to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 2, i32 4, i64 %[[I]], ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 3, i32 8, i64 %l, ptr null, i32 0)
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 4, i32 8, i64 %[[P]], ptr null, i32 0)
+
+; A floating-point value or a vector is reported as its bit pattern, a value
+; wider than 64 bits by its low 8 bytes, a pointer with the width of its address
+; space, and a value with no fixed-size bit pattern with size 0.
+define void @wide(float noundef %f, double noundef %d, i128 noundef %w, ptr addrspace(270) noundef %p32, <4 x i32> noundef %v, <2 x float> noundef %v2, <2 x ptr> noundef %vp, <vscale x 2 x i64> noundef %sv) {
+entry:
+  call void (...) @use(float %f, double %d, i128 %w, ptr addrspace(270) %p32, <4 x i32> %v, <2 x float> %v2, <2 x ptr> %vp, <vscale x 2 x i64> %sv)
+  ret void
+}
+; CHECK-LABEL: define void @wide(
+; CHECK: %[[F:[0-9]+]] = bitcast float %f to i32
+; CHECK: %[[FZ:[0-9]+]] = zext i32 %[[F]] to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 4, i64 %[[FZ]], ptr null, i32 0)
+; CHECK: %[[D:[0-9]+]] = bitcast double %d to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 1, i32 8, i64 %[[D]], ptr null, i32 0)
+; CHECK: %[[W:[0-9]+]] = trunc i128 %w to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 2, i32 8, i64 %[[W]], ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 3, i32 4,
+; CHECK: %[[V:[0-9]+]] = bitcast <4 x i32> %v to i128
+; CHECK: %[[VT:[0-9]+]] = trunc i128 %[[V]] to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 4, i32 8, i64 %[[VT]], ptr null, i32 0)
+; CHECK: %[[V2:[0-9]+]] = bitcast <2 x float> %v2 to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 5, i32 8, i64 %[[V2]], ptr null, i32 0)
+; CHECK: %[[VP:[0-9]+]] = ptrtoint <2 x ptr> %vp to <2 x i64>
+; CHECK: %[[VPB:[0-9]+]] = bitcast <2 x i64> %[[VP]] to i128
+; CHECK: %[[VPT:[0-9]+]] = trunc i128 %[[VPB]] to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 6, i32 8, i64 %[[VPT]], ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 7, i32 0, i64 0, ptr null, i32 0)
+
+; A first-class aggregate lives in registers and has no address: each element
+; is reported on its own, under the same arg_idx.
+define void @aggregate({ i32, i64 } noundef %s, [2 x i8] noundef %a) {
+entry:
+  call void (...) @use({ i32, i64 } %s, [2 x i8] %a)
+  ret void
+}
+; CHECK-LABEL: define void @aggregate(
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 4,
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 8,
+; CHECK: call void @__sanitizer_cov_trace_args(i32 1, i32 1,
+; CHECK: call void @__sanitizer_cov_trace_args(i32 1, i32 1,
+
+; byval, inalloca, preallocated and byref arguments are the address of a copy
+; of the parameter: they are reported as that object, described by the IR type.
+define void @objects(ptr byval(%struct.T) %a, ptr preallocated(i64) %b, ptr byref(%struct.T) %c) {
+entry:
+  call void (...) @use(ptr %a, ptr %b, ptr %c)
+  ret void
+}
+; CHECK-LABEL: define void @objects(
+; CHECK: %[[A:[0-9]+]] = ptrtoint ptr %a to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 16, i64 %[[A]], ptr @__sancov_offsets_, i32 3)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 1, i32 8, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.1, i32 1)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 2, i32 16, i64 %{{[0-9]+}}, ptr @__sancov_offsets_, i32 3)
+
+; inalloca has to be the last parameter.
+define void @inalloca(i32 noundef %x, ptr inalloca(%struct.T) %a) {
+entry:
+  call void (...) @use(i32 %x, ptr %a)
+  ret void
+}
+; CHECK-LABEL: define void @inalloca(
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 4,
+; CHECK: call void @__sanitizer_cov_trace_args(i32 1, i32 16, i64 %{{[0-9]+}}, ptr @__sancov_offsets_, i32 3)
+
+; A struct-return pointer is where the return value goes, which trace-ret
+; reports. It is not observed, but %x keeps its IR position, 1.
+define void @sret(ptr sret(%struct.T) %r, i32 noundef %x) {
+entry:
+  call void (...) @use(ptr %r, i32 %x)
+  ret void
+}
+; CHECK-LABEL: define void @sret(
+; CHECK-NOT: ptrtoint ptr %r
+; CHECK: call void @__sanitizer_cov_trace_args(i32 1, i32 4,
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(
+
+; The error slot of a Swift throwing function may only be loaded, stored, or
+; passed on as a swifterror argument: it is skipped, and %x keeps index 2.
+define swiftcc void @swifterror(ptr noundef swiftself %self, ptr swifterror %err, i64 noundef %x) {
+entry:
+  call void (...) @use(ptr %self, i64 %x)
+  ret void
+}
+; CHECK-LABEL: define swiftcc void @swifterror(
+; CHECK-NOT: ptrtoint ptr %err
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 8,
+; CHECK: call void @__sanitizer_cov_trace_args(i32 2, i32 8, i64 %x, ptr null, i32 0)
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(
+
+; The named arguments of a variadic function are reported; the variadic ones
+; have no IR argument.
+define void @varargs(i32 noundef %n, ...) {
+entry:
+  call void (...) @use(i32 %n)
+  ret void
+}
+; CHECK-LABEL: define void @varargs(
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 4,
+; CHECK-NOT: call void @__sanitizer_cov_trace_args(
+
+; The calls go at the first insertion point of the entry block, after its PHI
+; nodes and EH pad, where every argument is available. They are never tail
+; calls: the runtime identifies an observation by its call's return address.
+declare i32 @__gxx_personality_v0(...)
+define i32 @landing(i32 noundef %x) personality ptr @__gxx_personality_v0 {
+entry:
+  ret i32 %x
+}
+; CHECK-LABEL: define i32 @landing(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: %[[X:[0-9]+]] = zext i32 %x to i64
+; CHECK-NEXT: notail call void @__sanitizer_cov_trace_args(i32 0, i32 4, i64 %[[X]], ptr null, i32 0)
+; CHECK-NEXT: ret i32 %x
+
+; An unused argument may be passed anything: an unused value is still
+; reported and frozen, but an unused object is not read, as its pointer may
+; be poison. A value that may be poison - here
+; because none of the arguments is noundef - is frozen, so that the callback
+; receives bits.
+define void @unused(i32 %x, ptr byval(%struct.T) %o, { i32, i64 } %s) {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @unused(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: %[[XF:[0-9]+]] = freeze i32 %x
+; CHECK-NEXT: %[[X:[0-9]+]] = zext i32 %[[XF]] to i64
+; CHECK-NEXT: notail call void @__sanitizer_cov_trace_args(i32 0, i32 4, i64 %[[X]], ptr null, i32 0)
+; CHECK-NEXT: notail call void @__sanitizer_cov_trace_args(i32 1, i32 0, i64 0, ptr null, i32 0)
+; CHECK:      notail call void @__sanitizer_cov_trace_args(i32 2, i32 4,
+; CHECK:      notail call void @__sanitizer_cov_trace_args(i32 2, i32 8,
+; CHECK-NEXT: ret void
+
+; A pointer the IR says is dereferenceable(N) is reported with the first N
+; bytes it points to, at most -sanitizer-coverage-trace-pointee-bytes (64 by
+; default): `val` stays the pointer, `size` is the number of bytes and the
+; table covers them. dereferenceable_or_null may be null, which the runtime
+; checks. A pointer with no such attribute, one in another address space, and
+; one the function does not use are reported by value only.
+define void @pointee(ptr noundef nonnull align 8 dereferenceable(16) %p, ptr dereferenceable_or_null(100) %q, ptr noundef %raw, ptr addrspace(1) dereferenceable(16) %as1, ptr dereferenceable(12) %r, ptr dereferenceable(8) %dead) {
+entry:
+  call void (...) @use(ptr %p, ptr %q, ptr %raw, ptr addrspace(1) %as1, ptr %r)
+  ret void
+}
+; CHECK-LABEL: define void @pointee(
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 16, i64 %[[P]], ptr @__sancov_offsets_.2, i32 2)
+; CHECK: %[[Q:[0-9]+]] = ptrtoint ptr %{{.*}} to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 1, i32 64, i64 %[[Q]], ptr @__sancov_offsets_.3, i32 8)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 2, i32 8, i64 %{{[0-9]+}}, ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 3, i32 8, i64 %{{[0-9]+}}, ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 4, i32 12, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.4, i32 2)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 5, i32 8, i64 %{{[0-9]+}}, ptr null, i32 0)
+; NOPTEE-LABEL: define void @pointee(
+; NOPTEE-COUNT-6: call void @__sanitizer_cov_trace_args(i32 {{[0-5]}}, i32 8, i64 %{{[0-9]+}}, ptr null, i32 0)
+
+; An object whose IR type is an opaque byte array, as rustc passes and returns
+; one, is read in 8-byte pieces rather than by its first 8 bytes only; so is an
+; element wider than 8 bytes, and a nested struct is described by its elements.
+define void @wide_objects(ptr byval([32 x i8]) %o, ptr byval({ i32, { i8, i64 }, [3 x i32] }) %n, ptr byval({ i128, i8 }) %w) {
+entry:
+  call void (...) @use(ptr %o, ptr %n, ptr %w)
+  ret void
+}
+; CHECK-LABEL: define void @wide_objects(
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 32, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.5, i32 4)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 1, i32 40, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.6, i32 5)
+; CHECK: call void @__sanitizer_cov_trace_args(i32 2, i32 32, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.7, i32 3)
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-no-spill.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-no-spill.ll
new file mode 100644
index 00000000000000..e97929fa4f5e9d
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-no-spill.ll
@@ -0,0 +1,67 @@
+; Neither mode ever creates memory to report a value from, on any target: a
+; value that is not already in memory is reported as the value itself. Nothing
+; escapes to the stack, so no frame is realigned and no base pointer is
+; reserved on the instrumentation's account.
+;
+; This is what makes the modes portable. A function whose inline assembly claims
+; the x86-64 base pointer (rbx) is the case that would otherwise fail to
+; compile - a redzoned, realigned spill slot needs the very register the
+; assembly uses - and it now needs no special handling: it is instrumented
+; exactly like the function below it.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-level=3 -sanitizer-coverage-trace-args -sanitizer-coverage-trace-ret -S | FileCheck %s
+
+; CHECK-NOT: alloca
+
+; An rdtsc-style clobber of rbx.
+define i32 @clobbers_rbx(i32 noundef %x) #0 !dbg !6 {
+entry:
+  call void asm sideeffect "nop", "~{rbx},~{dirflag},~{fpsr},~{flags}"() #0, !dbg !9
+  ret i32 %x, !dbg !9
+}
+; CHECK-LABEL: define i32 @clobbers_rbx(
+; CHECK: %[[A:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 4, i64 %[[A]], ptr null, i32 0)
+; CHECK: %[[R:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i32 4, i64 %[[R]], ptr null, i32 0)
+; CHECK: ret i32 %x
+
+; A cpuid-style "=b" output operand names the same register.
+define i32 @uses_b_constraint(i32 noundef %x) #0 !dbg !10 {
+entry:
+  %0 = call i32 asm "cpuid", "=b,0"(i32 %x) #0, !dbg !11
+  ret i32 %0, !dbg !11
+}
+; CHECK-LABEL: define i32 @uses_b_constraint(
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+; CHECK: call void @__sanitizer_cov_trace_ret(i32 4, i64 %{{[0-9]+}}, ptr null, i32 0)
+
+; The same function without such inline assembly, instrumented identically.
+define i32 @plain(i32 noundef %x) #0 !dbg !12 {
+entry:
+  ret i32 %x, !dbg !13
+}
+; CHECK-LABEL: define i32 @plain(
+; CHECK: %[[PA:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_args(i32 0, i32 4, i64 %[[PA]], ptr null, i32 0)
+; CHECK: %[[PR:[0-9]+]] = zext i32 %x to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i32 4, i64 %[[PR]], ptr null, i32 0)
+
+attributes #0 = { nounwind sanitize_address }
+
+!llvm.dbg.cu = !{!0}
+!llvm.module.flags = !{!1, !2}
+
+!0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !3, isOptimized: true, emissionKind: FullDebug)
+!1 = !{i32 2, !"Dwarf Version", i32 5}
+!2 = !{i32 2, !"Debug Info Version", i32 3}
+!3 = !DIFile(filename: "no-spill.c", directory: "/")
+!4 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+!5 = !DISubroutineType(types: !{!4, !4})
+!6 = distinct !DISubprogram(name: "clobbers_rbx", scope: !3, file: !3, line: 1, type: !5, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !20)
+!9 = !DILocation(line: 1, column: 1, scope: !6)
+!10 = distinct !DISubprogram(name: "uses_b_constraint", scope: !3, file: !3, line: 5, type: !5, scopeLine: 5, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !20)
+!11 = !DILocation(line: 5, column: 1, scope: !10)
+!12 = distinct !DISubprogram(name: "plain", scope: !3, file: !3, line: 9, type: !5, scopeLine: 9, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !20)
+!13 = !DILocation(line: 9, column: 1, scope: !12)
+!20 = !{}
diff --git a/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
new file mode 100644
index 00000000000000..7eb2cd66241045
--- /dev/null
+++ b/llvm/test/Instrumentation/SanitizerCoverage/trace-ret.ll
@@ -0,0 +1,148 @@
+; Return value tracing: one __sanitizer_cov_trace_ret call before every return,
+; decided by the IR alone. A value that fits in a register is reported as the
+; value, a first-class aggregate as its elements, and an indirect (sret) return
+; as the buffer the returned object was written to.
+;
+; opt runs the verifier, so a passing run also proves the emitted IR is
+; well-formed.
+;
+; RUN: opt < %s -passes='module(sancov-module)' -sanitizer-coverage-trace-ret -S | FileCheck %s
+
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+%struct.S = type { i64, i64, i64 }
+
+; CHECK: @__sancov_offsets_ = private unnamed_addr constant [4 x i64] [i64 0, i64 8, i64 8, i64 4]
+; CHECK: @__sancov_offsets_.1 = private unnamed_addr constant [6 x i64] [i64 0, i64 8, i64 8, i64 8, i64 16, i64 8]
+; CHECK: @__sancov_offsets_.2 = private unnamed_addr constant [8 x i64] [i64 0, i64 8, i64 8, i64 8, i64 16, i64 8, i64 24, i64 8]
+; CHECK-NOT: @__sancov_offsets_
+
+define i32 @scalar(i32 noundef %x) {
+entry:
+  ret i32 %x
+}
+; CHECK-LABEL: define i32 @scalar(
+; CHECK: %[[X:[0-9]+]] = zext i32 %x to i64
+; CHECK-NEXT: call void @__sanitizer_cov_trace_ret(i32 4, i64 %[[X]], ptr null, i32 0)
+; CHECK-NEXT: ret i32 %x
+
+; A pointer is reported as the address it holds.
+define ptr @pointer(ptr noundef %p) {
+entry:
+  ret ptr %p
+}
+; CHECK-LABEL: define ptr @pointer(
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK: call void @__sanitizer_cov_trace_ret(i32 8, i64 %[[P]], ptr null, i32 0)
+
+; A pointer the return attributes say is dereferenceable at the return is
+; reported with the bytes it points to, as rustc returns a reference: `val` is
+; the pointer, `size` the 12 bytes read.
+define noundef nonnull align 4 dereferenceable(12) ptr @reference(ptr noundef %p) {
+entry:
+  ret ptr %p
+}
+; CHECK-LABEL: define noundef nonnull align 4 dereferenceable(12) ptr @reference(
+; CHECK: %[[P:[0-9]+]] = ptrtoint ptr %p to i64
+; CHECK-NEXT: call void @__sanitizer_cov_trace_ret(i32 12, i64 %[[P]], ptr @__sancov_offsets_, i32 2)
+; CHECK-NEXT: ret ptr %p
+
+; Every return is reported, each with its own value.
+define i64 @multi(i1 noundef %c, i64 noundef %x) {
+entry:
+  br i1 %c, label %a, label %b
+a:
+  ret i64 %x
+b:
+  ret i64 7
+}
+; CHECK-LABEL: define i64 @multi(
+; CHECK: call void @__sanitizer_cov_trace_ret(i32 8, i64 %x, ptr null, i32 0)
+; CHECK-NEXT: ret i64 %x
+; CHECK: call void @__sanitizer_cov_trace_ret(i32 8, i64 7, ptr null, i32 0)
+; CHECK-NEXT: ret i64 7
+
+; A struct returned in registers is reported as its elements.
+define { i64, double } @aggregate(i64 noundef %a, double noundef %b) {
+entry:
+  %0 = insertvalue { i64, double } poison, i64 %a, 0
+  %1 = insertvalue { i64, double } %0, double %b, 1
+  ret { i64, double } %1
+}
+; CHECK-LABEL: define { i64, double } @aggregate(
+; CHECK: call void @__sanitizer_cov_trace_ret(i32 8,
+; CHECK: call void @__sanitizer_cov_trace_ret(i32 8,
+; CHECK-NEXT: ret { i64, double }
+
+; An indirect return is reported as the buffer the returned object was written
+; to, described by the elements of its IR type.
+define void @indirect(ptr sret(%struct.S) %r, i64 noundef %x) {
+entry:
+  store i64 %x, ptr %r
+  ret void
+}
+; CHECK-LABEL: define void @indirect(
+; CHECK: %[[R:[0-9]+]] = ptrtoint ptr %r to i64
+; CHECK-NEXT: call void @__sanitizer_cov_trace_ret(i32 24, i64 %[[R]], ptr @__sancov_offsets_.1, i32 3)
+; CHECK-NEXT: ret void
+
+; rustc types the sret buffer of a Rust-ABI function as an opaque byte array:
+; it is read in 8-byte pieces, all 32 bytes of it.
+define void @opaque_indirect(ptr sret([32 x i8]) %r, i64 noundef %x) {
+entry:
+  store i64 %x, ptr %r
+  ret void
+}
+; CHECK-LABEL: define void @opaque_indirect(
+; CHECK: call void @__sanitizer_cov_trace_ret(i32 32, i64 %{{[0-9]+}}, ptr @__sancov_offsets_.2, i32 4)
+
+; A void return has nothing to report, and says so with size 0. The call is in
+; tail position, but must not become a tail call: the runtime identifies an
+; observation by its call's return address.
+define void @void() {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @void(
+; CHECK: notail call void @__sanitizer_cov_trace_ret(i32 0, i64 0, ptr null, i32 0)
+; CHECK-NEXT: ret void
+
+; A vector is reported as its bit pattern.
+define <2 x i32> @vector(<2 x i32> noundef %v) {
+entry:
+  ret <2 x i32> %v
+}
+; CHECK-LABEL: define <2 x i32> @vector(
+; CHECK: %[[V:[0-9]+]] = bitcast <2 x i32> %v to i64
+; CHECK-NEXT: notail call void @__sanitizer_cov_trace_ret(i32 8, i64 %[[V]], ptr null, i32 0)
+
+; A musttail call has to stay adjacent to the return that forwards it, so that
+; return is left alone.
+define i64 @forward(i64 noundef %x) {
+entry:
+  %r = musttail call i64 @forward(i64 %x)
+  ret i64 %r
+}
+; CHECK-LABEL: define i64 @forward(
+; CHECK-NOT: call void @__sanitizer_cov_trace_ret
+; CHECK: musttail call i64 @forward
+; CHECK-NEXT: ret i64 %r
+
+; Unwinding leaves the function without a value.
+declare void @may_throw()
+declare i32 @__gxx_personality_v0(...)
+define i32 @unwinds(i32 noundef %x) personality ptr @__gxx_personality_v0 {
+entry:
+  invoke void @may_throw() to label %ok unwind label %lp
+ok:
+  ret i32 %x
+lp:
+  %l = landingpad { ptr, i32 } cleanup
+  resume { ptr, i32 } %l
+}
+; CHECK-LABEL: define i32 @unwinds(
+; CHECK: call void @__sanitizer_cov_trace_ret(i32 4,
+; CHECK-NEXT: ret i32 %x
+; CHECK-NOT: call void @__sanitizer_cov_trace_ret
+; CHECK: resume

>From 1c14b976561cd9e52897c84d653d24a3975248be Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 27 Sep 2026 21:14:06 +0200
Subject: [PATCH 2/4] [Clang][SanitizerCoverage] Add
 -fsanitize-coverage=trace-args,trace-ret

Expose the SanitizerCoverage trace-args and trace-ret instrumentation
(SanitizerCoverageOptions::TraceArgs/TraceRet) to the driver and the
frontend. The implementation is entirely in LLVM; Clang only spells and
forwards the request:

- CodeGenOptions.def: SanitizeCoverageTraceArgs/Ret, and
  hasSanitizeCoverage() accounting for them, so either flag on its own
  runs the pass.
- Options.td: the cc1 flags -fsanitize-coverage-trace-args and
  -fsanitize-coverage-trace-ret.
- SanitizerArgs.cpp: accept trace-args and trace-ret in
  -fsanitize-coverage=, count both as instrumentation types so that they
  satisfy the "requires an instrumentation type" check, and forward the
  cc1 flags. Like the other instrumentation types, either one used
  without a coverage level selects the edge level
  (-fsanitize-coverage-type=3). That level alone emits no block or edge
  callbacks: the pass treats trace-args/ret as requested modes, so they
  do not pull in the trace-pc-guard default, and
  -fsanitize-coverage=trace-args compiles to argument tracing only.
- BackendUtil.cpp: forward both into SanitizerCoverageOptions.

Tests cover the driver accepting the new -fsanitize-coverage= values and
forwarding the cc1 flags, and the CodeGen pipeline emitting each callback
only for the flag that asks for it, with a struct pointer parameter
reported as a pointer value - with and without debug information, which
must not change it.

The byte limit of pointee snapshots stays an LLVM option,
-mllvm -sanitizer-coverage-trace-pointee-bytes=N, as the other
SanitizerCoverage tuning knobs are. SanitizerCoverage.md documents the
flags once the runtime side it refers to is in place.

Depends on "[SanitizerCoverage] Add trace-args and trace-ret
instrumentation".

Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 clang/include/clang/Basic/CodeGenOptions.def  |  2 +
 clang/include/clang/Basic/CodeGenOptions.h    |  3 +-
 clang/include/clang/Options/Options.td        | 10 +++++
 clang/lib/CodeGen/BackendUtil.cpp             |  2 +
 clang/lib/Driver/SanitizerArgs.cpp            | 14 ++++--
 .../sanitizer-coverage-trace-args-ret.c       | 43 +++++++++++++++++++
 clang/test/Driver/fsanitize-coverage.c        | 13 ++++++
 7 files changed, 83 insertions(+), 4 deletions(-)
 create mode 100644 clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c

diff --git a/clang/include/clang/Basic/CodeGenOptions.def b/clang/include/clang/Basic/CodeGenOptions.def
index c8ad0eb3f302e3..277fc5f69841e1 100644
--- a/clang/include/clang/Basic/CodeGenOptions.def
+++ b/clang/include/clang/Basic/CodeGenOptions.def
@@ -330,6 +330,8 @@ CODEGENOPT(SanitizeCoverageStackDepth, 1, 0, Benign) ///< Enable max stack depth
 VALUE_CODEGENOPT(SanitizeCoverageStackDepthCallbackMin , 32, 0, Benign) ///< Enable stack depth tracing callbacks.
 CODEGENOPT(SanitizeCoverageTraceLoads, 1, 0, Benign) ///< Enable tracing of loads.
 CODEGENOPT(SanitizeCoverageTraceStores, 1, 0, Benign) ///< Enable tracing of stores.
+CODEGENOPT(SanitizeCoverageTraceArgs, 1, 0, Benign) ///< Enable tracing of function args.
+CODEGENOPT(SanitizeCoverageTraceRet, 1, 0, Benign) ///< Enable tracing of return values.
 CODEGENOPT(SanitizeBinaryMetadataCovered, 1, 0, Benign) ///< Emit PCs for covered functions.
 CODEGENOPT(SanitizeBinaryMetadataAtomics, 1, 0, Benign) ///< Emit PCs for atomic operations.
 CODEGENOPT(SanitizeBinaryMetadataUAR, 1, 0, Benign) ///< Emit PCs for start of functions
diff --git a/clang/include/clang/Basic/CodeGenOptions.h b/clang/include/clang/Basic/CodeGenOptions.h
index ecd1a63d6de4f3..7809a974e3838c 100644
--- a/clang/include/clang/Basic/CodeGenOptions.h
+++ b/clang/include/clang/Basic/CodeGenOptions.h
@@ -727,7 +727,8 @@ class CodeGenOptions : public CodeGenOptionsBase {
   bool hasSanitizeCoverage() const {
     return SanitizeCoverageType || SanitizeCoverageIndirectCalls ||
            SanitizeCoverageTraceCmp || SanitizeCoverageTraceLoads ||
-           SanitizeCoverageTraceStores || SanitizeCoverageControlFlow;
+           SanitizeCoverageTraceStores || SanitizeCoverageControlFlow ||
+           SanitizeCoverageTraceArgs || SanitizeCoverageTraceRet;
   }
 
   // Check if any one of SanitizeBinaryMetadata* is enabled.
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 38d285b8268e15..0c57a09e06a1b7 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -8416,6 +8416,16 @@ def fsanitize_coverage_trace_stores
       Group<fsan_cov_Group>,
       HelpText<"Enable tracing of stores">,
       MarshallingInfoFlag<CodeGenOpts<"SanitizeCoverageTraceStores">>;
+def fsanitize_coverage_trace_args
+    : Flag<["-"], "fsanitize-coverage-trace-args">,
+      Group<fsan_cov_Group>,
+      HelpText<"Enable dataflow tracing of function arguments">,
+      MarshallingInfoFlag<CodeGenOpts<"SanitizeCoverageTraceArgs">>;
+def fsanitize_coverage_trace_ret
+    : Flag<["-"], "fsanitize-coverage-trace-ret">,
+      Group<fsan_cov_Group>,
+      HelpText<"Enable dataflow tracing of return values">,
+      MarshallingInfoFlag<CodeGenOpts<"SanitizeCoverageTraceRet">>;
 def fexperimental_sanitize_metadata_EQ_covered
     : Flag<["-"], "fexperimental-sanitize-metadata=covered">,
       HelpText<"Emit PCs for code covered with binary analysis sanitizers">,
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index 1ffcf7d7f6b0a7..f212ed641235cb 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -269,6 +269,8 @@ getSancovOptsFromCGOpts(const CodeGenOptions &CGOpts) {
   Opts.StackDepthCallbackMin = CGOpts.SanitizeCoverageStackDepthCallbackMin;
   Opts.TraceLoads = CGOpts.SanitizeCoverageTraceLoads;
   Opts.TraceStores = CGOpts.SanitizeCoverageTraceStores;
+  Opts.TraceArgs = CGOpts.SanitizeCoverageTraceArgs;
+  Opts.TraceRet = CGOpts.SanitizeCoverageTraceRet;
   Opts.CollectControlFlow = CGOpts.SanitizeCoverageControlFlow;
   return Opts;
 }
diff --git a/clang/lib/Driver/SanitizerArgs.cpp b/clang/lib/Driver/SanitizerArgs.cpp
index a1bfa90e5dceb2..4b189a0f143292 100644
--- a/clang/lib/Driver/SanitizerArgs.cpp
+++ b/clang/lib/Driver/SanitizerArgs.cpp
@@ -109,6 +109,8 @@ enum CoverageFeature {
   CoverageTraceStores = 1 << 17,
   CoverageControlFlow = 1 << 18,
   CoverageTracePCEntryExit = 1 << 19,
+  CoverageTraceArgs = 1 << 20,
+  CoverageTraceRet = 1 << 21,
 };
 
 enum BinaryMetadataFeature {
@@ -1100,6 +1102,7 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
   int InstrumentationTypes = CoverageTracePC | CoverageTracePCEntryExit |
                              CoverageTracePCGuard | CoverageInline8bitCounters |
                              CoverageTraceLoads | CoverageTraceStores |
+                             CoverageTraceArgs | CoverageTraceRet |
                              CoverageInlineBoolFlag | CoverageControlFlow;
   if ((CoverageFeatures & InsertionPointTypes) &&
       !(CoverageFeatures & InstrumentationTypes) && DiagnoseErrors) {
@@ -1111,9 +1114,10 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC,
 
   // trace-pc w/o func/bb/edge implies edge.
   if (!(CoverageFeatures & InsertionPointTypes)) {
-    if (CoverageFeatures & (CoverageTracePC | CoverageTracePCEntryExit |
-                            CoverageTracePCGuard | CoverageInline8bitCounters |
-                            CoverageInlineBoolFlag | CoverageControlFlow))
+    if (CoverageFeatures &
+        (CoverageTracePC | CoverageTracePCEntryExit | CoverageTracePCGuard |
+         CoverageInline8bitCounters | CoverageInlineBoolFlag |
+         CoverageControlFlow | CoverageTraceArgs | CoverageTraceRet))
       CoverageFeatures |= CoverageEdge;
 
     if (CoverageFeatures & CoverageStackDepth)
@@ -1477,6 +1481,8 @@ void SanitizerArgs::addArgs(const ToolChain &TC, const llvm::opt::ArgList &Args,
       std::make_pair(CoverageStackDepth, "-fsanitize-coverage-stack-depth"),
       std::make_pair(CoverageTraceLoads, "-fsanitize-coverage-trace-loads"),
       std::make_pair(CoverageTraceStores, "-fsanitize-coverage-trace-stores"),
+      std::make_pair(CoverageTraceArgs, "-fsanitize-coverage-trace-args"),
+      std::make_pair(CoverageTraceRet, "-fsanitize-coverage-trace-ret"),
       std::make_pair(CoverageControlFlow, "-fsanitize-coverage-control-flow")};
   for (auto F : CoverageFlags) {
     if (CoverageFeatures & F.first)
@@ -1867,6 +1873,8 @@ int parseCoverageFeatures(const Driver &D, const llvm::opt::Arg *A,
                 .Case("stack-depth", CoverageStackDepth)
                 .Case("trace-loads", CoverageTraceLoads)
                 .Case("trace-stores", CoverageTraceStores)
+                .Case("trace-args", CoverageTraceArgs)
+                .Case("trace-ret", CoverageTraceRet)
                 .Case("control-flow", CoverageControlFlow)
                 .Default(0);
     if (F == 0 && DiagnoseErrors)
diff --git a/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c b/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c
new file mode 100644
index 00000000000000..223db60b6ef0f5
--- /dev/null
+++ b/clang/test/CodeGen/sanitizer-coverage-trace-args-ret.c
@@ -0,0 +1,43 @@
+// Check that -fsanitize-coverage-trace-args and -fsanitize-coverage-trace-ret
+// reach the SanitizerCoverage pass and emit their callbacks, and that debug
+// information does not change what they emit.
+//
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 -debug-info-kind=limited \
+// RUN:     -fsanitize-coverage-type=3 -fsanitize-coverage-trace-args %s -o - \
+// RUN:   | FileCheck %s --check-prefix=ARGS
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 \
+// RUN:     -fsanitize-coverage-type=3 -fsanitize-coverage-trace-args %s -o - \
+// RUN:   | FileCheck %s --check-prefix=ARGS
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 -debug-info-kind=limited \
+// RUN:     -fsanitize-coverage-type=3 -fsanitize-coverage-trace-ret %s -o - \
+// RUN:   | FileCheck %s --check-prefix=RET
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm -O1 -debug-info-kind=limited \
+// RUN:     -fsanitize-coverage-type=3 %s -o - | FileCheck %s --check-prefix=NONE
+
+struct Foo {
+  int a;
+  long b;
+};
+
+void takes_struct_ptr(struct Foo *f) {}
+
+int returns_scalar(int x) { return x + 1; }
+
+// A pointer is an IR value like any other: it is reported as the address it
+// holds, and the struct it points at is not read. Whether debug information
+// describes that struct makes no difference. Nothing is spilled.
+// ARGS-NOT: @__sancov_offsets_
+// ARGS-LABEL: define {{.*}} @takes_struct_ptr(
+// ARGS-NOT: alloca
+// ARGS: %[[ADDR:.*]] = ptrtoint ptr %f to i64
+// ARGS: call void @__sanitizer_cov_trace_args(i32 0, i32 8, i64 %[[ADDR]], ptr null, i32 0)
+// ARGS-LABEL: define {{.*}} @returns_scalar(
+// ARGS: call void @__sanitizer_cov_trace_args(i32 0, i32 4,
+// ARGS-NOT: call void @__sanitizer_cov_trace_ret(
+
+// RET-LABEL: define {{.*}} @returns_scalar(
+// RET: call void @__sanitizer_cov_trace_ret(i32 4,
+// RET-NOT: call void @__sanitizer_cov_trace_args(
+
+// NONE-NOT: call void @__sanitizer_cov_trace_args(
+// NONE-NOT: call void @__sanitizer_cov_trace_ret(
diff --git a/clang/test/Driver/fsanitize-coverage.c b/clang/test/Driver/fsanitize-coverage.c
index 21e2c16bfb1b74..9a0c74bc3dda92 100644
--- a/clang/test/Driver/fsanitize-coverage.c
+++ b/clang/test/Driver/fsanitize-coverage.c
@@ -170,3 +170,16 @@
 // CHECK-NO-SHADOWCALLSTACK-NOT: unknown argument
 // CHECK-NO-SHADOWCALLSTACK-NOT: -fsanitize=shadow-call-stack
 // CHECK-NO-SHADOWCALLSTACK: -fsanitize-coverage-trace-pc-guard
+
+// RUN: %clang --target=x86_64-linux-gnu -fsanitize-coverage=trace-args %s -### 2>&1 | FileCheck %s --check-prefix=CHECK-TRACE-ARGS
+// CHECK-TRACE-ARGS: -fsanitize-coverage-type=3
+// CHECK-TRACE-ARGS: -fsanitize-coverage-trace-args
+
+// RUN: %clang --target=x86_64-linux-gnu -fsanitize-coverage=trace-ret %s -### 2>&1 | FileCheck %s --check-prefix=CHECK-TRACE-RET
+// CHECK-TRACE-RET: -fsanitize-coverage-type=3
+// CHECK-TRACE-RET: -fsanitize-coverage-trace-ret
+
+// RUN: %clang --target=x86_64-linux-gnu -fsanitize-coverage=edge,trace-args,trace-ret %s -### 2>&1 | FileCheck %s --check-prefix=CHECK-TRACE-ARGS-RET
+// CHECK-TRACE-ARGS-RET: -fsanitize-coverage-type=3
+// CHECK-TRACE-ARGS-RET: -fsanitize-coverage-trace-args
+// CHECK-TRACE-ARGS-RET: -fsanitize-coverage-trace-ret

>From 6f7bdeafe136847390b9f70a0a292878b16c6a62 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Sun, 27 Sep 2026 21:18:19 +0200
Subject: [PATCH 3/4] [compiler-rt][SanitizerCoverage] Add weak
 trace-args/trace-ret callbacks

Make -fsanitize-coverage=trace-args,trace-ret usable from userspace
programs - C, C++, and Rust code compiled by an LLVM carrying the pass -
built against a sanitizer runtime:

- sanitizer_coverage_libcdep_new.cpp: weak, empty default definitions of
  __sanitizer_cov_trace_args and __sanitizer_cov_trace_ret, so that an
  instrumented program links without a runtime consuming the values.
  libFuzzer, in a later patch, or the user provides a strong definition,
  which must tolerate a size of zero: the instrumentation passes it for a
  value it could not report.

- sanitizer_interface_internal.h: declarations describing the two
  contracts. arg_idx is the IR argument position, not the source
  parameter number. `val` is the reported value itself - a pointer as
  the address it holds - or, when num_fields is non-zero, the address of
  `size` bytes to read the num_fields {byte offset, byte size} pairs in
  `offsets`, none wider than 8 bytes, out of: an object the ABI passed or
  returned in memory, by its IR-type elements, or the bytes behind a
  pointer the IR says are dereferenceable, which may be null for
  dereferenceable_or_null.
  Neither callback is passed the function it reports for: a runtime
  that needs it takes its own return address, less one so that the
  address lies within the call, which identifies the observation.

- sanitizer_coverage_interface.inc: the weak-function interface entries.

- Test: an end-to-end sanitizer_common test in which a user definition
  overrides the weak defaults and checks the reported argument and
  return values, at -O2 with debug information and at -O0 without: the
  observations are IR values, the same either way.

Assisted-by: Kiro CLI (Claude Opus 5)
Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 .../sanitizer_coverage_interface.inc          |  2 +
 .../sanitizer_coverage_libcdep_new.cpp        |  7 +++
 .../sanitizer_interface_internal.h            | 28 ++++++++++
 .../sanitizer_coverage_trace_args.cpp         | 52 +++++++++++++++++++
 4 files changed, 89 insertions(+)
 create mode 100644 compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp

diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc
index 9d36a40270d5a2..ff7d971bfe51e3 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_interface.inc
@@ -38,6 +38,8 @@ INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store4)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store8)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_store16)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_switch)
+INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_args)
+INTERFACE_WEAK_FUNCTION(__sanitizer_cov_trace_ret)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_8bit_counters_init)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_bool_flag_init)
 INTERFACE_WEAK_FUNCTION(__sanitizer_cov_pcs_init)
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp
index 2f5b6f68afa413..963585c06dbd09 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_coverage_libcdep_new.cpp
@@ -257,6 +257,13 @@ SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_const_cmp2, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_const_cmp4, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_const_cmp8, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_switch, void) {}
+// Argument and return value tracing (trace-args / trace-ret). Weak, empty
+// defaults so that a program built with -fsanitize-coverage=trace-args and
+// trace-ret links without a runtime consuming the values; libFuzzer, or the
+// user, provides a strong definition. Such a definition must tolerate a size of
+// zero, which the instrumentation passes for a value it cannot report.
+SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_args, void) {}
+SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_ret, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_div4, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_div8, void) {}
 SANITIZER_INTERFACE_WEAK_DEF(void, __sanitizer_cov_trace_gep, void) {}
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h b/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h
index c424ab1cecf94f..de091bfc6e45fc 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_interface_internal.h
@@ -115,6 +115,34 @@ SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
 __sanitizer_cov_trace_const_cmp8();
 SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
 __sanitizer_cov_trace_switch();
+// Argument and return value tracing: observe the IR arguments of a function on
+// entry and its return values. What is observed is decided by the LLVM IR
+// alone; debug information never changes it.
+//   __sanitizer_cov_trace_args(u32 arg_idx, u32 size, u64 val,
+//                              u64 *offsets, u32 num_fields)
+//   __sanitizer_cov_trace_ret (u32 size, u64 val,
+//                              u64 *offsets, u32 num_fields)
+// `arg_idx` is the position of the argument in the IR parameter list, not the
+// source-language parameter number: one source parameter may be several IR
+// arguments, a hidden struct-return pointer takes a position, and the elements
+// of a first-class aggregate are reported one call each under the same index.
+// `val` is the reported value itself - a pointer as the address it holds - or,
+// when num_fields is non-zero, the address of `size` bytes to read the
+// num_fields {byte offset, byte size} pairs in `offsets` out of, none wider
+// than 8 bytes: an object the ABI passed or returned in memory, described by
+// its IR-type elements, or the bytes behind a pointer the IR says are
+// dereferenceable, as many as that says and
+// -sanitizer-coverage-trace-pointee-bytes allows. Such a pointer may be null
+// if the IR said dereferenceable_or_null. A size of zero means nothing was
+// reported: a void return, a value with no fixed-size bit pattern, or an
+// object the function does not use, whose address may be poison.
+// Neither callback is passed the function it speaks for. A runtime that needs
+// it takes its own return address, less one so that the address lies within the
+// call, which identifies the observation.
+SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
+__sanitizer_cov_trace_args();
+SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
+__sanitizer_cov_trace_ret();
 SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
 __sanitizer_cov_trace_div4();
 SANITIZER_INTERFACE_ATTRIBUTE SANITIZER_WEAK_ATTRIBUTE void
diff --git a/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp b/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp
new file mode 100644
index 00000000000000..46cac84d50cfd0
--- /dev/null
+++ b/compiler-rt/test/sanitizer_common/TestCases/sanitizer_coverage_trace_args.cpp
@@ -0,0 +1,52 @@
+// Check that -fsanitize-coverage=trace-args,trace-ret calls the two callbacks
+// with the observed argument and return values, and that a user definition
+// overrides compiler-rt's weak defaults.
+//
+// The arguments are the IR arguments, reported on entry, so the values are
+// right at any optimization level, with or without debug information.
+//
+// REQUIRES: has_sancovcc
+// UNSUPPORTED: i386-darwin
+// RUN: %clangxx -O2 -g -fsanitize-coverage=trace-pc-guard,trace-args,trace-ret %s -o %t
+// RUN: %run %t 2>&1 | FileCheck %s
+// RUN: %clangxx -O0 -fsanitize-coverage=trace-pc-guard,trace-args,trace-ret %s -o %t.O0
+// RUN: %run %t.O0 2>&1 | FileCheck %s
+
+#include <cstdint>
+#include <cstdio>
+
+extern "C" {
+// Stubs so the program links without a sanitizer runtime; these are weak in
+// compiler-rt, so these definitions win.
+void __sanitizer_cov_trace_pc_guard(uint32_t *) {}
+void __sanitizer_cov_trace_pc_guard_init(uint32_t *, uint32_t *) {}
+
+// The consumers under test. A size of zero means the instrumentation had
+// nothing to report, and a non-zero num_fields would mean `val` is an address
+// rather than a value. Neither callback is passed the function it speaks for;
+// a consumer that wants it takes its own return address.
+void __sanitizer_cov_trace_args(uint32_t arg_idx, uint32_t size, uint64_t val,
+                                uint64_t *offsets, uint32_t num_fields) {
+  if (size == sizeof(int) && !num_fields)
+    fprintf(stderr, "ARG idx=%u val=%d\n", arg_idx, (int)val);
+}
+
+void __sanitizer_cov_trace_ret(uint32_t size, uint64_t val, uint64_t *offsets,
+                               uint32_t num_fields) {
+  if (size == sizeof(int) && !num_fields)
+    fprintf(stderr, "RET val=%d\n", (int)val);
+}
+}
+
+__attribute__((noinline)) int add(int a, int b) { return a + b; }
+
+int main() {
+  volatile int r = add(41, 2);
+  fprintf(stderr, "r=%d\n", (int)r);
+  return 0;
+}
+
+// CHECK-DAG: ARG idx=0 val=41
+// CHECK-DAG: ARG idx=1 val=2
+// CHECK: RET val=43
+// CHECK: r=43

>From 56a34948d844cba7399935effad0af823f85c2b2 Mon Sep 17 00:00:00 2001
From: Yunseong Kim <yunseong.kim at est.tech>
Date: Thu, 15 Oct 2026 12:46:43 +0200
Subject: [PATCH 4/4] [Clang][SanitizerCoverage] Document trace-args and
 trace-ret

Document -fsanitize-coverage=trace-args,trace-ret and their consumers, as
implemented by the patches below this one, in a "Tracing function
arguments and return values" section of SanitizerCoverage.md:

- The design rule, stated up front: debug information may enrich the
  interpretation of an observation, but must never determine which
  instrumentation is emitted; a build with -g emits the same callbacks,
  operands and machine code as one without it.

- The two callback signatures, and what num_fields says about `val`: the
  value itself, or the address of an object the ABI placed in memory or
  of a pointee snapshot, with the table of 8-byte-or-smaller pieces to
  read, rustc's opaque [N x i8] objects included. A size of zero means
  there was nothing to report; the compiler never creates memory to
  report a value from.

- "IR arguments, not source parameters": arg_idx is the IR argument
  position, and where it differs from the source parameter number.

- "Pointers: values, layouts and snapshots": the pointer value, the
  pointee's source-level type and member layout (from debug information,
  which never causes a read), an explicit pointee snapshot, and complete,
  partial and unread members. Only an explicit IR dereferenceability
  contract - dereferenceable(N), or dereferenceable_or_null(N) with a
  null check - causes the runtime to read behind a pointer, at most
  -sanitizer-coverage-trace-pointee-bytes (64) bytes, so a member the
  limit cuts is partial. Which front-end constructs carry the contract:
  C++ references and `this`, C `T p[static N]`, Rust &T and &mut T, but
  not an ordinary C `T *` or a Rust raw pointer, which are pointer values
  only. The measured share of pointer arguments with any pointee byte
  recorded on a Linux kernel build, stated as such: it is not a share of
  arguments or structs reconstructed. Recording the values a function
  loads and stores through a pointer without a contract is mentioned as
  possible future work, not as a feature.

- "Consumers" and "Supported scope": compiler-rt's weak defaults and the
  Linux kernel's KCOV dataflow subsystem; C, C++ and Rust on ELF x86-64
  and AArch64, with or without -g, at any optimization level.

Assisted-by: Kiro CLI (Claude Opus 5.5)
Signed-off-by: Yunseong Kim <yunseong.kim at est.tech>
---
 clang/docs/SanitizerCoverage.md | 198 ++++++++++++++++++++++++++++++++
 1 file changed, 198 insertions(+)

diff --git a/clang/docs/SanitizerCoverage.md b/clang/docs/SanitizerCoverage.md
index ae074d13f5322a..6aef9c2c49ff9d 100644
--- a/clang/docs/SanitizerCoverage.md
+++ b/clang/docs/SanitizerCoverage.md
@@ -330,6 +330,202 @@ void __sanitizer_cov_store8(uint64_t *addr);
 void __sanitizer_cov_store16(__int128 *addr);
 ```
 
+## Tracing function arguments and return values
+
+With `-fsanitize-coverage=trace-args` the compiler will insert a callback for
+each argument of every instrumented function, on entry, and with
+`-fsanitize-coverage=trace-ret` a callback before every return. Neither flag
+needs another coverage mode: used alone, they emit no block or edge callbacks.
+
+The design rule for both flags is:
+
+> Debug information may enrich the interpretation of an observation, but must
+> never determine which instrumentation is emitted.
+
+What is observed is decided by the LLVM IR of the instrumented function alone.
+A build with `-g` emits exactly the same callbacks, with the same operands, and
+the same machine code, as a build without it; debug information gives the
+calls a source location and nothing else.
+
+```c++
+// Called once per observed IR argument, or element of one, on entry.
+// arg_idx:    zero-based position of the argument in the IR parameter list
+// size:       number of bytes reported, 0 if there was nothing to report
+// val:        the value itself, or the address of the bytes to read
+// offsets:    table of `num_fields` [byte offset, byte size] pairs, none
+//             wider than 8 bytes, of what to read at `val`, or null
+// num_fields: number of pairs in `offsets`, 0 if `val` is a value
+void __sanitizer_cov_trace_args(uint32_t arg_idx, uint32_t size, uint64_t val,
+                                uint64_t *offsets, uint32_t num_fields);
+
+// Called before each return; once per element of a first-class aggregate
+// return value.
+void __sanitizer_cov_trace_ret(uint32_t size, uint64_t val, uint64_t *offsets,
+                               uint32_t num_fields);
+```
+
+Neither callback is told which function it speaks for. A runtime that needs to
+know takes its own return address, less one so that the address lies within the
+call rather than on the instruction after it, and resolves it like any other
+code address. This is the same convention as the `trace-cmp` callbacks. Every
+observation has a call of its own, which is never a tail call, so that address
+identifies it.
+
+`num_fields` says what `val` holds:
+
+* `num_fields == 0`: `val` **is** the value, its low `size` bytes,
+  zero-extended. Integers, pointers, floating-point values and vectors are
+  reported this way: a pointer as the address it holds, a floating-point value
+  or a vector as its bit pattern, and a value wider than 64 bits by its low 64
+  bits.
+
+* `num_fields != 0`: `val` is the **address** of `size` bytes that may be read
+  at the call, and `offsets` says which: `num_fields` pieces, none wider than
+  8 bytes, so a consumer reading each into a 64-bit word reads all of them.
+  That is one of
+
+  * an object the ABI placed in memory, which the IR itself marks as one: an
+    argument with a `byval`, `byref`, `inalloca` or `preallocated` attribute,
+    or the `sret` buffer of an indirect return. `offsets` describes the
+    elements of its IR type, nested structs included; anything wider than
+    8 bytes - an array, or the opaque `[N x i8]` rustc types every such object
+    as - in 8-byte pieces.
+  * a *pointee snapshot*: a pointer argument the IR says is
+    `dereferenceable(N)` or `dereferenceable_or_null(N)` on entry, or a
+    pointer return value the function's return attributes say is, at the
+    return. `val` is the pointer, and `size` is the first
+    min(N, `-sanitizer-coverage-trace-pointee-bytes`) bytes it points to
+    (64 by default; 0 reports every pointer by value), covered in 8-byte
+    pieces. The IR attribute alone bounds the read - the IR has no pointee
+    type - so the bytes are raw; a consumer cuts them into members with the
+    binary's debug information. A
+    `dereferenceable_or_null` pointer may be null, which the runtime checks.
+
+  The callback does not say which of the two it is. Every other
+  pointer is reported by value, and what it points to is never read: the
+  instrumentation reads memory only as far as the IR proves it may.
+
+`size == 0` means there was nothing to report: a `void` return, a value with no
+fixed-size bit pattern, such as a scalable vector, or an object the function
+does not use. Callers may pass anything for an argument the function does not
+use - dead argument elimination passes poison in the callers it can see - so
+such an object is not handed to the runtime to read; an unused value is still
+reported. A value that may be poison is frozen first, so `val` always holds
+bits.
+
+The compiler never creates memory to report a value from, so no stack slot
+escapes and no frame is realigned on the instrumentation's account.
+
+### IR arguments, not source parameters
+
+`arg_idx` is the position of the argument in the IR function's parameter list.
+That is the ABI's view of the signature, and it is not guaranteed to equal the
+source-language parameter number:
+
+* One source parameter may be several IR arguments. On x86-64,
+  `void f(struct mix { double d; long l; } m, int z)` is
+  `f(double %0, i64 %1, i32 %2)`: `m` is IR arguments 0 and 1, and `z` is IR
+  argument 2.
+* One IR argument may be several observations. On AArch64 the same function is
+  `f([2 x i64] %0, i32 %1)`: the first-class aggregate `%0` is reported as two
+  callbacks, both with `arg_idx == 0`, and `z` is IR argument 1.
+* A hidden struct-return (`sret`) pointer and a Swift error slot are not
+  observed, but keep their positions: in `struct big f(long x)`, lowered to
+  `f(ptr sret %0, i64 %1)`, `x` is reported with `arg_idx == 1`.
+* A C++ `this` is an ordinary pointer argument, reported as its value - with
+  the `sizeof` bytes of the object it points to, which the IR says are
+  dereferenceable, as for a C++ reference.
+
+A struct the ABI passes by address without an IR attribute that says so - a
+large struct on AArch64, a non-trivially-copyable C++ object - is a pointer
+argument, and is reported as the pointer, with what it points to if the IR
+says it is dereferenceable.
+
+Which pointers the IR says that of depends on the front end. Clang gives C++
+references and `this` `dereferenceable(sizeof(T))`, as it does a struct the ABI
+passes by address (on AArch64, any over 16 bytes), and a C array parameter of
+static size, `const struct s p[static 1]`, `dereferenceable` that far; a plain
+C pointer has no such attribute, and is reported by value. rustc gives `&T` and
+`&mut T` arguments `dereferenceable(size_of::<T>())`, `Option<&T>`
+`dereferenceable_or_null`, and the vtable of a `&dyn Trait` its size; not a
+`Box<T>`, a raw pointer, the data of a slice or trait object, a `&` to a
+`!Freeze` type (`Cell`, `UnsafeCell`) or a `&mut` to a `!Unpin` one, nor any
+return value.
+
+### Pointers: values, layouts and snapshots
+
+Four things about a pointer argument or return value are easily confused, and
+only the first and the third are recorded:
+
+* The **pointer value**, the address it holds: always reported, as `val`.
+* The **source-level pointee type and member layout**: what the debug
+  information says it points to. A consumer can name and lay out the members
+  from it, but debug information never causes anything to be read.
+* An explicit **pointee snapshot**: bytes the runtime reads behind the pointer
+  at the trace point. Only an explicit IR dereferenceability contract causes
+  the instrumentation runtime to read behind a pointer:
+  `dereferenceable(N)`, or `dereferenceable_or_null(N)` with a null check, and
+  then the first min(N, `-sanitizer-coverage-trace-pointee-bytes`) bytes. The
+  attribute gives a bounded readable range, not a type: which members those
+  bytes are is the decoder's business, with the debug information.
+* **Complete and partial values**: a member wholly within the bytes read is a
+  complete value; one the byte limit cuts is partial, and one beyond it was
+  not read.
+
+What carries the contract is up to the front end (see above). An ordinary C
+`T *` argument normally does not, and is a pointer value only; a Rust `&T` or
+`&mut T` usually does; a Rust raw pointer, `*const T` or `*mut T`, does not,
+nor does a C pointer unless declared `T p[static N]`. Nothing is inferred to
+stand in for a missing contract - not from later accesses, the size of an
+object, the callers, the debug information or what must execute - and a
+pointer stored inside a snapshot is a value too: what it points to is not
+read. The instrumentation therefore does not reconstruct every pointer
+argument, nor complete structs behind pointers. As a measure of how far it
+goes, on an x86-64 Linux kernel build the pointee of 0 of 23,810 C pointer
+arguments has a byte recorded, and on arm64 21 of 22,606 (structs AArch64
+passes by reference); of the 756 pointer arguments of the kernel's Rust
+objects, 318 (42%) do. These are the shares of pointer arguments with at
+least one recorded pointee byte, not of arguments, structs or return values
+reconstructed in full.
+
+Recording, for a pointer without a contract, the values the function itself
+loads and stores through it, when it does, is possible future work, and not
+part of this implementation.
+
+Mapping an observation to a source parameter, or to a struct field, is the
+job of a consumer that has the binary's debug information; the
+instrumentation does not attempt it. An inlined callee is not an IR function
+of its own any more, so its parameters are not observed: arguments are those of
+the IR functions that remain after the inliner ran. `-fno-inline` makes more of
+them visible, at the usual cost.
+
+Argument values are read on entry, before the `-O0` prologue stores the
+incoming arguments to their stack slots, so they are correct at any
+optimization level; return values likewise.
+
+### Consumers
+
+`compiler-rt` defines both callbacks weakly and empty in `sanitizer_common`, so
+a program compiled with `trace-args`/`trace-ret` links without a runtime that
+consumes the values; a strong definition overrides the default.
+
+The Linux kernel consumes the same callbacks through the KCOV dataflow
+subsystem rather than through compiler-rt.
+
+### Supported scope
+
+The two flags are validated for:
+
+* C and C++ compiled by Clang, and Rust compiled by a `rustc` whose LLVM
+  carries the pass.
+* ELF targets on x86-64 and AArch64, both in userspace (a custom runtime) and
+  in the Linux kernel.
+* Code built with or without `-g`, at any optimization level.
+
+Any other frontend or target gets valid instrumentation: the pass works on
+LLVM IR and target-independent IR attributes only, and never emits IR the
+verifier rejects - a Swift `swifterror` slot, for one, is not reported.
+
 ## Tracing control flow
 
 With `-fsanitize-coverage=control-flow` the compiler will create a table to collect
@@ -566,8 +762,10 @@ Action (required):
   -not-covered-functions Print all not covered funcions.
   -print-coverage-pcs    Print coverage instrumentation points addresses.
   -print-coverage-stats  Print coverage statistics.
+  -print-dataflow-pcs    Print trace-args/trace-ret instrumentation points addresses.
   -print                 Print coverage addresses
   -symbolize             Produces a symbolized JSON report from binary report.
+  -symbolize-dataflow    Produces a symbolized JSON report of trace-args/trace-ret instrumentation points.
   -union                 Compute union of multiple sancov files and write to the new output sancov file
 
 Generic Options:



More information about the cfe-commits mailing list