[llvm-branch-commits] [llvm] [Dexter] Add support for aggregate expects in the debugger (PR #202545)
Stephen Tozer via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Mon Jun 15 04:39:34 PDT 2026
https://github.com/SLTozer updated https://github.com/llvm/llvm-project/pull/202545
>From 767a87e7a21f650c5a03cd1c01cf5412ac88eb58 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Mon, 8 Jun 2026 17:17:18 +0100
Subject: [PATCH 1/4] [Dexter] Add support for aggregate expects in the
debugger
Allows aggregate expects to be written in Dexter scripts, in the form:
!value expect:
member1: 0
member2:
submember1: 1
submember2: 2
This uses the "variables" feature provided in the DAP interface to
recursively fetch sub-variables from each evaluated variable, and modifies
the DebuggerExpectMatch class to also be recursive, containing submatches
for each expected aggregate member value.
Some key concepts that are affected by this are the "match_result", which
has changed from a bool to a 3-way result ("FALSE", "TRUE", "PARTIAL"),
where "PARTIAL" means that at least one submatch was TRUE and at least one
was FALSE. We also add a `match_distance`, which is used as a measure of
correctness. The match distance ranges from 0.0 to 1.0: a total match has a
distance of 0.0, a total mismatch has 1.0, and a partial match's distance is
equal to the average distance of its immediate children.
---
.../dexter/dex/debugger/DAP.py | 51 ++++-
.../dexter/dex/dextIR/StepIR.py | 5 +-
.../dexter/dex/dextIR/ValueIR.py | 10 +
.../dexter/dex/evaluation/ExpectMatch.py | 181 +++++++++++++++++-
.../dexter/dex/evaluation/Metrics.py | 32 +++-
.../dexter/dex/evaluation/RunMatch.py | 14 +-
.../scripts/debugging/debug_aggregates.cpp | 75 ++++++++
.../scripts/evaluation/eval_aggregates.cpp | 60 ++++++
.../evaluation/eval_list_aggregates.cpp | 49 +++++
9 files changed, 454 insertions(+), 23 deletions(-)
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/debug_aggregates.cpp
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_aggregates.cpp
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_list_aggregates.cpp
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 86d3217cba07a..a5020d7de6cec 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py
@@ -18,7 +18,7 @@
import threading
import time
from enum import Enum
-from typing import List, Optional
+from typing import Dict, List, Optional, Union
from dex.debugger.DebuggerBase import DebuggerBase, watch_is_active
from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR
@@ -551,6 +551,22 @@ def _await_response(self, seq: int, timeout: float = 0.0) -> dict:
time.sleep(0.001)
return self._debugger_state.get_response(seq)
+ # Helper method that sends the request defined by "command" + "arguments", awaits the response, and returns the
+ # response when it arrives. An optional timeout for the response may be passed.
+ # If allow_failure is passed, then the result may instead be a str containing the fail reason if the request failed.
+ def _communicate_request(
+ self, command: str, arguments=None, timeout: float = 60.0, allow_failure=False
+ ) -> Union[Dict, str]:
+ req_id = self.send_message(self.make_request(command, arguments))
+ response = self._await_response(req_id, timeout)
+ if not response["success"]:
+ if not allow_failure:
+ raise DebuggerException(
+ f"received failure response for command {command}"
+ )
+ return response["message"]
+ return response["body"]
+
## End of DAP communication methods
############################################################################
@@ -1018,6 +1034,32 @@ def frames_below_main(self):
def _evaluate_result_value(expression: str, result_string: str) -> ValueIR:
"""For the result of an "evaluate" message, return a ValueIR. Implementation must be debugger-specific."""
+ # For the given `value` and associated `variables_reference`, recursively requests "variables" information for all
+ # child variables and adds them as sub_values to `value`.
+ def _evaluate_subvariables(self, value: ValueIR, variables_reference: int):
+ if variables_reference == 0:
+ return
+ # DFS subvariables recursively, adding them as sub_values to their parent ValueIRs.
+ variables_irs = {variables_reference: value}
+ search_vars = [variables_reference]
+ while search_vars:
+ next_var = search_vars[0]
+ search_vars = search_vars[1:]
+ # The ValueIR for the variable/subvariable whose children we are examining.
+ variable_ir: ValueIR = variables_irs[next_var]
+ result_vars: Dict = self._communicate_request(
+ "variables", {"variablesReference": next_var, "filter": "named"}
+ )
+ for var in result_vars["variables"]:
+ new_ir = self._evaluate_result_value(
+ var["name"], var["value"], var.get("type")
+ )
+ variable_ir.sub_values.append(new_ir)
+ new_ref = var.get("variablesReference", 0)
+ if new_ref != 0:
+ variables_irs[new_ref] = new_ir
+ search_vars.append(new_ref)
+
def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
# The frame_idx passed in here needs to be translated to the debug adapter's internal frame ID.
dap_frame_id = self._debugger_state.frame_map[frame_idx]
@@ -1040,8 +1082,13 @@ def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
result = eval_response["message"]
else:
result = "<unable to evaluate expression>"
+ variables_ref = 0
else:
result = eval_response["body"]["result"]
+ variables_ref = eval_response["body"].get("variablesReference", 0)
type_str = eval_response["body"].get("type")
- return self._evaluate_result_value(expression, result, type_str)
+ value_ir = self._evaluate_result_value(expression, result, type_str)
+ self._evaluate_subvariables(value_ir, variables_ref)
+
+ return value_ir
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/StepIR.py b/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/StepIR.py
index 92e66fc525388..cde5f554b5dc1 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/StepIR.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/StepIR.py
@@ -128,5 +128,8 @@ def detailed_print(self) -> List[str]:
if self.watches:
lines.append(f"Variables:")
for value in sorted(self.watches.values(), key=lambda v: v.expression):
- lines.append(f" {value}")
+ if not value.sub_values:
+ lines.append(f" {value}")
+ else:
+ value.dump_nested(lines, 1)
return lines
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/ValueIR.py b/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/ValueIR.py
index 770f646258f73..714d90367fd3a 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/ValueIR.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/ValueIR.py
@@ -6,6 +6,9 @@
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+from typing import List, Optional
+
+
class ValueIR:
"""Data class to store the result of an expression evaluation."""
@@ -26,6 +29,7 @@ def __init__(
self.error_string = error_string
self.is_optimized_away = is_optimized_away
self.is_irretrievable = is_irretrievable
+ self.sub_values: list[ValueIR] = []
def __str__(self):
prefix = '"{}": '.format(self.expression)
@@ -39,3 +43,9 @@ def __str__(self):
self.could_evaluate, self.is_irretrievable, self.is_optimized_away
)
)
+
+ def dump_nested(self, lines: List[str], indent: int = 0):
+ indent_str = " " * indent
+ lines.append(f"{indent_str}{self}")
+ for v in self.sub_values:
+ v.dump_nested(lines, indent + 1)
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectMatch.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectMatch.py
index 7296f2226af8a..c9047c6f7d80c 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectMatch.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectMatch.py
@@ -6,24 +6,182 @@
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""Utilities for matching debugger output to script expected values."""
-from typing import Any, Dict, List, Union
+from collections import Counter, OrderedDict
+from enum import Enum, IntEnum
+from typing import Any, Dict, List, Optional, Set, Tuple, Union
from dex.dextIR import ValueIR
from dex.test_script.Nodes import Expect, Value
+def get_expected_value_set(
+ expected, prepend_tuple: Tuple = ()
+) -> Dict[Tuple[str], int]:
+ """For the given "expected" taken directly from the script YAML, returns the set of all actual expected
+ values, using tuples to represent nested expected values, and mapping each result to the number of times that it
+ appears in the set (giving a count). For example, the expected values of:
+ ```
+ !value foo:
+ - 4
+ - 6
+ - x: 5
+ y: 10
+ - x: 5
+ y: 20
+ ```
+ Would be represented by the dict:
+ ```
+ {
+ (4,): 1,
+ (6,): 1,
+ ("x", 5): 2,
+ ("y", 10): 1,
+ ("y", 20): 1,
+ }
+ ```
+ """
+ result: Dict[Tuple, int] = Counter()
+ if isinstance(expected, list):
+ for ev in expected:
+ result.update(get_expected_value_set(ev, prepend_tuple))
+ elif isinstance(expected, dict):
+ for sub_expect, sub_expected in expected.items():
+ next_prepend = prepend_tuple + (str(sub_expect),)
+ result.update(get_expected_value_set(sub_expected, next_prepend))
+ else:
+ result[prepend_tuple + (str(expected),)] += 1
+ return result
+
+
+class MatchResult(IntEnum):
+ FALSE = 0
+ PARTIAL = 1
+ TRUE = 2
+
+
class DebuggerExpectMatch:
"""Class that represents the match between a particular expected value for an Expect node and the actual debugger
- output corresponding to the watched value for that node."""
+ output corresponding to the watched value for that node.
+ `actual_result` is None if `actual` or `expect.get_variable_result(actual)` is None,
+ Otherwise, if `expected` is a dict, then `actual_result` is a dict[str, DebuggerExpectMatch],
+ Otherwise, `actual_result` is a str.
+ """
- def __init__(self, expect: Expect, expected, actual: ValueIR):
+ def __init__(self, expect: Expect, expected, actual: Optional[ValueIR]):
self.expect = expect
self.expected = expected
self.actual = actual
- self.actual_result = self.expect.get_variable_result(self.actual)
- self.match_result = (
- self.expected is not None and str(self.expected) == self.actual_result
+ self.actual_result, self.match_result = self._get_actual_result()
+ self.match_distance = self._get_match_distance()
+
+ def _get_actual_result(
+ self,
+ ) -> Tuple[Union[str, Dict[str, "DebuggerExpectMatch"], None], MatchResult]:
+ if isinstance(self.expected, dict):
+ sub_expect_results: Dict[str, DebuggerExpectMatch] = OrderedDict()
+ for sub_expect, sub_expected in self.expected.items():
+ value = (
+ None
+ if self.actual is None
+ else next(
+ (
+ sub_value
+ for sub_value in self.actual.sub_values
+ if sub_value.expression == sub_expect
+ ),
+ None,
+ )
+ )
+ sub_expect_results[sub_expect] = DebuggerExpectMatch(
+ self.expect, sub_expected, value
+ )
+ if all(
+ result.match_result == MatchResult.TRUE
+ for result in sub_expect_results.values()
+ ):
+ match_result = MatchResult.TRUE
+ elif all(
+ result.match_result == MatchResult.FALSE
+ for result in sub_expect_results.values()
+ ):
+ match_result = MatchResult.FALSE
+ else:
+ match_result = MatchResult.PARTIAL
+ return sub_expect_results, match_result
+
+ actual_result = (
+ self.expect.get_variable_result(self.actual)
+ if self.actual is not None
+ else None
+ )
+ match_result = (
+ MatchResult.TRUE
+ if (self.expected is not None and str(self.expected) == actual_result)
+ else MatchResult.FALSE
)
+ return actual_result, match_result
+
+ def _get_match_distance(self) -> float:
+ if self.match_result == MatchResult.TRUE:
+ return 0.0
+ if self.match_result == MatchResult.FALSE:
+ return 1.0
+ assert (
+ isinstance(self.actual_result, Dict) and self.actual_result
+ ), "Partial match without submatches."
+ dists = [m.match_distance for m in self.actual_result.values()]
+ return sum(dists) / len(dists)
+
+ def get_expression(self) -> Optional[str]:
+ return self.actual.expression if self.actual else None
+
+ def get_all_matched_values(self, prepend_tuple: Tuple = ()) -> Set[Tuple]:
+ """Similar to `get_expected_value_set` above, but returns the set of expected values that were successfully
+ matched in this DebuggerExpectMatch, and returns just a set (no counts)."""
+ if self.match_result == MatchResult.FALSE:
+ return set()
+ assert (
+ self.actual_result is not None and self.actual is not None
+ ), "Non-false match with no actual result."
+ if isinstance(self.actual_result, str):
+ assert (
+ self.match_result == MatchResult.TRUE
+ ), "Partial match without submatches."
+ return {prepend_tuple + (str(self.expected),)}
+ result = set()
+ for sub_expr, sub_result in self.actual_result.items():
+ result = result.union(
+ sub_result.get_all_matched_values(prepend_tuple + (sub_expr,))
+ )
+ return result
+
+ def short_str(self, use_color=True) -> str:
+ def colorize(input: str, match_result: MatchResult) -> str:
+ if not use_color:
+ return input
+ if match_result == MatchResult.TRUE:
+ return f"<g>{input}</>"
+ if match_result == MatchResult.PARTIAL:
+ return f"<y>{input}</>"
+ return f"<r>{input}</>"
+
+ if self.actual is None:
+ return colorize("<Missing>", self.match_result)
+ if self.actual_result is None:
+ if self.actual.is_optimized_away:
+ return colorize("<OptimizedOut>", self.match_result)
+ if self.actual.is_irretrievable:
+ return colorize("<Irretrievable>", self.match_result)
+ return colorize("<EvaluateFailed>", self.match_result)
+ if isinstance(self.actual_result, str):
+ return colorize(self.actual_result, self.match_result)
+ assert isinstance(self.expected, dict)
+ sub_values = [
+ colorize(f'"{sub_expr}": ', sub_result.match_result)
+ + sub_result.short_str()
+ for sub_expr, sub_result in self.actual_result.items()
+ ]
+ return f"{{ {', '.join(sub_values)} }}"
def get_expect_match(expect: Expect, expected_values, actual: ValueIR):
@@ -31,8 +189,15 @@ def get_expect_match(expect: Expect, expected_values, actual: ValueIR):
matching expected values, or for None if there are no matching expected values."""
if not isinstance(expected_values, list):
expected_values = [expected_values]
+ best_partial_match = DebuggerExpectMatch(expect, None, actual)
+ best_partial_match_dist = 1.0
for expected_value in expected_values:
expect_match = DebuggerExpectMatch(expect, expected_value, actual)
- if expect_match.match_result:
+ if expect_match.match_result == MatchResult.TRUE:
return expect_match
- return DebuggerExpectMatch(expect, None, actual)
+ # A "FALSE" match will have a match distance of 1.0, and therefore will never be considered a "best match".
+ if expect_match.match_distance < best_partial_match_dist:
+ best_partial_match = expect_match
+ best_partial_match_dist = expect_match.match_distance
+
+ return best_partial_match
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/Metrics.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/Metrics.py
index 551eb153c0710..1cac20303e09f 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/Metrics.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/Metrics.py
@@ -9,7 +9,11 @@
from typing import Any, Dict, List, Union
-from dex.evaluation.ExpectMatch import DebuggerExpectMatch
+from dex.evaluation.ExpectMatch import (
+ DebuggerExpectMatch,
+ MatchResult,
+ get_expected_value_set,
+)
from dex.test_script.Nodes import Expect, Value
@@ -52,6 +56,8 @@ def aggregate(self, other):
return ScalarMetric(self.value + other.value, self.improves_asc)
def __repr__(self):
+ if isinstance(self.value, float):
+ return f"{self.value:.4}"
return f"{self.value}"
@@ -98,19 +104,31 @@ def get_variable_metrics(
if not isinstance(expected_values, list):
expected_values = [expected_values]
num_total_steps = len(matches)
+ all_expected_values = get_expected_value_set(expected_values)
seen_expected_values = set()
num_correct_steps = 0
num_missing_var_steps = 0
num_unexpected_value_steps = 0
+ partial_step_correctness = 0.0
for match in matches:
- if match.match_result:
- seen_expected_values.add(match.expected)
+ partial_step_correctness += 1.0 - match.match_distance
+ seen_expected_values = seen_expected_values.union(
+ match.get_all_matched_values()
+ )
+ if match.match_result == MatchResult.TRUE:
num_correct_steps += 1
elif match.actual_result is None:
num_missing_var_steps += 1
else:
num_unexpected_value_steps += 1
- num_seen_values = sum(ev in seen_expected_values for ev in expected_values)
+ assert all(
+ ev in all_expected_values for ev in seen_expected_values
+ ), "Saw expected values that weren't expected?"
+ num_seen_values = sum(all_expected_values[ev] for ev in seen_expected_values)
+ num_missing_values = sum(
+ 0 if ev in seen_expected_values else count
+ for ev, count in all_expected_values.items()
+ )
# And finally produce the metrics map and add the new result to the list.
metrics = {
# The number of steps. Though this is not a useful metric in itself, it may be useful to see in tandem with
@@ -122,6 +140,8 @@ def get_variable_metrics(
"incorrect_steps": ScalarMetric(
num_total_steps - num_correct_steps, improves_asc=False
),
+ # The sum of the 0.0-1.0 "correctness value" of matches across each step.
+ "partial_step_correctness": ScalarMetric(partial_step_correctness),
# The number of steps where the watched variable/expression was not available in the debugger.
"missing_var_steps": ScalarMetric(num_missing_var_steps, improves_asc=False),
# The number of steps where the watched variable/expression had a value not in the set of expected values.
@@ -133,8 +153,6 @@ def get_variable_metrics(
# The number of expected values that were observed at least once.
"seen_values": ScalarMetric(num_seen_values),
# The number of expected values that were not observed.
- "missing_values": ScalarMetric(
- len(expected_values) - num_seen_values, improves_asc=False
- ),
+ "missing_values": ScalarMetric(num_missing_values, improves_asc=False),
}
return metrics
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py
index 1be5157997222..a7112ce4c5b30 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py
@@ -14,7 +14,11 @@
from typing import Any, Dict, List, Tuple
from dex.dextIR import DextIR, StepIR
-from dex.evaluation.ExpectMatch import DebuggerExpectMatch, get_expect_match
+from dex.evaluation.ExpectMatch import (
+ DebuggerExpectMatch,
+ MatchResult,
+ get_expect_match,
+)
from dex.evaluation.Metrics import (
Metric,
get_variable_metrics,
@@ -132,17 +136,17 @@ def dump_step_results(self) -> str:
matching_expects = [
(expect, match)
for expect, match in step_match.expect_matches.items()
- if match.match_result
+ if match.match_result == MatchResult.TRUE
]
non_matching_expects = [
(expect, match)
for expect, match in step_match.expect_matches.items()
- if not match.match_result
+ if match.match_result != MatchResult.TRUE
]
if matching_expects:
- result += f" Matching nodes: [{', '.join(f'{expect}={match.actual_result}' for expect, match in matching_expects)}]\n"
+ result += f" Matching nodes: [{', '.join(f'{expect}={match.short_str()}' for expect, match in matching_expects)}]\n"
if non_matching_expects:
- result += f" Non-matching nodes: [{', '.join(f'{expect}={match.actual_result}' for expect, match in non_matching_expects)}]\n"
+ result += f" Non-matching nodes: [{', '.join(f'{expect}={match.short_str()}' for expect, match in non_matching_expects)}]\n"
return result
def get_metric_output(self):
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/debug_aggregates.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/debug_aggregates.cpp
new file mode 100644
index 0000000000000..33aa404c3d349
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/debug_aggregates.cpp
@@ -0,0 +1,75 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t -- %s | FileCheck %s
+
+/// Check that the debugger is able to fetch the components of aggregate values.
+
+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 };
+ return 0; // !dex_label ret
+}
+
+// CHECK: Frame 0:
+// CHECK-NEXT: main
+// CHECK: "I": (int *)
+// CHECK-NEXT: "*I": (int) 1
+// CHECK-NEXT: "L": (int[5])
+// CHECK-NEXT: "[0]": (int) 0
+// CHECK-NEXT: "[1]": (int) 1
+// CHECK-NEXT: "[2]": (int) 2
+// CHECK-NEXT: "[3]": (int) 3
+// CHECK-NEXT: "[4]": (int) 4
+// CHECK-NEXT: "P": (Point)
+// CHECK-NEXT: "X": (int) 1
+// CHECK-NEXT: "Y": (int) 2
+// CHECK-NEXT: "Z": (int) 3
+// CHECK-NEXT: "R": (Rect)
+// CHECK-NEXT: "TopLeft": (Point)
+// CHECK-NEXT: "X": (int) 1
+// CHECK-NEXT: "Y": (int) 1
+// CHECK-NEXT: "Z": (int) 1
+// CHECK-NEXT: "BottomRight": (Point)
+// CHECK-NEXT: "X": (int) 2
+// CHECK-NEXT: "Y": (int) 2
+// CHECK-NEXT: "Z": (int) 2
+
+
+/*
+---
+!where {lines: !label ret}:
+ !value P:
+ X: 1
+ Y: 2
+ Z: 3
+ !value I:
+ "*I": 1
+ !value R:
+ TopLeft:
+ X: 1
+ Y: 1
+ Z: 1
+ BottomRight:
+ X: 2
+ Y: 2
+ Z: 2
+ !value L:
+ "[0]": 0
+ "[1]": 1
+ "[2]": 2
+ "[3]": 3
+ "[4]": 4
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_aggregates.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_aggregates.cpp
new file mode 100644
index 0000000000000..97209869466f1
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_aggregates.cpp
@@ -0,0 +1,60 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --binary %t -- %s | FileCheck %s
+
+/// Check that the debugger is able to evaluate the components of aggregate values.
+
+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 };
+ return 0; // !dex_label ret
+}
+
+// CHECK: total_watched_steps: 4
+// CHECK: correct_steps: 3
+// CHECK: incorrect_steps: 1
+// CHECK: partial_step_correctness: 3.333
+// CHECK: missing_var_steps: 0
+// CHECK: unexpected_value_steps: 1
+// CHECK: seen_values: 13
+// CHECK: missing_values: 2
+
+/*
+---
+!where {lines: !label ret}:
+ !value P:
+ X: 1 # Correct
+ Y: 0 # Incorrect
+ # Missing "Z"
+ W: 8 # Not present
+ !value I:
+ "*I": 1
+ !value R:
+ TopLeft:
+ X: 1
+ Y: 1
+ Z: 1
+ BottomRight:
+ X: 2
+ Y: 2
+ Z: 2
+ !value L:
+ "[0]": 0
+ "[1]": 1
+ "[2]": 2
+ "[3]": 3
+ "[4]": 4
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_list_aggregates.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_list_aggregates.cpp
new file mode 100644
index 0000000000000..356949115a8e8
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_list_aggregates.cpp
@@ -0,0 +1,49 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --binary %t -- %s | FileCheck %s
+
+/// Check that the debugger is able to correctly evaluate a list of expected
+/// aggregate 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: 12
+// 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: 1
+ Y: 2
+ - X: 0
+ Y: 0
+...
+*/
>From 6db3d1d21616e1059691da204ccb713da92a4c7e Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Wed, 10 Jun 2026 13:19:21 +0100
Subject: [PATCH 2/4] format
---
.../scripts/debugging/debug_aggregates.cpp | 20 ++++++++---------
.../scripts/evaluation/eval_aggregates.cpp | 22 ++++++++++---------
.../evaluation/eval_list_aggregates.cpp | 9 ++++----
3 files changed, 27 insertions(+), 24 deletions(-)
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/debug_aggregates.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/debug_aggregates.cpp
index 33aa404c3d349..9c1ae041b45bb 100644
--- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/debug_aggregates.cpp
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/debug_aggregates.cpp
@@ -1,24 +1,25 @@
// RUN: %dexter_regression_test_cxx_build %s -o %t
-// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t -- %s | FileCheck %s
+// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t \
+// RUN: -- %s | FileCheck %s
/// Check that the debugger is able to fetch the components of aggregate values.
struct Point {
- int X;
- int Y;
- int Z;
+ int X;
+ int Y;
+ int Z;
};
struct Rect {
- Point TopLeft;
- Point BottomRight;
+ Point TopLeft;
+ Point BottomRight;
};
int main() {
- Point P { 1, 2, 3 };
+ Point P{1, 2, 3};
int *I = &P.X;
- Rect R { { 1, 1, 1 }, { 2, 2, 2 } };
- int L[] = { 0, 1, 2, 3, 4 };
+ Rect R{{1, 1, 1}, {2, 2, 2}};
+ int L[] = {0, 1, 2, 3, 4};
return 0; // !dex_label ret
}
@@ -46,7 +47,6 @@ int main() {
// CHECK-NEXT: "Y": (int) 2
// CHECK-NEXT: "Z": (int) 2
-
/*
---
!where {lines: !label ret}:
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_aggregates.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_aggregates.cpp
index 97209869466f1..77662cbbf2437 100644
--- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_aggregates.cpp
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_aggregates.cpp
@@ -1,24 +1,26 @@
// RUN: %dexter_regression_test_cxx_build %s -o %t
-// RUN: %dexter_regression_test_run --use-script --binary %t -- %s | FileCheck %s
+// RUN: %dexter_regression_test_run --use-script --binary %t -- %s \
+// RUN: | FileCheck %s
-/// Check that the debugger is able to evaluate the components of aggregate values.
+/// Check that the debugger is able to evaluate the components of aggregate
+/// values.
struct Point {
- int X;
- int Y;
- int Z;
+ int X;
+ int Y;
+ int Z;
};
struct Rect {
- Point TopLeft;
- Point BottomRight;
+ Point TopLeft;
+ Point BottomRight;
};
int main() {
- Point P { 1, 2, 3 };
+ Point P{1, 2, 3};
int *I = &P.X;
- Rect R { { 1, 1, 1 }, { 2, 2, 2 } };
- int L[] = { 0, 1, 2, 3, 4 };
+ Rect R{{1, 1, 1}, {2, 2, 2}};
+ int L[] = {0, 1, 2, 3, 4};
return 0; // !dex_label ret
}
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_list_aggregates.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_list_aggregates.cpp
index 356949115a8e8..f76c929253f99 100644
--- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_list_aggregates.cpp
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_list_aggregates.cpp
@@ -1,16 +1,17 @@
// RUN: %dexter_regression_test_cxx_build %s -o %t
-// RUN: %dexter_regression_test_run --use-script --binary %t -- %s | FileCheck %s
+// RUN: %dexter_regression_test_run --use-script --binary %t -- %s \
+// RUN: | FileCheck %s
/// Check that the debugger is able to correctly evaluate a list of expected
/// aggregate values.
struct Point {
- int X;
- int Y;
+ int X;
+ int Y;
};
int main() {
- Point P { 1, 2 };
+ Point P{1, 2};
P.X = 3; // !dex_label start
P.Y = 0;
P.X = 1;
>From 1e5e8ff13b96df21a6f25595d97dabd894587792 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Thu, 11 Jun 2026 11:20:55 +0100
Subject: [PATCH 3/4] Prevent evaluating through invalid values/nullptrs
---
cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 a5020d7de6cec..bc3ea2af27f59 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py
@@ -1056,7 +1056,7 @@ def _evaluate_subvariables(self, value: ValueIR, variables_reference: int):
)
variable_ir.sub_values.append(new_ir)
new_ref = var.get("variablesReference", 0)
- if new_ref != 0:
+ if new_ir.could_evaluate and not new_ir.is_irretrievable and new_ref != 0:
variables_irs[new_ref] = new_ir
search_vars.append(new_ref)
>From 7a9fbedca02c474edc9c259a7c7039ee917b5a5b Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Thu, 11 Jun 2026 13:25:05 +0100
Subject: [PATCH 4/4] format
---
.../debuginfo-tests/dexter/dex/debugger/DAP.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
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 bc3ea2af27f59..0dfded0ff881a 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py
@@ -1056,7 +1056,11 @@ def _evaluate_subvariables(self, value: ValueIR, variables_reference: int):
)
variable_ir.sub_values.append(new_ir)
new_ref = var.get("variablesReference", 0)
- if new_ir.could_evaluate and not new_ir.is_irretrievable and new_ref != 0:
+ if (
+ new_ir.could_evaluate
+ and not new_ir.is_irretrievable
+ and new_ref != 0
+ ):
variables_irs[new_ref] = new_ir
search_vars.append(new_ref)
More information about the llvm-branch-commits
mailing list