[llvm-branch-commits] [llvm] [Dexter] Add support for writing !step values (PR #203845)
Stephen Tozer via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Wed Jun 24 07:30:08 PDT 2026
https://github.com/SLTozer updated https://github.com/llvm/llvm-project/pull/203845
>From e73651933b3a44e00341da308c902af55b7f29e3 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Fri, 12 Jun 2026 17:25:12 +0100
Subject: [PATCH] [Dexter] Add support for writing !step values
Following from the previous patch, this patch adds support to Dexter for
generating expected values for !step nodes. This is relatively limited:
the kind of !step which this is most well-suited to this is !step exactly,
as the !step order of ignoring extra lines is redundant (all lines are added
as expected values), and !step never can't know what lines could have been
stepped on but weren't without some extra work (e.g. finding viable
breakpoint locations in the enclosing state node).
---
.../dexter/dex/evaluation/ExpectRewriter.py | 62 ++++++++++--
.../dexter/dex/test_script/Script.py | 4 +-
.../rewriting/Inputs/rewrite_step_lines.cpp | 31 ++++++
.../Inputs/rewrite_step_lines_expected.cpp | 97 +++++++++++++++++++
.../scripts/rewriting/rewrite_step_lines.test | 21 ++++
5 files changed, 205 insertions(+), 10 deletions(-)
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_step_lines.cpp
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_step_lines_expected.cpp
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_step_lines.test
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectRewriter.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectRewriter.py
index 45fc4ec18e51e..265cc7b1bb7ec 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectRewriter.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectRewriter.py
@@ -11,7 +11,16 @@
from dex.dextIR import DextIR, StepIR, ValueIR
from dex.evaluation.StateMatch import StateMatchContext, get_active_where_matches
-from dex.test_script.Nodes import DexRange, Expect, Line, Then, Value, ValueAll, Where
+from dex.test_script.Nodes import (
+ DexRange,
+ Expect,
+ Line,
+ Step,
+ Then,
+ Value,
+ ValueAll,
+ Where,
+)
from dex.test_script.Script import DexterScript, Scope
from dex.tools.Main import Context
from dex.utils.Exceptions import Error
@@ -156,6 +165,21 @@ def collect_scope_values(
return per_range_var_unique_expected_values
+def get_expected_lines(expect: Expect, lines: List[int]) -> List[int]:
+ """For a !step expect and the list of lines seen while that expect was active, returns the list of lines that should
+ be expected by that expect."""
+ assert isinstance(expect, Step), "Trying to get expected lines for non-step node?"
+ if expect.kind == "never":
+ # We can't really get useful "expected values" for a !step never node, unless we throw in some convoluted extra
+ # steps, e.g. finding all breakpoint locations within the expect's enclosing scope, and creating a list of all
+ # lines that have valid breakpoint locations but weren't seen.
+ return []
+ # Although !step order and !step exactly are evaluated differently, they both aim to match lines stepped on; since
+ # we don't have any meaningful reason to exclude any seen lines from the written expected line list, we just use the
+ # whole thing.
+ return lines
+
+
class StepExpectRewriter:
"""Processes all active, unknown expects at a given debugger step and produces ExpectedValueRewriter results for
each."""
@@ -173,6 +197,7 @@ def __init__(
}
self.expect_value_matches: Dict[Expect, ExpectedValueRewriter] = {}
self.expect_scope_matches: Dict[Expect, ExpectedScopeRewriter] = {}
+ self.expect_step_matches: Dict[Expect, int] = {}
def add_expected_values(expect: Expect, expected_value: Any, scope: Scope):
if expect not in active_expects or expected_value is not None:
@@ -191,6 +216,8 @@ def add_expected_values(expect: Expect, expected_value: Any, scope: Scope):
step,
[step.frames[expect_frame_idx].watches[var] for var in scope_vars],
)
+ elif isinstance(expect, Step):
+ self.expect_step_matches[expect] = step.current_location.lineno
else:
raise Error(
f"Unexpected expect without watched expression or scope: {expect}"
@@ -212,6 +239,7 @@ def __init__(self, context: Context, dext_ir: DextIR):
self.scope_expect_rewrites: Dict[
Expect, List[Tuple[int, ExpectedScopeRewriter]]
] = {}
+ self.step_expect_rewrites: Dict[Expect, List[Tuple[int, int]]] = {}
self.new_script: Optional[DexterScript] = None
self.new_expected_values: Dict[Expect, Any] = {}
self.new_expected_scopes: Dict[Expect, ExpectedScopeRewrites] = {}
@@ -227,19 +255,25 @@ def __init__(self, context: Context, dext_ir: DextIR):
def collect_expects_to_rewrite(
expect: Expect, expected_value: Any, scope: Scope
):
+ if expected_value is not None:
+ return
if isinstance(expect, ValueAll):
- assert expected_value is None
self.scope_expect_rewrites[expect] = []
return
- if expected_value is not None:
+ if isinstance(expect, Step):
+ self.step_expect_rewrites[expect] = []
return
- assert isinstance(expect, Value), "Non-Value expects currently unsupported"
+ assert isinstance(expect, Value), f"Unexpected expect node kind {expect}"
self.unknown_expect_rewrites[expect] = []
script.visit_script(visit_expect=collect_expects_to_rewrite)
# If there are no expects to update, then there is no rewriting to be done - exit early.
- if not self.unknown_expect_rewrites and not self.scope_expect_rewrites:
+ if (
+ not self.unknown_expect_rewrites
+ and not self.scope_expect_rewrites
+ and not self.step_expect_rewrites
+ ):
return
state_match_context = StateMatchContext()
@@ -266,10 +300,15 @@ def collect_expects_to_rewrite(
self.scope_expect_rewrites[expect].append(
(step_idx, expected_scope_rewriter)
)
+ for (
+ expect,
+ line,
+ ) in step_rewriter.expect_step_matches.items():
+ self.step_expect_rewrites[expect].append((step_idx, line))
# For each unknown expect, merge the observed values into a writable "expected values" entry, which may be a
# list or a single value.
- self.new_expected_values = {
+ self.new_expected_values: Dict[Expect, Any] = {
expect: expected_values
for expect, expect_rewriters in self.unknown_expect_rewrites.items()
if (
@@ -279,6 +318,15 @@ def collect_expects_to_rewrite(
)
is not None
}
+ # Do the same for unknown step expects.
+ self.new_expected_values.update(
+ {
+ expect: get_expected_lines(
+ expect, [line for step_index, line in step_lines]
+ )
+ for expect, step_lines in self.step_expect_rewrites.items()
+ }
+ )
# Do the same for unknown scope expects.
self.new_expected_scopes = {
expect: collect_scope_values(
@@ -382,7 +430,7 @@ def replace_expect(expect: Expect, expected_value, scope: Scope):
new_expect_sibling_list.append(new_expect)
new_node_child_map[new_expect] = expected_values
return
- assert isinstance(expect, Value)
+ assert isinstance(expect, (Step, Value))
new_expected_value = add_expected_values.get(expect) or expected_value
new_node_child_map[expect] = new_expected_value
scope_where_children.append(expect)
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 5546d46527cb9..c5c44d295b4af 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
@@ -188,9 +188,7 @@ def validate_expect(expect: Expect, expected_value, scope: Scope):
raise DexterScriptError(
f"!expect/all node {expect} should not have an expected value."
)
- if isinstance(expect, Step):
- if expected_value is None:
- raise DexterScriptError(f"rewriting !step nodes not yet supported.")
+ if isinstance(expect, Step) and expected_value is not None:
if not (
isinstance(expected_value, list)
and all(isinstance(l, (int, Label)) for l in expected_value)
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_step_lines.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_step_lines.cpp
new file mode 100644
index 0000000000000..4e1ea6b1798ec
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_step_lines.cpp
@@ -0,0 +1,31 @@
+/// NB: The exact contents of this file are compared against the corresponding
+/// "_expected" file in this directory; any changes to this file, including
+/// comments, will require updating the expected file.
+
+int main() {
+ int Go = 0;
+ int Times = 0;
+start:
+ Times += 1;
+ for (int I = 0; I < 3; ++I) {
+ if (I == 1)
+ Go += 1;
+ if (I > 1 && Go % 2)
+ goto start; // !dex_label first_goto
+ }
+ if (Times < 4)
+ goto start; // !dex_label second_goto
+ return 0;
+}
+
+/*
+---
+!where {function: main}:
+ ? !step exactly
+ # For test clarity: we step on the first goto 2 times, and the second once.
+ !and {lines: !label first_goto}:
+ !step exactly: [!label first_goto, !label first_goto]
+ !and {lines: !label second_goto}:
+ !step exactly: [!label second_goto]
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_step_lines_expected.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_step_lines_expected.cpp
new file mode 100644
index 0000000000000..5154962238544
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_step_lines_expected.cpp
@@ -0,0 +1,97 @@
+/// NB: The exact contents of this file are compared against the corresponding
+/// "_expected" file in this directory; any changes to this file, including
+/// comments, will require updating the expected file.
+
+int main() {
+ int Go = 0;
+ int Times = 0;
+start:
+ Times += 1;
+ for (int I = 0; I < 3; ++I) {
+ if (I == 1)
+ Go += 1;
+ if (I > 1 && Go % 2)
+ goto start; // !dex_label first_goto
+ }
+ if (Times < 4)
+ goto start; // !dex_label second_goto
+ return 0;
+}
+
+/*
+---
+? !where {function: main}
+: !step 'exactly':
+ - 6
+ - 7
+ - 9
+ - 10
+ - 11
+ - 13
+ - 15
+ - 10
+ - 11
+ - 12
+ - 13
+ - 15
+ - 10
+ - 11
+ - 13
+ - 14
+ - 9
+ - 10
+ - 11
+ - 13
+ - 15
+ - 10
+ - 11
+ - 12
+ - 13
+ - 15
+ - 10
+ - 11
+ - 13
+ - 15
+ - 10
+ - 16
+ - 17
+ - 9
+ - 10
+ - 11
+ - 13
+ - 15
+ - 10
+ - 11
+ - 12
+ - 13
+ - 15
+ - 10
+ - 11
+ - 13
+ - 14
+ - 9
+ - 10
+ - 11
+ - 13
+ - 15
+ - 10
+ - 11
+ - 12
+ - 13
+ - 15
+ - 10
+ - 11
+ - 13
+ - 15
+ - 10
+ - 16
+ - 18
+ ? !and {lines: !label 'first_goto'}
+ : !step 'exactly':
+ - !label 'first_goto'
+ - !label 'first_goto'
+ ? !and {lines: !label 'second_goto'}
+ : !step 'exactly':
+ - !label 'second_goto'
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_step_lines.test b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_step_lines.test
new file mode 100644
index 0000000000000..a67664c96d61d
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_step_lines.test
@@ -0,0 +1,21 @@
+RUN: rm -rf %t
+RUN: mkdir %t
+RUN: %dexter_regression_test_cxx_build %S/Inputs/rewrite_step_lines.cpp \
+RUN: -o %t/test
+RUN: %dexter_regression_test_run --use-script --binary %t/test \
+RUN: --results-directory %t/results -- %S/Inputs/rewrite_step_lines.cpp 2>&1 \
+RUN: | FileCheck %s
+RUN: diff %t/results/rewrite_step_lines.cpp \
+RUN: %S/Inputs/rewrite_step_lines_expected.cpp
+
+Test that Dexter can rewrite expected values for !step nodes.
+
+CHECK: Rewrote script to add 1 expected values.
+
+CHECK: total_line_steps: 67
+CHECK: correct_line_steps: 67
+CHECK: correct_line_score: 100.0% (67/67)
+CHECK: misordered_line_steps: 0
+CHECK: missing_lines: 0
+CHECK: incorrect_line_steps: 0
+CHECK: unexpected_lines: 0
More information about the llvm-branch-commits
mailing list