[llvm-branch-commits] [llvm] [Dexter] Add at_frame_idx to check values in frames above current (PR #203505)

Stephen Tozer via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Wed Jun 17 07:59:38 PDT 2026


https://github.com/SLTozer updated https://github.com/llvm/llvm-project/pull/203505

>From 6ffe89d4b2a72a0a73116a1bd694485e2129085c Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Fri, 12 Jun 2026 11:57:05 +0100
Subject: [PATCH] [Dexter] Add at_frame_idx to check values in frames above
 current

This patch adds a new attribute for !and nodes, `at_frame_idx`, which
matches against frames above its parent node; for example, in the script:

```
!where {function: foo}:
  !where {function: bar}:
    !and {at_frame_idx: 1}:
      !value x: 0
```

The `!value x` node checks the value of 'x' in 'foo' while the debugger is
inside 'bar'. Use of this attribute comes with some restrictions: a !where
node can never be nested under a !and{at_frame_idx} node, and neither can
another !and{at_frame_idx} node.
---
 .../dexter/dex/debugger/DAP.py                | 15 +++--
 .../dexter/dex/debugger/DebuggerBase.py       |  2 +-
 .../ScriptDebuggerController.py               | 31 ++++++----
 .../dexter/dex/debugger/dbgeng/dbgeng.py      |  2 +-
 .../dexter/dex/debugger/lldb/LLDB.py          |  2 +-
 .../dex/debugger/visualstudio/VisualStudio.py |  2 +-
 .../dexter/dex/dextIR/FrameIR.py              |  3 +
 .../dexter/dex/dextIR/StepIR.py               | 41 ++++++++-----
 .../dexter/dex/evaluation/ExpectWriter.py     | 13 ++--
 .../dexter/dex/evaluation/RunMatch.py         |  5 +-
 .../dexter/dex/evaluation/StateMatch.py       | 21 ++++---
 .../dexter/dex/test_script/Nodes.py           |  6 +-
 .../dexter/dex/test_script/Script.py          | 25 +++++++-
 .../scripts/debugging/then_at_frame.cpp       | 60 ++++++++++++++++++
 .../scripts/debugging/watch_scope.cpp         |  2 +-
 .../scripts/evaluation/eval_at_frame.cpp      | 46 ++++++++++++++
 .../parser/reject-bad-at_frame_idx.test       | 33 ++++++++++
 .../Inputs/rewrite_at_frame_expected.cpp      | 61 +++++++++++++++++++
 .../scripts/rewriting/rewrite_at_frame.cpp    | 49 +++++++++++++++
 19 files changed, 365 insertions(+), 54 deletions(-)
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_at_frame.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_at_frame.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/reject-bad-at_frame_idx.test
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_at_frame_expected.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_at_frame.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 b1e2538ea8891..04675f7409002 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DAP.py
@@ -1016,10 +1016,9 @@ def get_stack_frames(self, step_index: int) -> StepIR:
         )
 
     def collect_watches(
-        self, step: StepIR, watches: List[str], scope_watches: List[str]
+        self, step: StepIR, frame_idx: int, watches: List[str], scope_watches: List[str]
     ):
         """Evaluates the provided watches and stores their evaluation results (ValueIR) in the provided step."""
-        frame_idx = 0
         if not watches and not scope_watches:
             return
         active_exprs = set(watches)
@@ -1055,15 +1054,19 @@ def collect_watches(
                 )
                 self._evaluate_subvariables(value, var["variablesReference"])
                 scope_var_values[value.expression] = value
-            step.scope_watches[scope_name] = list(scope_var_values.keys())
-            for var_name in sorted(step.scope_watches[scope_name]):
-                step.watches[var_name] = scope_var_values[var_name]
+            step.frames[frame_idx].scope_watches[scope_name] = sorted(
+                scope_var_values.keys()
+            )
+            for var_name in sorted(step.frames[frame_idx].scope_watches[scope_name]):
+                step.frames[frame_idx].watches[var_name] = scope_var_values[var_name]
             for expr in list(active_exprs):
                 if expr in scope_var_values:
                     active_exprs.remove(expr)
 
         for expr in active_exprs:
