https://github.com/SLTozer updated https://github.com/llvm/llvm-project/pull/203845
>From 264f615aa30ddc31113a02cb9cca49b3a5136dd1 Mon Sep 17 00:00:00 2001 From: Stephen Tozer <[email protected]> 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 _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
