[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
Mon Jun 15 05:48:10 PDT 2026


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

>From 43725474dfe862c21ce3d498eb9d8f24b53029c3 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/ExpectWriter.py     | 61 ++++++++++--
 .../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, 204 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/ExpectWriter.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py
index c6e32e2823435..4fdc2beac494a 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.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
@@ -154,6 +163,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 StepExpectWriter:
     """Processes all active, unknown expects at a given debugger step and produces ExpectedValueWriter results for
     each."""
@@ -171,6 +195,7 @@ def __init__(
         }
         self.expect_value_matches: Dict[Expect, ExpectedValueWriter] = {}
         self.expect_scope_matches: Dict[Expect, ExpectedScopeWriter] = {}
+        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:
@@ -189,6 +214,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}"
@@ -210,19 +237,22 @@ def __init__(self, context: Context, dext_ir: DextIR):
         self.scope_expect_rewrites: Dict[
             Expect, List[Tuple[int, ExpectedScopeWriter]]
         ] = {}
+        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] = {}
         self.missing_expect_rewrites: List[Expect] = []
 
         def collect_expects_to_write(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 = dext_ir.script
@@ -232,7 +262,11 @@ def collect_expects_to_write(expect: Expect, expected_value: Any, scope: Scope):
         script.visit_script(visit_expect=collect_expects_to_write)
 
         # 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()
@@ -256,8 +290,13 @@ def collect_expects_to_write(expect: Expect, expected_value: Any, scope: Scope):
                 self.scope_expect_rewrites[expect].append(
                     (step_idx, expected_scope_writer)
                 )
+            for (
+                expect,
+                line,
+            ) in step_writer.expect_step_matches.items():
+                self.step_expect_rewrites[expect].append((step_idx, line))
 
-        self.new_expected_values = {
+        self.new_expected_values: Dict[Expect, Any] = {
             expect: expected_values
             for expect, expect_writers in self.unknown_expect_rewrites.items()
             if (
@@ -267,6 +306,14 @@ def collect_expects_to_write(expect: Expect, expected_value: Any, scope: Scope):
             )
             is not None
         }
+        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()
+            }
+        )
         self.new_expected_scopes = {
             expect: collect_scope_values([writer for idx, writer in expect_writers])
             for expect, expect_writers in self.scope_expect_rewrites.items()
@@ -365,7 +412,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