https://github.com/SLTozer created 
https://github.com/llvm/llvm-project/pull/202545

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.

>From 522045e96a115bb6ec2b7626255c7021a1d9d05f Mon Sep 17 00:00:00 2001
From: Stephen Tozer <[email protected]>
Date: Mon, 8 Jun 2026 17:17:18 +0100
Subject: [PATCH] [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
+...
+*/

_______________________________________________
llvm-branch-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits

Reply via email to