[llvm] Reapply "[Dexter] Add rewriting for aggregate variables (#202800)" (PR #206535)
Stephen Tozer via llvm-commits
llvm-commits at lists.llvm.org
Tue Jun 30 06:04:23 PDT 2026
https://github.com/SLTozer updated https://github.com/llvm/llvm-project/pull/206535
>From b5965655092009712c28ea373f051454a6c0ba5a Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Mon, 29 Jun 2026 15:01:59 +0100
Subject: [PATCH 1/3] Reapply "[Dexter] Add rewriting for aggregate variables
(#202800)"
This reverts commit 5bf495456b9898c45c437b8dadae21417d04a9a5.
---
.../dexter/dex/debugger/lldb/LLDB.py | 1 +
.../dexter/dex/evaluation/ExpectRewriter.py | 26 ++++++-
.../Inputs/rewrite_aggregates_expected.cpp | 70 +++++++++++++++++++
.../rewrite_list_aggregates_expected.cpp | 58 +++++++++++++++
.../scripts/rewriting/rewrite_aggregates.cpp | 53 ++++++++++++++
.../rewriting/rewrite_list_aggregates.cpp | 48 +++++++++++++
6 files changed, 253 insertions(+), 3 deletions(-)
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_aggregates_expected.cpp
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_list_aggregates_expected.cpp
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_aggregates.cpp
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_list_aggregates.cpp
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py
index 8f90981f67e4c..4b92da9e0f38c 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py
@@ -510,6 +510,7 @@ def _evaluate_result_value(
"couldn't read from memory",
"Cannot access memory at address",
"invalid address (fault address:",
+ "error: parent is NULL",
]
)
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectRewriter.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectRewriter.py
index 7519d96a02a6d..5f8829287efe4 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectRewriter.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectRewriter.py
@@ -25,7 +25,20 @@ class ExpectedValueRewriter:
def __init__(self, expect: Expect, value: ValueIR):
self.expect = expect
self.root_value = value
- self.expected_value = expect.get_variable_result(value)
+ self.expected_value: Union[Dict, str, None] = None
+ if sub_values := self.root_value.sub_values:
+ self.expected_value = {
+ sub_value.expression: expected_value
+ for sub_value in sub_values
+ if (
+ expected_value := ExpectedValueRewriter(
+ expect, sub_value
+ ).expected_value
+ )
+ is not None
+ }
+ if not self.expected_value:
+ self.expected_value = expect.get_variable_result(value)
def unique_expected_values(elements: List[ExpectedValueRewriter]):
@@ -33,14 +46,21 @@ def unique_expected_values(elements: List[ExpectedValueRewriter]):
values, or a single item if there is only one non-duplicated expected value in the list, or None if there are no
valid expected values."""
+ def freeze(input):
+ assert input is not None, "Unexpected 'None' in an expected_value"
+ if isinstance(input, dict):
+ return tuple(sorted((str(k), freeze(v)) for k, v in input.items()))
+ return input
+
unique_set = set()
result = []
for element in elements:
expected_value = element.expected_value
if expected_value is None:
continue
- if expected_value not in unique_set:
- unique_set.add(expected_value)
+ frozen_value = freeze(expected_value)
+ if frozen_value not in unique_set:
+ unique_set.add(frozen_value)
result.append(expected_value)
if not result:
return None
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_aggregates_expected.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_aggregates_expected.cpp
new file mode 100644
index 0000000000000..8f9c5206e6480
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_aggregates_expected.cpp
@@ -0,0 +1,70 @@
+// RUN: rm -rf %t
+// RUN: mkdir %t
+// RUN: %dexter_regression_test_cxx_build %s -o %t/test
+// RUN: %dexter_regression_test_run --use-script --binary %t/test \
+// RUN: --results-directory %t/results -- %s 2>&1 | FileCheck %s
+// RUN: diff %t/results/%{s:basename} %S/Inputs/rewrite_aggregates_expected.cpp
+
+/// Test that Dexter can write disaggregated expected values for aggregates,
+/// including falling back to the parent value if sub_values contain errors,
+/// e.g. for pointers that are not dereferencable.
+
+/// NB: The exact contents of this file are compared against the expect file in
+/// the Inputs/ directory; any changes to this file, including comments,
+/// will require updating the corresponding expected file.
+
+// CHECK: Rewrote script to add 5 expected values.
+
+// CHECK: total_watched_steps: 5
+// CHECK: correct_steps: 5
+// CHECK: incorrect_steps: 0
+// CHECK: seen_values: 16
+// CHECK: missing_values: 0
+
+struct Point {
+ int X;
+ int Y;
+ int Z;
+};
+
+struct Rect {
+ Point TopLeft;
+ Point BottomRight;
+};
+
+int main() {
+ Point P{1, 2, 3};
+ int *I = &P.X;
+ Rect R{{1, 1, 1}, {2, 2, 2}};
+ int L[] = {0, 1, 2, 3, 4};
+ int *InvalidPtr = nullptr;
+ return 0; // !dex_label ret
+}
+
+/*
+---
+? !where {lines: !label 'ret'}
+: !value 'P':
+ X: '1'
+ Y: '2'
+ Z: '3'
+ !value 'I':
+ '*I': '1'
+ !value 'R':
+ BottomRight:
+ X: '2'
+ Y: '2'
+ Z: '2'
+ TopLeft:
+ X: '1'
+ Y: '1'
+ Z: '1'
+ !value 'L':
+ '[0]': '0'
+ '[1]': '1'
+ '[2]': '2'
+ '[3]': '3'
+ '[4]': '4'
+ !value 'InvalidPtr': '0x0000000000000000'
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_list_aggregates_expected.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_list_aggregates_expected.cpp
new file mode 100644
index 0000000000000..630ed6e887413
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_list_aggregates_expected.cpp
@@ -0,0 +1,58 @@
+// RUN: rm -rf %t
+// RUN: mkdir %t
+// RUN: %dexter_regression_test_cxx_build %s -o %t/test
+// RUN: %dexter_regression_test_run --use-script --binary %t/test \
+// RUN: --results-directory %t/results -- %s 2>&1 | FileCheck %s
+// RUN: diff %t/results/%{s:basename} \
+// RUN: %S/Inputs/rewrite_list_aggregates_expected.cpp
+
+/// Test that Dexter can write expects for variables that are aggregates and
+/// have more than one value, without writing any duplicate expected values.
+
+/// NB: The exact contents of this file are compared against the expect file in
+/// the Inputs/ directory; any changes to this file, including comments,
+/// will require updating the corresponding expected file.
+
+// CHECK: Rewrote script to add 1 expected values.
+
+struct Point {
+ int X;
+ int Y;
+};
+
+int main() {
+ Point P{1, 2};
+ P.X = 3; // !dex_label start
+ P.Y = 0;
+ P.X = 1;
+ P.Y = 2;
+ P = {0, 0};
+ return 0; // !dex_label end
+}
+
+// CHECK: total_watched_steps: 6
+// CHECK: correct_steps: 6
+// CHECK: incorrect_steps: 0
+// CHECK: partial_step_correctness: 6.0
+// CHECK: missing_var_steps: 0
+// CHECK: unexpected_value_steps: 0
+// CHECK: correct_step_coverage: 100.0% (6/6)
+// CHECK: seen_values: 10
+// CHECK: missing_values: 0
+
+/*
+---
+? !where {lines: !range [!label 'start', !label 'end']}
+: !value 'P':
+ - X: '1'
+ Y: '2'
+ - X: '3'
+ Y: '2'
+ - X: '3'
+ Y: '0'
+ - X: '1'
+ Y: '0'
+ - X: '0'
+ Y: '0'
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_aggregates.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_aggregates.cpp
new file mode 100644
index 0000000000000..da55a3345093c
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_aggregates.cpp
@@ -0,0 +1,53 @@
+// RUN: rm -rf %t
+// RUN: mkdir %t
+// RUN: %dexter_regression_test_cxx_build %s -o %t/test
+// RUN: %dexter_regression_test_run --use-script --binary %t/test \
+// RUN: --results-directory %t/results -- %s 2>&1 | FileCheck %s
+// RUN: diff %t/results/%{s:basename} %S/Inputs/rewrite_aggregates_expected.cpp
+
+/// Test that Dexter can write disaggregated expected values for aggregates,
+/// including falling back to the parent value if sub_values contain errors,
+/// e.g. for pointers that are not dereferencable.
+
+/// NB: The exact contents of this file are compared against the expect file in
+/// the Inputs/ directory; any changes to this file, including comments,
+/// will require updating the corresponding expected file.
+
+// CHECK: Rewrote script to add 5 expected values.
+
+// CHECK: total_watched_steps: 5
+// CHECK: correct_steps: 5
+// CHECK: incorrect_steps: 0
+// CHECK: seen_values: 16
+// CHECK: missing_values: 0
+
+struct Point {
+ int X;
+ int Y;
+ int Z;
+};
+
+struct Rect {
+ Point TopLeft;
+ Point BottomRight;
+};
+
+int main() {
+ Point P{1, 2, 3};
+ int *I = &P.X;
+ Rect R{{1, 1, 1}, {2, 2, 2}};
+ int L[] = {0, 1, 2, 3, 4};
+ int *InvalidPtr = nullptr;
+ return 0; // !dex_label ret
+}
+
+/*
+---
+!where {lines: !label ret}:
+ ? !value P
+ ? !value I
+ ? !value R
+ ? !value L
+ ? !value InvalidPtr
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_list_aggregates.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_list_aggregates.cpp
new file mode 100644
index 0000000000000..ab517d1342b2f
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_list_aggregates.cpp
@@ -0,0 +1,48 @@
+// RUN: rm -rf %t
+// RUN: mkdir %t
+// RUN: %dexter_regression_test_cxx_build %s -o %t/test
+// RUN: %dexter_regression_test_run --use-script --binary %t/test \
+// RUN: --results-directory %t/results -- %s 2>&1 | FileCheck %s
+// RUN: diff %t/results/%{s:basename} \
+// RUN: %S/Inputs/rewrite_list_aggregates_expected.cpp
+
+/// Test that Dexter can write expects for variables that are aggregates and
+/// have more than one value, without writing any duplicate expected values.
+
+/// NB: The exact contents of this file are compared against the expect file in
+/// the Inputs/ directory; any changes to this file, including comments,
+/// will require updating the corresponding expected file.
+
+// CHECK: Rewrote script to add 1 expected values.
+
+struct Point {
+ int X;
+ int Y;
+};
+
+int main() {
+ Point P{1, 2};
+ P.X = 3; // !dex_label start
+ P.Y = 0;
+ P.X = 1;
+ P.Y = 2;
+ P = {0, 0};
+ return 0; // !dex_label end
+}
+
+// CHECK: total_watched_steps: 6
+// CHECK: correct_steps: 6
+// CHECK: incorrect_steps: 0
+// CHECK: partial_step_correctness: 6.0
+// CHECK: missing_var_steps: 0
+// CHECK: unexpected_value_steps: 0
+// CHECK: correct_step_coverage: 100.0% (6/6)
+// CHECK: seen_values: 10
+// CHECK: missing_values: 0
+
+/*
+---
+!where {lines: !range [!label start, !label end]}:
+ ? !value P
+...
+*/
>From d9cf4758c447fb85ad432c5db29fcff9e2bb2176 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Tue, 30 Jun 2026 13:46:52 +0100
Subject: [PATCH 2/3] [Dexter] Require lldb-dap for Dexter and log when Dexter
is disabled
For some time, Dexter tests have been run using lldb-dap when it is
available rather than just lldb/lldb-server. However, the cross project test
dependencies have not been updated since then, meaning that lldb-dap is not
automatically built by check-cross-project. The Dexter-specific lit config
then skips the Dexter tests if lldb-dap is unavailable, which leads to some
check-cross-project builds never running the Dexter tests. This patch adds
lldb-dap to the build dependencies, and also adds a small log message to
inform when the Dexter tests are skipped; most cases where we skip the
Dexter test were already logged, and this addition expands that to cover all
cases.
---
cross-project-tests/CMakeLists.txt | 2 +-
cross-project-tests/lit.cfg.py | 5 +++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/cross-project-tests/CMakeLists.txt b/cross-project-tests/CMakeLists.txt
index 866dc98406af4..4dab5d3d51fd6 100644
--- a/cross-project-tests/CMakeLists.txt
+++ b/cross-project-tests/CMakeLists.txt
@@ -61,7 +61,7 @@ if("compiler-rt" IN_LIST LLVM_ENABLE_PROJECTS)
endif()
# Many dexter tests depend on lldb.
if("lldb" IN_LIST LLVM_ENABLE_PROJECTS)
- list(APPEND CROSS_PROJECT_TEST_DEPS lldb lldb-server)
+ list(APPEND CROSS_PROJECT_TEST_DEPS lldb lldb-server lldb-dap)
endif()
if ("lld" IN_LIST LLVM_ENABLE_PROJECTS)
diff --git a/cross-project-tests/lit.cfg.py b/cross-project-tests/lit.cfg.py
index 248c7609389f1..ae4647d33672e 100644
--- a/cross-project-tests/lit.cfg.py
+++ b/cross-project-tests/lit.cfg.py
@@ -282,6 +282,11 @@ def can_target_host():
dependencies = configure_dexter_substitutions()
if all(d in config.available_features for d in dependencies):
config.available_features.add("dexter")
+ else:
+ print(
+ "Skipping Dexter tests due to missing required projects: "
+ + ", ".join(d for d in dependencies if d not in config.available_features)
+ )
tool_dirs = [config.llvm_tools_dir]
>From dce3dce5cef4e58f9ef67e83084860be27f68402 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Tue, 30 Jun 2026 13:53:16 +0100
Subject: [PATCH 3/3] [Dexter] Avoid incorrect state matching against frames
below main
This patch fixes an error that caused some Dexter test failures, driven by
two separate causes. The first issue is that frames below main were
appearing in the program stacktrace; while Dexter tries to filter frames
below main during the stacktrace collection step based on a pre-written
list, this list may not be comprehensive enough, as the symbol
"___lldb_unnamed_symbol_2a150" has also appeared. In order to guard against
this and future cases that might appear, this patch adds a check to Dexter
for "presentationHint: deemphasize" in the DAP response; this is added by
LLDB (and other dap-based debuggers) as a hint that the frame is not user
source, and should be a generally useful way of avoiding evaluating frames
that are not wanted.
The second issue is a mismatch between the breakpoint-setting logic and the
state-matching logic: the former allows root !where nodes to omit the "file"
field, using the script file as a default file. The state matching logic
does not perform any checking for an omitted file. Together, this means that
we may correctly set breakpoints for e.g. "test.cpp:10", but when we go to
match against frames, the !where node may match against some other line-10
that appears below (e.g. as a caller of) the frame at test.cpp:10. This
patch copies the default_file logic to state matching, meaning that we will
only match against the intended frame.
---
.../debuginfo-tests/dexter/dex/debugger/DAP.py | 4 ++++
.../debuginfo-tests/dexter/dex/evaluation/StateMatch.py | 8 ++++++--
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py
index 0dfded0ff881a..ac5b4cdd57434 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py
@@ -969,6 +969,10 @@ def get_stack_frames(self, step_index: int) -> StepIR:
frames = []
for stackframe in stackframes:
+ # Some frames are marked "deemphasize" to indicate that they are not interesting; these frames can be
+ # skipped by Dexter.
+ if stackframe.get("presentationHint", None) == "deemphasize":
+ continue
# No source, skip the frame! Currently I've only observed this for frames below main, so we break here; if
# it happens elsewhere, then this will break more stuff and we'll come up with a better solution.
if (
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py
index 2be87590b978c..3919e302d69d5 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py
@@ -9,7 +9,7 @@
from dataclasses import dataclass, field
import os
-from typing import Dict, List, Tuple
+from typing import Dict, List, Optional, Tuple
from dex.dextIR import FrameIR, StepIR
from dex.test_script import DexterScript, Scope
@@ -29,9 +29,13 @@ def match_where_to_frame(
where: Where,
frame: FrameIR,
labels: FileLabels,
+ default_path: Optional[str] = None,
) -> bool:
"""A very simple matcher, returns True iff `where` matches `frame`."""
- if where.file is not None and not is_subpath(where.file, frame.loc.path):
+ file = where.file
+ if not file and not where.function:
+ file = default_path
+ if file is not None and not is_subpath(file, frame.loc.path):
return False
if where.function is not None:
fn = frame.function
More information about the llvm-commits
mailing list