[llvm-branch-commits] [llvm] [Dexter] Add !then node (PR #202546)
Stephen Tozer via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Tue Jun 16 07:12:15 PDT 2026
https://github.com/SLTozer updated https://github.com/llvm/llvm-project/pull/202546
>From a7236c3feb7b07bed718a4b6598c10b467028a18 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Mon, 8 Jun 2026 19:32:46 +0100
Subject: [PATCH 1/3] [Dexter] Add !then node
In order to exercise more control over stepping in Dexter tests, this patch
adds the `!then` node which can be used to step out of a function or exit
the current test. Unlike expect nodes, !then nodes appear as direct singular
children of a state node:
!where {lines: 10}: !then finish
The two currently available commands are "step_out" and "finish". step_out
performs a debugger "step out" command, skipping over all !wheres in the
current frame and not stepping into any lower !wheres. The finish command
ends the debugger session immediately after finishing the current step.
---
.../ScriptDebuggerController.py | 26 +++++++---
.../dexter/dex/evaluation/StateMatch.py | 17 +++++-
.../dexter/dex/test_script/Nodes.py | 40 ++++++++++++++
.../dexter/dex/test_script/Script.py | 9 +++-
.../scripts/debugging/then_finish.cpp | 52 +++++++++++++++++++
.../scripts/debugging/then_step_out.cpp | 46 ++++++++++++++++
.../scripts/parser/invalid-script-nodes.test | 6 +++
.../scripts/parser/valid-parse.test | 3 ++
8 files changed, 189 insertions(+), 10 deletions(-)
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_finish.cpp
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_step_out.cpp
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 54e924edd40c2..1b42a679e3b4f 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
@@ -130,11 +130,24 @@ def _run_debugger_custom(self, cmdline):
]
self.debugger.collect_watches(step_info, watches)
+ active_thens = [
+ then
+ for where_match in active_where_matches.values()
+ for then in where_match.active_thens
+ ]
+ should_step_out = any(then.command == "step_out" for then in active_thens)
+ should_finish = any(then.command == "finish" for then in active_thens)
+
# Our stepping behaviour is as follows:
- # - If any !where matches the current stack frame, we step.
+ # - If any !then is active, we follow its command.
+ # - Otherwise, if any !where matches the current stack frame, we step.
# - Otherwise, if any !where matches any non-current stack frame, we step out.
# - Otherwise, we continue.
- if any(
+ if should_finish:
+ next_action = DebuggerAction.EXIT
+ elif should_step_out:
+ next_action = DebuggerAction.STEP_OUT
+ elif any(
where_match.frame_idx == 0
for where_match in active_where_matches.values()
):
@@ -155,14 +168,15 @@ def _run_debugger_custom(self, cmdline):
if (
bp_ids
and where not in script.root_wheres
- and where not in pending_wheres
+ and (where not in pending_wheres or should_step_out)
):
bp_to_delete.extend(bp_ids)
bp_ids.clear()
self.debugger.delete_breakpoints(bp_to_delete)
- for where in pending_wheres:
- if not self._where_bps[where]:
- self.add_where_entry_bp(where)
+ if not should_step_out:
+ for where in pending_wheres:
+ if not self._where_bps[where]:
+ self.add_where_entry_bp(where)
if step_info.current_frame:
self._step_index += 1
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 bc6309f4d117e..aeb7100e16ff9 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py
@@ -13,7 +13,7 @@
from dex.dextIR import FrameIR, StepIR
from dex.test_script import DexterScript, Scope
-from dex.test_script.Nodes import Expect, FileLabels, Value, Where
+from dex.test_script.Nodes import Expect, FileLabels, Value, Where, Then
def is_subpath(subpath: str, superpath: str) -> bool:
@@ -61,6 +61,7 @@ class WhereMatchResult:
frame_idx: int
active_expects: List[Value] = field(default_factory=list)
+ active_thens: List[Then] = field(default_factory=list)
pending_wheres: List[Where] = field(default_factory=list)
@@ -118,6 +119,18 @@ def get_active_expects(expect: Expect, expected_value, scope: Scope):
):
active_where_expects[scope.where].active_expects.append(expect)
- script.visit_script(visit_where=get_active_wheres, visit_expect=get_active_expects)
+ def get_active_thens(then: Then, scope: Scope):
+ if (
+ scope.where in active_where_expects
+ and active_where_expects[scope.where].frame_idx == 0
+ ):
+ print(scope.where)
+ active_where_expects[scope.where].active_thens.append(then)
+
+ script.visit_script(
+ visit_where=get_active_wheres,
+ visit_expect=get_active_expects,
+ visit_then=get_active_thens,
+ )
return active_where_expects
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 0eb5e2de9ab64..a690fcd98ec1b 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
@@ -23,6 +23,7 @@ def setup_yaml_parser(loader):
Value,
DexRange,
Label,
+ Then,
]
for c in reg_classes:
c.register_yaml(loader)
@@ -192,6 +193,45 @@ def register_yaml(loader):
yaml.add_representer(Value, Value.representer)
+##############
+## Execution Nodes: Can appear as leaf nodes directly under a state node to perform debugger actions when they become
+## active, to advance the debugger state.
+
+
+class Then:
+ """Used to perform actions, such as finishing the test or continuing. Will trigger as soon as it becomes active, and
+ intends to advance debugger state, so this must be used directly under a state node, e.g.:
+ `!where {line: 4}: !then finish`
+ """
+
+ def __init__(self, command: str):
+ self.command = command
+ if not self.is_valid():
+ raise DexterNodeError(self, f'Invalid !then command "{self.command}"')
+
+ def is_valid(self) -> bool:
+ valid_commands = ["finish", "step_out"]
+ if self.command not in valid_commands:
+ return False
+ return True
+
+ def __repr__(self):
+ return f"Then({self.command})"
+
+ @staticmethod
+ def constructor(loader, node):
+ return Then(loader.construct_scalar(node))
+
+ @staticmethod
+ def representer(dumper, data):
+ return dumper.represent_scalar("!then", data.command)
+
+ @staticmethod
+ def register_yaml(loader):
+ yaml.add_constructor("!then", Then.constructor, loader)
+ yaml.add_representer(Then, Then.representer)
+
+
##############
## Utility Nodes: Can be used anywhere in a script as a form of syntactic sugar.
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 b21998f9708d9..26a38a604dbe5 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
@@ -20,6 +20,7 @@
Expect,
FileLabels,
Where,
+ Then,
setup_yaml_parser,
)
@@ -176,7 +177,10 @@ def do(visitor, *args):
if result := do(visit_where, key, scope):
return result
new_scope = scope.add_where(key)
- if result := self._visit_script(
+ if isinstance(value, Then):
+ if result := do(visit_then, value, new_scope):
+ return result
+ elif result := self._visit_script(
value, new_scope, visit_where, visit_expect, visit_then
):
return result
@@ -191,6 +195,7 @@ def visit_script(
self,
visit_where: Optional[Callable[[Where, Scope], Any]] = None,
visit_expect: Optional[Callable[[Expect, Any, Scope], Any]] = None,
+ visit_then: Optional[Callable[[Then, Scope], Any]] = None,
) -> Any:
"""Visits all nodes in the script in pre-order traversal, calling any non-none provided visitor functions for
each respective node type. Note that we do not visit expected values independently of their associated expect;
@@ -199,7 +204,7 @@ def visit_script(
If any visit function returns a truthy value, traversal will early-exit and this function returns that value;
otherwise, this function returns None."""
return self._visit_script(
- self.script_obj, self.root_scope, visit_where, visit_expect
+ self.script_obj, self.root_scope, visit_where, visit_expect, visit_then
)
@property
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_finish.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_finish.cpp
new file mode 100644
index 0000000000000..7d4fcb4938a8c
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_finish.cpp
@@ -0,0 +1,52 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t -- %s | FileCheck %s
+
+/// Test that when we use !then finish, we finish the entire test immediately,
+/// without observing any more steps afterwards.
+
+void fizz() {}
+void buzz() {}
+void fizzbuzz() {}
+
+void doFizzbuzz(int N) {
+// CHECK: then_finish.cpp([[# @LINE + 1 ]]:14)
+ for (int I = 1; I < N; ++I) {
+// CHECK-COUNT-15: then_finish.cpp([[# @LINE + 1 ]]:13)
+ if (I % 3 == 0) { // !dex_label loop_top
+ if (I % 5 == 0)
+// CHECK: then_finish.cpp([[# @LINE + 1 ]]:17)
+ fizzbuzz(); // !dex_label fizzbuzz
+ else
+ fizz();
+ } else if (I % 5 == 0) {
+ buzz();
+ }
+ }
+}
+/// We'll see main in "Frame 1" at the same step that we exit from; we should
+/// not see it (or doFizzbuzz) afterwards.
+// CHECK: Frame 1:
+// CHECK-NEXT: main
+// CHECK-NOT: main
+// CHECK-NOT: doFizzbuzz
+
+int main() {
+ int V = 0;
+ doFizzbuzz(30); // !dex_label main_start
+ V = 1;
+ return 0; // !dex_label main_end
+}
+
+/*
+---
+!where {function: main}:
+ !where {function: doFizzbuzz}:
+ !and {lines: !label loop_top}:
+ !value I: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
+ !and {lines: !label fizzbuzz}: !then finish
+ !and {lines: !range [!label main_start, !label main_end]}:
+ !value V: [0, 1]
+!where {lines: !label main_end}:
+ !value V: 1
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_step_out.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_step_out.cpp
new file mode 100644
index 0000000000000..d2450b4593ca1
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_step_out.cpp
@@ -0,0 +1,46 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t -- %s | FileCheck %s
+
+/// Test that when we use !then step_out, we jump out of the current frame, but
+/// continue stepping through the frame above.
+
+void fizz() {}
+void buzz() {}
+void fizzbuzz() {}
+
+void doFizzbuzz(int N) {
+// CHECK: then_step_out.cpp([[# @LINE + 1 ]]:14)
+ for (int I = 1; I < N; ++I) {
+// CHECK-COUNT-15: then_step_out.cpp([[# @LINE + 1 ]]:13)
+ if (I % 3 == 0) { // !dex_label loop_top
+ if (I % 5 == 0)
+// CHECK: then_step_out.cpp([[# @LINE + 1 ]]:17)
+ fizzbuzz(); // !dex_label fizzbuzz
+ else
+ fizz();
+ } else if (I % 5 == 0) {
+ buzz();
+ }
+ }
+}
+// CHECK-NOT: doFizzbuzz
+
+int main() {
+ int V = 0;
+ doFizzbuzz(30); // !dex_label main_start
+ V = 1;
+// CHECK: then_step_out.cpp([[# @LINE + 1 ]]:3)
+ return 0; // !dex_label main_end
+}
+
+/*
+---
+!where {function: main}:
+ !where {function: doFizzbuzz}:
+ !and {lines: !label loop_top}:
+ !value I: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
+ !and {lines: !label fizzbuzz}: !then step_out
+ !and {lines: !range [!label main_start, !label main_end]}:
+ !value V: [0, 1]
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/invalid-script-nodes.test b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/invalid-script-nodes.test
index 6989f475a11e5..993b8998a99e0 100644
--- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/invalid-script-nodes.test
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/invalid-script-nodes.test
@@ -22,3 +22,9 @@ CHECK-NEXT: Script starting line [[# @LINE + 1 ]]
!where {function: foo}:
- !value x: 5
...
+
+CHECK-NEXT: Script starting line [[# @LINE + 1 ]]
+---
+# CHECK-NEXT: Invalid !then command "bar"
+!where {function: foo}: !then bar
+...
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/valid-parse.test b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/valid-parse.test
index 9a57df7c90175..f4da3bff9a1ef 100644
--- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/valid-parse.test
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/parser/valid-parse.test
@@ -6,6 +6,8 @@ CHECK: ? !where {function: foo}
CHECK-NEXT: : !value 'x': 5
CHECK-NEXT: ? !where {file: lib.cpp, lines: !range [10, 20]}
CHECK-NEXT: : !value 'y': 10
+CHECK-NEXT: ? !and {lines: 14}
+CHECK-NEXT: : !then 'finish'
CHECK-NEXT: ? !where {lines: 5}
CHECK-NEXT: : ? !and {lines: 5}
CHECK-NEXT: : !value 'y': 7
@@ -17,6 +19,7 @@ CHECK-NEXT: !value 'z': bees
!value x: 5
!where {file: "lib.cpp", lines: !range [10, 20]}:
!value y: 10
+ !and {lines: 14}: !then finish
!where {lines: 5}:
!and {lines: 5}:
!value y: 7
>From 134de02e4991c8dcb8b52b7af942fb5a9ea05401 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Wed, 10 Jun 2026 10:28:40 +0100
Subject: [PATCH 2/3] Remove debug print
---
.../debuginfo-tests/dexter/dex/evaluation/StateMatch.py | 1 -
1 file changed, 1 deletion(-)
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 aeb7100e16ff9..2be87590b978c 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py
@@ -124,7 +124,6 @@ def get_active_thens(then: Then, scope: Scope):
scope.where in active_where_expects
and active_where_expects[scope.where].frame_idx == 0
):
- print(scope.where)
active_where_expects[scope.where].active_thens.append(then)
script.visit_script(
>From 593491154cbcfe7242729799a51b127823f69113 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Wed, 10 Jun 2026 13:22:17 +0100
Subject: [PATCH 3/3] format
---
.../scripts/debugging/then_finish.cpp | 27 ++++++++---------
.../scripts/debugging/then_step_out.cpp | 29 ++++++++++---------
2 files changed, 29 insertions(+), 27 deletions(-)
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_finish.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_finish.cpp
index 7d4fcb4938a8c..44c580d95545d 100644
--- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_finish.cpp
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_finish.cpp
@@ -1,5 +1,6 @@
// RUN: %dexter_regression_test_cxx_build %s -o %t
-// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t -- %s | FileCheck %s
+// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t \
+// RUN: -- %s | FileCheck %s
/// Test that when we use !then finish, we finish the entire test immediately,
/// without observing any more steps afterwards.
@@ -9,19 +10,19 @@ void buzz() {}
void fizzbuzz() {}
void doFizzbuzz(int N) {
-// CHECK: then_finish.cpp([[# @LINE + 1 ]]:14)
- for (int I = 1; I < N; ++I) {
-// CHECK-COUNT-15: then_finish.cpp([[# @LINE + 1 ]]:13)
- if (I % 3 == 0) { // !dex_label loop_top
- if (I % 5 == 0)
-// CHECK: then_finish.cpp([[# @LINE + 1 ]]:17)
- fizzbuzz(); // !dex_label fizzbuzz
- else
- fizz();
- } else if (I % 5 == 0) {
- buzz();
- }
+ // CHECK: then_finish.cpp([[# @LINE + 1 ]]:12)
+ for (int I = 1; I < N; ++I) {
+ // CHECK-COUNT-15: then_finish.cpp([[# @LINE + 1 ]]:9)
+ if (I % 3 == 0) { // !dex_label loop_top
+ if (I % 5 == 0)
+ // CHECK: then_finish.cpp([[# @LINE + 1 ]]:9)
+ fizzbuzz(); // !dex_label fizzbuzz
+ else
+ fizz();
+ } else if (I % 5 == 0) {
+ buzz();
}
+ }
}
/// We'll see main in "Frame 1" at the same step that we exit from; we should
/// not see it (or doFizzbuzz) afterwards.
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_step_out.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_step_out.cpp
index d2450b4593ca1..c8b351f0ccf5f 100644
--- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_step_out.cpp
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/debugging/then_step_out.cpp
@@ -1,5 +1,6 @@
// RUN: %dexter_regression_test_cxx_build %s -o %t
-// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t -- %s | FileCheck %s
+// RUN: %dexter_regression_test_run --use-script --skip-evaluate --binary %t \
+// RUN: -- %s | FileCheck %s
/// Test that when we use !then step_out, we jump out of the current frame, but
/// continue stepping through the frame above.
@@ -9,19 +10,19 @@ void buzz() {}
void fizzbuzz() {}
void doFizzbuzz(int N) {
-// CHECK: then_step_out.cpp([[# @LINE + 1 ]]:14)
- for (int I = 1; I < N; ++I) {
-// CHECK-COUNT-15: then_step_out.cpp([[# @LINE + 1 ]]:13)
- if (I % 3 == 0) { // !dex_label loop_top
- if (I % 5 == 0)
-// CHECK: then_step_out.cpp([[# @LINE + 1 ]]:17)
- fizzbuzz(); // !dex_label fizzbuzz
- else
- fizz();
- } else if (I % 5 == 0) {
- buzz();
- }
+ // CHECK: then_step_out.cpp([[# @LINE + 1 ]]:12)
+ for (int I = 1; I < N; ++I) {
+ // CHECK-COUNT-15: then_step_out.cpp([[# @LINE + 1 ]]:9)
+ if (I % 3 == 0) { // !dex_label loop_top
+ if (I % 5 == 0)
+ // CHECK: then_step_out.cpp([[# @LINE + 1 ]]:9)
+ fizzbuzz(); // !dex_label fizzbuzz
+ else
+ fizz();
+ } else if (I % 5 == 0) {
+ buzz();
}
+ }
}
// CHECK-NOT: doFizzbuzz
@@ -29,7 +30,7 @@ int main() {
int V = 0;
doFizzbuzz(30); // !dex_label main_start
V = 1;
-// CHECK: then_step_out.cpp([[# @LINE + 1 ]]:3)
+ // CHECK: then_step_out.cpp([[# @LINE + 1 ]]:3)
return 0; // !dex_label main_end
}
More information about the llvm-branch-commits
mailing list