[llvm-branch-commits] [llvm] [Dexter] Add support for aggregate expects in the debugger (PR #202545)

Orlando Cazalet-Hyams via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Mon Jun 15 03:25:16 PDT 2026


================
@@ -6,33 +6,198 @@
 # 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):
     """Given one or more expected values for an Expect node and an actual ValueIR, returns a match for the first
     matching expected values, or for None if there are no matching expected values."""
----------------
OCHyams wrote:

Perhaps worth updating the docstring to explain partial match behaviour

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


More information about the llvm-branch-commits mailing list