-            step.watches[expr] = self.evaluate_expression(expr, frame_idx)
+            step.frames[frame_idx].watches[expr] = self.evaluate_expression(
+                expr, frame_idx
+            )
 
     @property
     def is_running(self):
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py
index af8e69e75c212..8e1a1487b4d49 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py
@@ -224,7 +224,7 @@ def get_stack_frames(self, step_index: int) -> StepIR:
 
     @abc.abstractmethod
     def collect_watches(
-        self, step: StepIR, watches: List[str], scope_watches: List[str]
+        self, step: StepIR, frame_idx: int, watches: List[str], scope_watches: List[str]
     ):
         pass
 
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ScriptDebuggerController.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ScriptDebuggerController.py
index fc052b5274089..e80dd5405a3a0 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ScriptDebuggerController.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerControllers/ScriptDebuggerController.py
@@ -137,19 +137,24 @@ def _run_debugger_custom(self, cmdline):
                 script, step_info, state_match_context
             )
 
-            watches = [
-                watch
-                for where_match in active_where_matches.values()
-                for expect in where_match.active_expects
-                if (watch := expect.get_watched_expr())
-            ]
-            scope_watches = [
-                scope_watch
-                for where_match in active_where_matches.values()
-                for expect in where_match.active_expects
-                if (scope_watch := expect.get_watched_scope())
-            ]
-            self.debugger.collect_watches(step_info, watches, scope_watches)
+            watches = defaultdict(list)
+            scope_watches = defaultdict(list)
+            for where, where_match in active_where_matches.items():
+                watches[where_match.frame_idx].extend(
+                    watch
+                    for expect in where_match.active_expects
+                    if (watch := expect.get_watched_expr())
+                )
+                scope_watches[where_match.frame_idx].extend(
+                    watch
+                    for expect in where_match.active_expects
+                    if (watch := expect.get_watched_scope())
+                )
+
+            for frame_idx in watches:
+                self.debugger.collect_watches(
+                    step_info, frame_idx, watches[frame_idx], scope_watches[frame_idx]
+                )
 
             active_thens = [
                 then
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py
index 86d5f78f4c556..c32e6df3d0094 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py
@@ -172,7 +172,7 @@ def get_stack_frames(self, step_index: int) -> StepIR:
         raise NotImplementedError("--use-script debugging not supported in dbgeng yet.")
 
     def collect_watches(
-        self, step: StepIR, watches: List[str], scope_watches: List[str]
+        self, step: StepIR, frame_idx: int, watches: List[str], scope_watches: List[str]
     ):
         raise NotImplementedError("--use-script debugging not supported in dbgeng yet.")
 
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 d328498553938..6ab06d066dfe6 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
@@ -323,7 +323,7 @@ def get_stack_frames(self, step_index: int) -> StepIR:
         raise NotImplementedError("--use-script debugging not supported in lldb yet.")
 
     def collect_watches(
-        self, step: StepIR, watches: List[str], scope_watches: List[str]
+        self, step: StepIR, frame_idx: int, watches: List[str], scope_watches: List[str]
     ):
         raise NotImplementedError("--use-script debugging not supported in lldb yet.")
 
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py
index 606e08502fae3..63b0abf681932 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py
@@ -399,7 +399,7 @@ def get_stack_frames(self, step_index: int) -> StepIR:
         )
 
     def collect_watches(
-        self, step: StepIR, watches: List[str], scope_watches: List[str]
+        self, step: StepIR, frame_idx: int, watches: List[str], scope_watches: List[str]
     ):
         raise NotImplementedError(
             "--use-script debugging not supported in visual studio yet."
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/FrameIR.py b/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/FrameIR.py
index 4dd9a8b63ccc7..a34b26a4d0b07 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/FrameIR.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/FrameIR.py
@@ -4,6 +4,7 @@
 # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 # See https://llvm.org/LICENSE.txt for license information.
 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+from collections import OrderedDict
 from typing import Optional
 
 from dex.dextIR.LocIR import LocIR
@@ -23,3 +24,5 @@ def __init__(
         self.is_inlined = is_inlined
         self.loc = loc
         self.instruction_addr = instruction_addr
+        self.watches = {}
+        self.scope_watches = {}
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 639e334f4aa20..fb6fd465f63a1 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/StepIR.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/dextIR/StepIR.py
@@ -51,7 +51,6 @@ def __init__(
         frames: List[FrameIR],
         step_kind: StepKind = None,
         watches: OrderedDict = None,
-        scope_watches: Optional[Dict[str, List[str]]] = None,
         program_state: ProgramState = None,
     ):
         self.step_index = step_index
@@ -66,7 +65,6 @@ def __init__(
         if watches is None:
             watches = {}
         self.watches = watches
-        self.scope_watches = scope_watches or OrderedDict()
         self.hit_where_bps: List[Where] = []
 
     def __str__(self):
@@ -128,18 +126,33 @@ def detailed_print(self) -> List[str]:
             lines.append(f"    {frame.loc}")
             lines.append(f"    $pc = {frame.instruction_addr}")
 
-        if self.scope_watches:
+        frame_scope_watches = {
+            frame_idx: frame.scope_watches
+            for frame_idx, frame in enumerate(self.frames)
+        }
+        if frame_scope_watches:
             lines.append(f"Variable Scopes:")
-            for scope_name in sorted(self.scope_watches):
-                lines.append(
-                    f"  {scope_name}: [{', '.join(self.scope_watches[scope_name])}]"
-                )
-
-        if self.watches:
+            for frame_idx, scope_watches in sorted(frame_scope_watches.items()):
+                frame_str = "" if frame_idx == 0 else f"(Frame {frame_idx}) "
+                for scope_name, scope_vars in scope_watches.items():
+                    lines.append(
+                        f"  {frame_str}{scope_name}: [{', '.join(scope_vars)}]"
+                    )
+
+        frame_watches = {
+            frame_idx: frame.watches for frame_idx, frame in enumerate(self.frames)
+        }
+        if frame_watches:
             lines.append(f"Variables:")
-            for value in sorted(self.watches.values(), key=lambda v: v.expression):
-                if value.sub_values:
-                    value.dump_nested(lines, 1)
-                else:
-                    lines.append(f"  {value}")
+            for frame_idx, watches in frame_watches.items():
+                indent = 1
+                if frame_idx != 0:
+                    lines.append(f"  Frame {frame_idx}:")
+                    indent = 2
+                for value in sorted(watches.values(), key=lambda v: v.expression):
+                    if value.sub_values:
+                        value.dump_nested(lines, indent)
+                    else:
+                        lines.append(f"{'  ' * indent}{value}")
+
         return lines
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py
index a39549d271209..e49ad5c2f35a7 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py
@@ -165,7 +165,7 @@ def __init__(
         self.script = script
         self.state_match = get_active_where_matches(script, step, state_match_context)
         active_expects = {
-            expect
+            expect: where_match.frame_idx
             for where_match in self.state_match.values()
             for expect in where_match.active_expects
         }
@@ -175,14 +175,19 @@ def __init__(
         def add_expected_values(expect: Expect, expected_value: Any, scope: Scope):
             if expect not in active_expects or expected_value is not None:
                 return
+            expect_frame_idx = active_expects[expect]
             if (expr := expect.get_watched_expr()) is not None:
                 self.expect_value_matches[expect] = ExpectedValueWriter(
-                    expect, step.watches[expr]
+                    expect, step.frames[expect_frame_idx].watches[expr]
                 )
             elif (scope_name := expect.get_watched_scope()) is not None:
-                scope_vars = step.scope_watches.get(scope_name, [])
+                scope_vars = step.frames[expect_frame_idx].scope_watches.get(
+                    scope_name, []
+                )
                 self.expect_scope_matches[expect] = ExpectedScopeWriter(
-                    expect, step, [step.watches[var] for var in scope_vars]
+                    expect,
+                    step,
+                    [step.frames[expect_frame_idx].watches[var] for var in scope_vars],
                 )
             else:
                 raise Error(
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 e09cce69e6c28..75f6e6fa81a0b 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py
@@ -46,7 +46,7 @@ def __init__(
         self.match_context = match_context
         self.state_match = get_active_where_matches(script, step, state_match_context)
         expects_to_match = {
-            expect
+            expect: where_match.frame_idx
             for where_match in self.state_match.values()
             for expect in where_match.active_expects
         }
@@ -55,10 +55,11 @@ def __init__(
         def add_expected_values(expect: Expect, expected_value: Any, scope: Scope):
             assert isinstance(expect, Value), "Non-Value expects currently unsupported"
             if expect in expects_to_match:
+                expect_frame_idx = expects_to_match[expect]
                 self.expect_matches[expect] = get_expect_match(
                     expect,
                     expected_value,
-                    step.watches[expect.get_watched_expr()],
+                    step.frames[expect_frame_idx].watches[expect.get_watched_expr()],
                     self.match_context,
                 )
 
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 563ec798e6c83..32301bf468b5e 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py
@@ -149,6 +149,13 @@ def get_active_wheres(where: Where, scope: Scope):
                 # If the target frame is -1, we can't match the !where yet, but we should prepare to step into it.
                 active_where_expects[scope.where].pending_wheres.append(where)
                 return
+            if where.at_frame_idx is not None:
+                # !and {at_frame_idx} is a special case: it cannot contain !where nodes, so there's no point checking it
+                # when the parent !where is not in the current frame (frame_idx=0), and we match its other conditions
+                # against the requested frame index.
+                if target_frame_idx != 0 or where.at_frame_idx >= len(step_info.frames):
+                    return
+                target_frame_idx = where.at_frame_idx
             labels = script.get_labels(
                 expected_file or step_info.frames[target_frame_idx].loc.path
             )
@@ -175,17 +182,15 @@ def get_active_wheres(where: Where, scope: Scope):
     # As we visit the script nodes in pre-order traversal, we can always assume that an expect's parent !where
     # has already been visited, and thus should have an entry in active_where_expects if it is active.
     def get_active_expects(expect: Expect, expected_value, scope: Scope):
-        if (
-            scope.where in active_where_expects
-            and active_where_expects[scope.where].frame_idx == 0
-        ):
+        if scope.where in active_where_expects and active_where_expects[
+            scope.where
+        ].frame_idx == (scope.get_desired_frame_idx() or 0):
             active_where_expects[scope.where].active_expects.append(expect)
 
     def get_active_thens(then: Then, scope: Scope):
-        if (
-            scope.where in active_where_expects
-            and active_where_expects[scope.where].frame_idx == 0
-        ):
+        if scope.where in active_where_expects and active_where_expects[
+            scope.where
+        ].frame_idx == (scope.get_desired_frame_idx() or 0):
             active_where_expects[scope.where].active_thens.append(then)
 
     script.visit_script(
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Nodes.py b/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Nodes.py
index 4381823d052db..a5e27a85fb7e5 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Nodes.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Nodes.py
@@ -61,9 +61,10 @@ def __init__(self, attributes: dict, is_and: bool):
         if isinstance(lines, (int, Label)):
             lines = Line(lines)
         self.lines: Union[Line, DexRange, None] = lines
+        self.at_frame_idx: Optional[int] = attributes.pop("at_frame_idx", None)
         self.after_hit_count: Optional[int] = attributes.pop("after_hit_count", None)
         self.for_hit_count: Optional[int] = attributes.pop("for_hit_count", None)
-        self.conditions: Optional[dict] = attributes.pop("conditions", None)
+        self.conditions: dict = attributes.pop("conditions", None)
         self.is_and = is_and
         if attributes:
             raise DexterNodeError(
@@ -77,6 +78,8 @@ def __init__(self, attributes: dict, is_and: bool):
             raise DexterNodeError(
                 self, "can't check hit counts without an explicit lines or function arg"
             )
+        if self.at_frame_idx is not None and not self.is_and:
+            raise DexterNodeError(self, "at_frame_idx can only be used with !and nodes")
 
     def __repr__(self):
         elts = [
@@ -92,6 +95,7 @@ def get_attrs(self) -> Dict[str, Any]:
             "file": self.file,
             "function": self.function,
             "lines": self.lines.value if isinstance(self.lines, Line) else self.lines,
+            "at_frame_idx": self.at_frame_idx,
             "for_hit_count": self.for_hit_count,
             "after_hit_count": self.after_hit_count,
             "conditions": self.conditions,
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Script.py b/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Script.py
index 25ba27c70dc17..f1d430ea8cf43 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Script.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/test_script/Script.py
@@ -139,6 +139,14 @@ def get_known_file_for_where(self, where: Where) -> Optional[str]:
             next_scope = next_scope.parent_scope
         return next_scope.file
 
+    def get_desired_frame_idx(self) -> Optional[int]:
+        if not (self.where and self.where.is_and):
+            return None
+        if self.where.at_frame_idx is not None:
+            return self.where.at_frame_idx
+        assert self.parent_scope
+        return self.parent_scope.get_desired_frame_idx()
+
 
 class ScriptLoadContext:
     """Contains information about the context that the script was loaded from."""
@@ -179,9 +187,24 @@ def validate_expect(expect: Expect, expected_value, scope: Scope):
                     f"!expect/all node {expect} should not have an expected value."
                 )
 
+        def validate_where(where: Where, scope: Scope):
+            if where.is_and and not scope.where:
+                raise DexterScriptError(
+                    f"!and node must be contained by another state node."
+                )
+            if scope.get_desired_frame_idx() is not None:
+                if not where.is_and:
+                    raise DexterScriptError(
+                        f"!where node {where} cannot be contained by a node with at_frame_idx."
+                    )
+                if where.at_frame_idx:
+                    raise DexterScriptError(
+                        f"!and node {where} with at_frame_idx cannot be contained by another node with at_frame_idx."
+                    )
+
         # `visit_script` will validate the structure of the script, as it traverses the full script and raises an
         # exception if it sees anything unexpected.
-        self.visit_script(visit_expect=validate_expect)
+        self.visit_script(visit_expect=validate_expect, visit_where=validate_where)
 
     # If a truthy value is returned, abort further visiting and return that value.
     def _visit_script(
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_at_frame.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_at_frame.cpp
new file mode 100644
index 0000000000000..91aa278450074
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_at_frame.cpp
@@ -0,0 +1,60 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t \
+// RUN:   -- %s | FileCheck %s --implicit-check-not="(int) -5"
+
+// NB: Test CHECKs use line numbers, update them accordingly if adding/removing
+//     lines in this test.
+
+int flipIt(int Input) {
+  int Result = -Input; // !dex_label flip_start
+  return Result;       // !dex_label flip_ret
+}
+
+int main() {
+  int Value = 5;
+  Value = flipIt(Value);
+  Value = flipIt(Value); // !dex_label second_call
+  Value = flipIt(Value);
+  return Value;
+}
+
+/// Test that when we can use !then under an at_frame_idx state node.
+/// In the second call to flipIt, we trigger `!then step_out` before we reach
+/// the line where we evaluate Input, and so we should only see Input=5.
+
+// CHECK-LABEL: Step 0
+
+// CHECK: flipIt(int)
+// CHECK-NEXT: then_at_frame.cpp(9:
+// CHECK-NOT: flipIt(int)
+
+// CHECK: flipIt(int)
+// CHECK-NEXT: then_at_frame.cpp(10:
+// CHECK: Variables:
+// CHECK: "Input": (int) 5
+// CHECK-NOT: flipIt(int)
+
+// CHECK: flipIt(int)
+// CHECK-NEXT: then_at_frame.cpp(9:
+// CHECK-NOT: flipIt(int)
+
+// CHECK: flipIt(int)
+// CHECK-NEXT: then_at_frame.cpp(9:
+// CHECK-NOT: flipIt(int)
+
+// CHECK: flipIt(int)
+// CHECK-NEXT: then_at_frame.cpp(10:
+// CHECK: Variables:
+// CHECK: "Input": (int) 5
+// CHECK-NOT: flipIt(int)
+
+/*
+---
+!where {function: main}:
+    !where {function: flipIt}:
+        !and {lines: !label flip_ret}:
+            !value Input: 5
+        !and {lines: !label flip_start}:
+            !and {at_frame_idx: 1, lines: !label second_call}: !then step_out
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/watch_scope.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/watch_scope.cpp
index 11710d6591eb7..07ccb1cd5aa9f 100644
--- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/watch_scope.cpp
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/watch_scope.cpp
@@ -18,8 +18,8 @@ int main() {
 // CHECK:      Step 0
 // CHECK:          main
 // CHECK:      Variable Scopes:
-// CHECK-NEXT:   Globals: [::There]
 // CHECK-NEXT:   Locals: [One, Red]
+// CHECK-NEXT:   Globals: [::There]
 // CHECK-NEXT: Variables:
 // CHECK-NEXT:   "::There": (char[5]) "Here"
 // CHECK-NEXT:     "[0]": (char) 'H'
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_at_frame.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_at_frame.cpp
new file mode 100644
index 0000000000000..fa01c7d7c2ab7
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_at_frame.cpp
@@ -0,0 +1,46 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --binary %t -- %s \
+// RUN:   | FileCheck %s
+
+// Test evaluation of !and{at_frame_idx} nodes in Dexter.
+
+// CHECK: total_watched_steps: 18
+// CHECK: correct_steps: 18
+// CHECK: incorrect_steps: 0
+// CHECK: missing_var_steps: 0
+// CHECK: unexpected_value_steps: 0
+// CHECK: seen_values: 8
+// CHECK: missing_values: 0
+
+int Global = 4;
+
+int bar(int Z) {
+  Global *= 2;
+  return Z / 2;
+}
+
+int foo(int Y) {
+  int First = bar(Y);
+  return First + bar(Y * 2) * 2; // !dex_label second_call
+}
+
+int main() {
+  int X = 9;
+  return foo(X + 1); // !dex_label root_call
+}
+
+/*
+---
+!where {function: main}:
+  !where {function: foo}:
+    !where {function: bar}:
+      !value Z: [10, 20]
+      !and {at_frame_idx: 1}:
+        !value Y: 10
+        !and {lines: !label second_call}:
+            !value First: 5
+      !and {at_frame_idx: 2, lines: !label root_call}:
+        !value X: 9
+        !value Global: [4, 8, 16]
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/reject-bad-at_frame_idx.test b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/reject-bad-at_frame_idx.test
new file mode 100644
index 0000000000000..2edf4139322e6
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/reject-bad-at_frame_idx.test
@@ -0,0 +1,33 @@
+RUN: not %dexter_regression_test_run --binary %s --use-script --skip-run -- %s 2>&1 | FileCheck %s
+
+Tests that we reject invalid at_frame_idx uses.
+
+CHECK: No valid Dexter script found in file
+
+CHECK: Script starting line [[# @LINE + 2]]:
+CHECK: Error with node: Where(function=bar, at_frame_idx=1): at_frame_idx can only be used with !and nodes
+---
+!where {function: foo}:
+    !where {function: bar, at_frame_idx: 1}:
+        !value a: 0
+...
+
+CHECK: Script starting line [[# @LINE + 2]]:
+CHECK: !where node Where(function=baz) cannot be contained by a node with at_frame_idx.
+---
+!where {function: foo}:
+    !where {function: bar}:
+        !and {at_frame_idx: 1}:
+            !where {function: baz}:
+                !value b: 1
+...
+
+CHECK: Script starting line [[# @LINE + 2]]:
+CHECK: !and node And(at_frame_idx=2) with at_frame_idx cannot be contained by another node with at_frame_idx.
+---
+!where {function: foo}:
+    !where {function: bar}:
+        !and {at_frame_idx: 1}:
+            !and {at_frame_idx: 2}:
+                !value c: 2
+...
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_at_frame_expected.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_at_frame_expected.cpp
new file mode 100644
index 0000000000000..b871b07896df1
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_at_frame_expected.cpp
@@ -0,0 +1,61 @@
+// 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_at_frame_expected.cpp
+
+/// Tests that we can rewrite variables and scopes at frames above the current
+/// frame.
+
+/// 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.
+
+int ChangeCount = 0;
+
+void setVariable(int &Var, int NewValue) {
+  Var = NewValue;
+  ChangeCount += 1;
+  return;
+}
+
+int main() {
+  int X = 1;
+  int Y = 2;
+  int Z = 3;
+  setVariable(X, 9);
+  setVariable(Y, 8);
+  setVariable(Z, 7);
+  return 0;
+}
+
+// CHECK: total_watched_steps: 36
+// CHECK: correct_steps: 36
+// CHECK: incorrect_steps: 0
+// CHECK: missing_var_steps: 0
+// CHECK: unexpected_value_steps: 0
+// CHECK: seen_values: 10
+// CHECK: missing_values: 0
+
+/*
+---
+? !where {function: setVariable}
+: ? !and {at_frame_idx: 1}
+  : !value 'X':
+    - '1'
+    - '9'
+    !value 'Y':
+    - '2'
+    - '8'
+    !value 'Z':
+    - '3'
+    - '7'
+    !value 'ChangeCount':
+    - '0'
+    - '1'
+    - '2'
+    - '3'
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_at_frame.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_at_frame.cpp
new file mode 100644
index 0000000000000..48ce084769a21
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_at_frame.cpp
@@ -0,0 +1,49 @@
+// 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_at_frame_expected.cpp
+
+/// Tests that we can rewrite variables and scopes at frames above the current
+/// frame.
+
+/// 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.
+
+int ChangeCount = 0;
+
+void setVariable(int &Var, int NewValue) {
+  Var = NewValue;
+  ChangeCount += 1;
+  return;
+}
+
+int main() {
+  int X = 1;
+  int Y = 2;
+  int Z = 3;
+  setVariable(X, 9);
+  setVariable(Y, 8);
+  setVariable(Z, 7);
+  return 0;
+}
+
+// CHECK: total_watched_steps: 36
+// CHECK: correct_steps: 36
+// CHECK: incorrect_steps: 0
+// CHECK: missing_var_steps: 0
+// CHECK: unexpected_value_steps: 0
+// CHECK: seen_values: 10
+// CHECK: missing_values: 0
+
+/*
+---
+!where {function: setVariable}:
+  !and {at_frame_idx: 1}:
+    ? !value/all Locals
+    ? !value ChangeCount
+...
+*/



More information about the llvm-branch-commits mailing list