[llvm] [WIP][docs] Strongly typed DIExpression design doc (PR #208541)

Scott Linder via llvm-commits llvm-commits at lists.llvm.org
Fri Aug 7 08:10:50 PDT 2026


https://github.com/slinder1 updated https://github.com/llvm/llvm-project/pull/208541

>From f37cab04af7130c90db0922486db528b5ea62b98 Mon Sep 17 00:00:00 2001
From: Scott Linder <Scott.Linder at amd.com>
Date: Tue, 23 Jun 2026 20:30:07 +0000
Subject: [PATCH 1/3] [WIP][docs] Strongly typed DIExpression design doc

Change-Id: If3fdbad08a511ccd65e36c66b722e249e82e3a60
---
 .../StronglyTypedDIExpressionDebugInfo.md     | 239 ++++++++++++++++++
 llvm/docs/UserGuides.md                       |   5 +
 2 files changed, 244 insertions(+)
 create mode 100644 llvm/docs/StronglyTypedDIExpressionDebugInfo.md

diff --git a/llvm/docs/StronglyTypedDIExpressionDebugInfo.md b/llvm/docs/StronglyTypedDIExpressionDebugInfo.md
new file mode 100644
index 0000000000000..dcea26a6a768d
--- /dev/null
+++ b/llvm/docs/StronglyTypedDIExpressionDebugInfo.md
@@ -0,0 +1,239 @@
+# Strongly typed `DIExpression` API
+
+```{contents}
+:local:
+```
+
+## Introduction
+
+The current [`DIExpression`](#diexpression) representation has a number of nice
+properties:
+
+* The core C++ API is just `SmallVector<uint64_t>` and a `get` function.
+  * Code can inspect and edit the representation with no hard dependencies.
+  * The cost to `#include` the support code for this is light on templates and
+    other things which make compilation slow.
+  * The representation is pretty compact, and plays nice with the cache and
+    branch predictor.
+
+It also has some downsides:
+
+* The core C++ API is just `SmallVector<uint64_t>` and a `get` function.
+  * There is no boundary at which to separate concerns, and no easy path to
+    improving the representation without affecting users.
+  * There are places in the codebase which muck about with the internals
+    directly.
+* In response to the relaxed API there are myriad attempts to systematize
+  certain aspects, but these do not cohere well, since they have grown up
+  organically and often are fit to a specific use-case.
+  * These are spread across:
+    * `static` member functions,
+    * non-`static` member function,
+    * free functions across the codebase.
+  * Among the `static` and free function cases there are also two flavors:
+    * functions which implicitly `get`, and so accept/return `unique`d `const
+      DIExpression *`,
+    * functions which accept a `SmallVectorImpl<uint64_t>&`.
+
+The goal of the design in this document is to maintain as many of the positive
+aspects as possible, while codifying the best parts of the code that has grown
+up around it, and packaging it all in a strongly typed shell.
+
+## Design
+
+The core principle is to leave the current representation alone, with the
+`unique`d `DIExpression *` in `LLVMContext` not changing.
+
+On top of that, three new types are added that nearly all code will be moved
+over to using: `DIOp::Op`, `DIExprRef`, and `DIExprBuf`.
+
+By way of analogy:
+
+* `DIExpression *` is to `const char *`, as
+* `DIOp::Op` is to `char`, as
+* `DIExprRef` is to `std::string_view`/`llvm::StringRef`, as
+* `DIExprBuf` is to `std::string`/`llvm::SmallString`.
+
+```{note}
+The analogy isn't airtight. The `DIExpression *` doesn't actually point to a
+contiguous view of `DIOp::Op`s. In the new hierarchy it acts more as an opaque
+handle to a `unique`d expression.
+
+There is also no equivalent to `std::char_traits`.
+```
+
+### `DIOp::Op`
+
+The `namespace DIOp` contains `Op`, a compact (2-`qword` on `x86_64`) value
+type, modeled around `std::variant` but not actually deriving from it.
+`DIOp::Op` is passed by value.
+
+The alternative types of `Op` have `PascalCase`-ified names corresponding to
+the `DW_OP_*` names currently in use, and all live in `namespace DIOp`, next to
+`Op`. For example:
+
+```{list-table}
+:header-rows: 1
+* - Old
+  - New
+* - `{DW_OP_lit3}`
+  - `DIOp::Lit3()`
+* - `{DW_OP_breg4, 21}`
+  - `DIOp::BReg4(21)`
+* - `{DW_OP_LLVM_fragment, 24, 8}`
+  - `DIOp::LLVMFragment(24, 8)`
+* - `{DW_OP_addr, 42, DW_OP_plus_uconst, 8, DW_OP_deref}`
+  - `{DIOp::Addr(42), DIOp::PlusUConst(8), DIOp::Deref()}`
+```
+
+The new type is not `POD`, but is standard layout and trivially destructible.
+This enables a couple optimizations:
+
+* The tag can be part of the "Common Initial Sequence", reducing the size of
+  the `Op`. This is part of the rationale for a new type rather than a
+  `std::variant`.
+* Operations over containers of `Op` can e.g. use `memcpy` and avoid having to
+  call destructors in some cases.
+
+### `DIExprRef`
+
+Compact (2-`qword` on `x86_64`) non-owning view type. `DIExprRef` is passed by
+value.
+
+Generally constructed from an existing `DIExpression *`, and so has the
+same lifetime as the associated `LLVMContext`.
+
+Always verified at construction (either by a cached property or by iterating
+over the underlying `uint64_t[]`) to cover a syntactically valid expression.
+For example, the `DIExpressions::getRef` might return
+`std::optional<DIExprRef>`.
+
+The expected general shape of this type is:
+
+```cpp
+class DIExprRef {
+  iterator_range<DIOp::FromUIntIterator> Ops;
+
+public:
+  // no public constructor
+
+  bool isValid() const;
+  bool isSingleLocationExpression() const;
+  // more query methods ...
+};
+```
+
+### `DIExprBuf`
+
+Larger, owning buffer type. `DIExprBuf` is passed by value (when moved) or by
+non-`const` reference (when shared).
+
+Double buffered internally, to avoid extra allocations for chained mutations.
+Can also be reused by e.g. a pass which needs to update many expressions, to
+further reduce allocations.
+
+Since mutations on this type can be chained without `unique`ing the
+intermediate results, there is some potential improvement to RSS, although
+the overall contribution of `DIExpression` to the memory used during
+compilation isn't so high that this is particularly significant.
+
+The expected general shape of this type is:
+
+```cpp
+class DIExprBuf {
+  LLVMContext *Ctx = nullptr;
+  SmallVector<uint64_t, 0> Elements;
+  // Nearly all operations require double-buffering, so we bake it in.
+  // This allows us to re-use a small number of allocations for the
+  // processing of many expressions, even where each expression may require
+  // multiple operations.
+  //
+  // Each method has as an implicit post-condition that the backing buffer
+  // NewElements is empty, and so on entry it can be used without being cleared.
+  SmallVector<uint64_t, 0> NewElements;
+
+public:
+  DIExprBuf() = default;
+  explicit DIExprBuf(LLVMContext *Ctx);
+  explicit DIExprBuf(const DIExpression *From);
+  explicit DIExprBuf(DIExprRef From);
+
+  DIExprRef asRef() const;
+
+  DIExprBuf &clear();
+  DIExprBuf &convertToUndefExpression();
+  DIExprBuf &prepend(uint8_t Flags, int64_t Offset = 0);
+  DIExprBuf &foldConstantMath();
+  DIExprBuf &replaceArg(uint64_t OldArgIndex, uint64_t NewArgIndex);
+  // more mutation methods ...
+
+  DIExpression *toExpr() const;
+};
+```
+
+## Future Work
+
+### IR representation
+
+Initially, the IR representation can be kept identical.
+
+Eventually, we can consider supporting a new syntax in the IR to track the
+logical encapsulation of each operation and more directly track the
+implementation:
+
+```{list-table}
+:header-rows: 1
+* - Old
+  - New
+* - `DIExpression(DW_OP_addr, 42, DW_OP_plus_uconst, 8, DW_OP_deref)`
+  - `DIExpression(DIOp::Addr(42), DIOp::PlusUConst(8), DIOp::Deref())`
+```
+
+This can be bi-direction and forward compatible from existing IR. As the
+bitcode does not change in any event (it is still always `vector<uint64_t>`)
+this does not represent a compatibility break.
+
+### `LLVMContext` representation
+
+If you squint, `DIOp::Op` is just a vector of two `uint64_t`. Some initial
+profiling shows that replacing the `vector<uint64_t>` representation with
+`vector<DIOp::Op>` may amount to a wash in terms of and instructions retired
+and RSS, with some significant improvements in some cases and some significant
+regressions in others:
+
+```{table} instructions:u
+|                       | Old                  | New          |
+| :-------------------- | -------------------: | -----------: |
+| stage1-O3             | `   60554M (-0.02%)` | `   60563M`  |
+| stage1-ReleaseThinLTO | `   76249M (+0.00%)` | `   76246M`  |
+| stage1-ReleaseLTO-g   | `   89006M (-0.04%)` | `   89038M`  |
+| stage1-O0-g           | `   18520M (+0.01%)` | `   18519M`  |
+| stage1-aarch64-O3     | `   67536M (+0.00%)` | `   67536M`  |
+| stage1-aarch64-O0-g   | `   22600M (-0.01%)` | `   22603M`  |
+| stage2-O3             | `   52437M (-0.04%)` | `   52460M`  |
+| stage2-O0-g           | `   16219M (-0.01%)` | `   16221M`  |
+| stage2-clang          | `34618385M (+0.07%)` | `34592728M`  |
+```
+
+```{table} max-rss
+|                       | Old                | New       |
+| :-------------------- | -----------------: | --------: |
+| stage1-O3             | `2780MiB (-0.82%)` | `2803MiB` |
+| stage1-ReleaseThinLTO | `2712MiB (-0.57%)` | `2727MiB` |
+| stage1-ReleaseLTO-g   | `3280MiB (-0.77%)` | `3305MiB` |
+| stage1-O0-g           | `2605MiB (-0.90%)` | `2629MiB` |
+| stage1-aarch64-O3     | `2974MiB (-0.68%)` | `2995MiB` |
+| stage1-aarch64-O0-g   | `2596MiB (-0.78%)` | `2617MiB` |
+| stage2-O3             | `2590MiB (+0.17%)` | `2586MiB` |
+| stage2-O0-g           | `2493MiB (+0.14%)` | `2490MiB` |
+| stage2-clang          | `2321MiB (+0.91%)` | `2300MiB` |
+```
+
+However, the situation does get marginally worse once we have eliminated
+`DW_OP_LLVM_fragment`/`DIOp::LLVMFragment`, since the existing representation
+is larger in this case and it is relatively common.
+
+This switch is not inevitable, and the hope is to make dealing with the
+internal representation uncommon for most developers anyway, but it could be
+considered in the future.
+
diff --git a/llvm/docs/UserGuides.md b/llvm/docs/UserGuides.md
index 96024beae9fe1..38a6cd67eec27 100644
--- a/llvm/docs/UserGuides.md
+++ b/llvm/docs/UserGuides.md
@@ -43,6 +43,7 @@ Instrumentor
 InstrRefDebugInfo
 RemoveDIsDebugInfo
 KeyInstructionsDebugInfo
+StronglyTypedDIExpressionDebugInfo
 InstrProfileFormat
 InstCombineContributorGuide
 WritingAnLLVMBackend
@@ -236,6 +237,10 @@ yaml2obj
   This document explains how the debug info feature Key Instructions is
   implemented in LLVM.
 
+- {doc}`StronglyTypedDIExpressionDebugInfo`
+
+  A design document for a rework of the `DIExpression` API.
+
 - {doc}`InstrProfileFormat`
 
   This document explains two binary formats of instrumentation-based profiles.

>From 67af92938fc6dbefaa797fe175c4fc0bf9f9e23c Mon Sep 17 00:00:00 2001
From: Scott Linder <Scott.Linder at amd.com>
Date: Fri, 10 Jul 2026 13:38:28 +0000
Subject: [PATCH 2/3] Clarify the constructors of DIExpRef

The comment was ambiguous, just write out the constructors explicitly.

Change-Id: I68af740dffbeb6862b1e3ffd5a5536e743fc2137
---
 llvm/docs/StronglyTypedDIExpressionDebugInfo.md | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/llvm/docs/StronglyTypedDIExpressionDebugInfo.md b/llvm/docs/StronglyTypedDIExpressionDebugInfo.md
index dcea26a6a768d..03adceffa6acb 100644
--- a/llvm/docs/StronglyTypedDIExpressionDebugInfo.md
+++ b/llvm/docs/StronglyTypedDIExpressionDebugInfo.md
@@ -112,10 +112,20 @@ The expected general shape of this type is:
 
 ```cpp
 class DIExprRef {
+  friend class DIExpression;
+  friend class DIExprBuf;
+
   iterator_range<DIOp::FromUIntIterator> Ops;
 
+  // Private constructor assumes Ops is syntactically valid, which
+  // is ensured by friends before construction.
+  explicit DIExprRef(iterator_range<DIOp::FromUIntIterator> Ops) : Ops(Ops) {};
 public:
-  // no public constructor
+  DIExprRef(const DIExprRef&) = default;
+  DIExprRef(DIExprRef&&) = default;
+  DIExprRef &operator=(const DIExprRef&) = default;
+  DIExprRef &operator=(DIExprRef&&) = default;
+  ~DIExprRef() = default;
 
   bool isValid() const;
   bool isSingleLocationExpression() const;
@@ -156,7 +166,7 @@ public:
   DIExprBuf() = default;
   explicit DIExprBuf(LLVMContext *Ctx);
   explicit DIExprBuf(const DIExpression *From);
-  explicit DIExprBuf(DIExprRef From);
+  explicit DIExprBuf(DIExprRef From, LLVMContext *Ctx = nullptr);
 
   DIExprRef asRef() const;
 

>From 1890710e4464fc6e2767f23252d0d8135109743b Mon Sep 17 00:00:00 2001
From: Scott Linder <Scott.Linder at amd.com>
Date: Thu, 16 Jul 2026 19:57:14 +0000
Subject: [PATCH 3/3] Address feedback and finish rebase

* Try to be more clear that the existing approach is fast to compile
* Try to argue the case for the double-buffering more clearly
* Use colon-delimited blocks for sphinx forms

Change-Id: I5825d109ff8795087bfe6395662d5f9dfec19d61
---
 .../StronglyTypedDIExpressionDebugInfo.md     | 46 ++++++++++++++-----
 1 file changed, 34 insertions(+), 12 deletions(-)

diff --git a/llvm/docs/StronglyTypedDIExpressionDebugInfo.md b/llvm/docs/StronglyTypedDIExpressionDebugInfo.md
index 03adceffa6acb..fa500ba3844dd 100644
--- a/llvm/docs/StronglyTypedDIExpressionDebugInfo.md
+++ b/llvm/docs/StronglyTypedDIExpressionDebugInfo.md
@@ -11,8 +11,8 @@ properties:
 
 * The core C++ API is just `SmallVector<uint64_t>` and a `get` function.
   * Code can inspect and edit the representation with no hard dependencies.
-  * The cost to `#include` the support code for this is light on templates and
-    other things which make compilation slow.
+  * The cost to `#include` the support code for this is light, making it fast
+    to compile.
   * The representation is pretty compact, and plays nice with the cache and
     branch predictor.
 
@@ -54,13 +54,13 @@ By way of analogy:
 * `DIExprRef` is to `std::string_view`/`llvm::StringRef`, as
 * `DIExprBuf` is to `std::string`/`llvm::SmallString`.
 
-```{note}
+:::{note}
 The analogy isn't airtight. The `DIExpression *` doesn't actually point to a
 contiguous view of `DIOp::Op`s. In the new hierarchy it acts more as an opaque
 handle to a `unique`d expression.
 
 There is also no equivalent to `std::char_traits`.
-```
+:::
 
 ### `DIOp::Op`
 
@@ -72,7 +72,7 @@ The alternative types of `Op` have `PascalCase`-ified names corresponding to
 the `DW_OP_*` names currently in use, and all live in `namespace DIOp`, next to
 `Op`. For example:
 
-```{list-table}
+:::{list-table}
 :header-rows: 1
 * - Old
   - New
@@ -84,7 +84,7 @@ the `DW_OP_*` names currently in use, and all live in `namespace DIOp`, next to
   - `DIOp::LLVMFragment(24, 8)`
 * - `{DW_OP_addr, 42, DW_OP_plus_uconst, 8, DW_OP_deref}`
   - `{DIOp::Addr(42), DIOp::PlusUConst(8), DIOp::Deref()}`
-```
+:::
 
 The new type is not `POD`, but is standard layout and trivially destructible.
 This enables a couple optimizations:
@@ -142,6 +142,27 @@ Double buffered internally, to avoid extra allocations for chained mutations.
 Can also be reused by e.g. a pass which needs to update many expressions, to
 further reduce allocations.
 
+:::{note}
+The double buffering essentially just codifies an existing pattern whereby most
+mutation method for `DIExpression` looks like:
+
+```cpp
+// check preconditions...
+
+// start making a new expression:
+SmallVector<uint64_t> NewOps;
+
+// push to NewOps...
+
+// unique the new expression:
+return DIExpression::get(NewOPs);
+```
+
+As `DIExprBuf` is intended to be reused for e.g. all expression updates within
+a pass, this change means the difference between `O(N)` and `O(1)` allocations,
+while not cluttering the interface with mutable buffer parameters.
+:::
+
 Since mutations on this type can be chained without `unique`ing the
 intermediate results, there is some potential improvement to RSS, although
 the overall contribution of `DIExpression` to the memory used during
@@ -168,6 +189,7 @@ public:
   explicit DIExprBuf(const DIExpression *From);
   explicit DIExprBuf(DIExprRef From, LLVMContext *Ctx = nullptr);
 
+  // Any operation on *this invalidates the returned DIExprRef
   DIExprRef asRef() const;
 
   DIExprBuf &clear();
@@ -191,13 +213,13 @@ Eventually, we can consider supporting a new syntax in the IR to track the
 logical encapsulation of each operation and more directly track the
 implementation:
 
-```{list-table}
+:::{list-table}
 :header-rows: 1
 * - Old
   - New
 * - `DIExpression(DW_OP_addr, 42, DW_OP_plus_uconst, 8, DW_OP_deref)`
   - `DIExpression(DIOp::Addr(42), DIOp::PlusUConst(8), DIOp::Deref())`
-```
+:::
 
 This can be bi-direction and forward compatible from existing IR. As the
 bitcode does not change in any event (it is still always `vector<uint64_t>`)
@@ -211,7 +233,7 @@ profiling shows that replacing the `vector<uint64_t>` representation with
 and RSS, with some significant improvements in some cases and some significant
 regressions in others:
 
-```{table} instructions:u
+:::{table} instructions:u
 |                       | Old                  | New          |
 | :-------------------- | -------------------: | -----------: |
 | stage1-O3             | `   60554M (-0.02%)` | `   60563M`  |
@@ -223,9 +245,9 @@ regressions in others:
 | stage2-O3             | `   52437M (-0.04%)` | `   52460M`  |
 | stage2-O0-g           | `   16219M (-0.01%)` | `   16221M`  |
 | stage2-clang          | `34618385M (+0.07%)` | `34592728M`  |
-```
+:::
 
-```{table} max-rss
+:::{table} max-rss
 |                       | Old                | New       |
 | :-------------------- | -----------------: | --------: |
 | stage1-O3             | `2780MiB (-0.82%)` | `2803MiB` |
@@ -237,7 +259,7 @@ regressions in others:
 | stage2-O3             | `2590MiB (+0.17%)` | `2586MiB` |
 | stage2-O0-g           | `2493MiB (+0.14%)` | `2490MiB` |
 | stage2-clang          | `2321MiB (+0.91%)` | `2300MiB` |
-```
+:::
 
 However, the situation does get marginally worse once we have eliminated
 `DW_OP_LLVM_fragment`/`DIOp::LLVMFragment`, since the existing representation



More information about the llvm-commits mailing list