[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