[llvm-branch-commits] [llvm] [Dexter] Add ability to rewrite scripts to fill-in unknown values (PR #202799)

Stephen Tozer via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Mon Jun 15 05:42:19 PDT 2026


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

>From a40271fe2f46912f757cafcf9fb95724e3dc2b9f Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Tue, 9 Jun 2026 15:40:46 +0100
Subject: [PATCH 1/2] [Dexter] Add ability to rewrite scripts to fill-in
 unknown values

This patch adds a feature to Dexter that allows scripts to be passed to
Dexter with missing expected values (`null` values in YAML), which Dexter
will attempt to "fill-in" with expected values that match the debugger's
actual output. The result is written to a file with the same name as the
original test file, in the directory given by --results-directory if one
is present; all content outside of the Dexter script itself is preserved
exactly as-is.
---
 .../dexter/dex/evaluation/ExpectWriter.py     | 201 ++++++++++++++++++
 .../dexter/dex/test_script/Script.py          |  26 +++
 .../dexter/dex/tools/ToolBase.py              |   3 +-
 .../dexter/dex/tools/test/Tool.py             |  40 +++-
 .../Inputs/rewrite_expect_list_expected.cpp   | 128 +++++++++++
 .../Inputs/rewrite_expects_expected.cpp       |  51 +++++
 .../rewrite_multiple_scripts_expected.cpp     |  46 ++++
 .../scripts/rewriting/Inputs/simple_prog.cpp  |   7 +
 .../rewriting/Inputs/whole_file_test.dex      |   2 +
 .../Inputs/whole_file_test_expected.dex       |   6 +
 .../scripts/rewriting/rewrite_expect_list.cpp |  42 ++++
 .../scripts/rewriting/rewrite_expects.cpp     |  52 +++++
 .../rewriting/rewrite_multiple_scripts.cpp    |  46 ++++
 .../scripts/rewriting/whole_file.test         |  22 ++
 14 files changed, 665 insertions(+), 7 deletions(-)
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_expect_list_expected.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_expects_expected.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_multiple_scripts_expected.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/simple_prog.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/whole_file_test.dex
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/whole_file_test_expected.dex
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_expect_list.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_expects.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_multiple_scripts.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/whole_file.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
new file mode 100644
index 0000000000000..b943aadc9abfa
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectWriter.py
@@ -0,0 +1,201 @@
+# DExTer : Debugging Experience Tester
+# ~~~~~~   ~         ~~         ~   ~~
+#
+# 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
+"""Utilities for using debugger output to generate expected values that match that output."""
+
+from collections import Counter, OrderedDict, defaultdict
+from copy import deepcopy
+from enum import Enum, IntEnum
+from typing import Any, Dict, List, Optional, Set, Tuple, Union
+
+from dex.dextIR import DextIR, StepIR, ValueIR
+from dex.evaluation.StateMatch import get_active_where_matches
+from dex.test_script.Nodes import Expect, Then, Value, Where
+from dex.test_script.Script import DexterScript, Scope
+from dex.tools.Main import Context
+
+
+class ExpectedValueWriter:
+    """Given a ValueIR for an Expect, generates a complete expected value that matches that value if one can be
+    provided."""
+
+    def __init__(self, expect: Expect, value: ValueIR):
+        self.expect = expect
+        self.root_value = value
+        self.expected_value = expect.get_variable_result(value)
+
+
+def unique_expected_values(elements: List[ExpectedValueWriter]):
+    """Given a list of ExpectedValueWriters, and returns either a list containing the unique set of non-None expected
+    values, or a single item if there is only one non-duplicated expected value in the list, or None if there are no
+    valid expected values."""
+
+    unique_set = set()
+    result = []
+    for element in elements:
+        expected_value = element.expected_value
+        if expected_value is None:
+            continue
+        if expected_value not in unique_set:
+            unique_set.add(expected_value)
+            result.append(expected_value)
+    if not result:
+        return None
+    if len(result) == 1:
+        return result[0]
+    return result
+
+
+class StepExpectWriter:
+    """Processes all active, unknown expects at a given debugger step and produces ExpectedValueWriter results for
+    each."""
+
+    def __init__(self, step: StepIR, script: DexterScript):
+        self.step = step
+        self.script = script
+        self.state_match = get_active_where_matches(script, step)
+        active_expects = {
+            expect
+            for where_match in self.state_match.values()
+            for expect in where_match.active_expects
+        }
+        self.expect_matches: Dict[Expect, ExpectedValueWriter] = {}
+
+        def add_expected_values(expect: Expect, expected_value: Any, scope: Scope):
+            assert isinstance(expect, Value), "Non-Value expects currently unsupported"
+            if expect in active_expects and expected_value is None:
+                self.expect_matches[expect] = ExpectedValueWriter(
+                    expect, step.watches[expect.get_watched_expr()]
+                )
+
+        script.visit_script(visit_expect=add_expected_values)
+
+
+class ScriptExpectWriter:
+    """Given the full output from a debugger run and a script with missing expected values, returns a script with
+    filled-in expected values that match the debugger output."""
+
+    def __init__(self, context: Context, dext_ir: DextIR):
+        self.context = context
+        self.dext_ir = dext_ir
+        self.unknown_expect_rewrites: Dict[
+            Expect, List[Tuple[int, ExpectedValueWriter]]
+        ] = {}
+        self.new_script: Optional[DexterScript] = None
+        self.new_expected_values: Dict[Expect, Any] = {}
+        self.missing_expect_rewrites: List[Expect] = []
+
+        def collect_unknown_expects(expect: Expect, expected_value: Any, scope: Scope):
+            assert isinstance(expect, Value), "Non-Value expects currently unsupported"
+            if expected_value is None:
+                self.unknown_expect_rewrites[expect] = []
+
+        script = dext_ir.script
+        assert (
+            script is not None
+        ), "Cannot use ScriptExpectWriter on a non-script Dexter test."
+        script.visit_script(visit_expect=collect_unknown_expects)
+
+        # If there are no expects to update, then there is no rewriting to be done - exit early.
+        if not self.unknown_expect_rewrites:
+            return
+
+        self.step_writers = [StepExpectWriter(step, script) for step in dext_ir.steps]
+        for step_writer in self.step_writers:
+            step_idx = step_writer.step.step_index
+            for expect, expected_value_writer in step_writer.expect_matches.items():
+                self.unknown_expect_rewrites[expect].append(
+                    (step_idx, expected_value_writer)
+                )
+
+        self.new_expected_values = {
+            expect: expected_values
+            for expect, expect_writers in self.unknown_expect_rewrites.items()
+            if (
+                expected_values := unique_expected_values(
+                    [writer for idx, writer in expect_writers]
+                )
+            )
+            is not None
+        }
+        self.new_script = rewrite_script(script, self.new_expected_values)
+        self.missing_expect_rewrites = [
+            expect
+            for expect in self.unknown_expect_rewrites
+            if expect not in self.new_expected_values
+        ]
+
+    @property
+    def num_successful_rewrites(self):
+        return len(self.new_expected_values)
+
+    @property
+    def num_unsuccessful_rewrites(self):
+        return len(self.missing_expect_rewrites)
+
+
+def rewrite_script(
+    script: DexterScript, add_expected_values: Dict[Expect, Any]
+) -> DexterScript:
+    """Given a set of updates to apply to a provided script, returns a copy of the script_obj with the updates
+    applied.
+    Does not deep copy, meaning the new script contains the same node objects as the old script; this is safe as we do
+    not modify these objects."""
+    # First build up a map describing the children of every node in the script, adding add_expected_values to the
+    # required expect nodes.
+    new_node_child_map = {}
+
+    def replace_where(where: Where, scope: Scope):
+        if scope.where:
+            scope_where_children = new_node_child_map.setdefault(scope.where, [])
+            assert isinstance(
+                scope_where_children, list
+            ), f"Unexpected child for !where node: {scope_where_children}"
+            scope_where_children.append(where)
+
+    def replace_then(then: Then, scope: Scope):
+        assert (
+            scope.where not in new_node_child_map
+        ), "!then must be the sole child of a state node."
+        new_node_child_map[scope.where] = then
+
+    def replace_expect(expect: Expect, expected_value, scope: Scope):
+        new_expected_value = add_expected_values.get(expect) or expected_value
+        new_node_child_map[expect] = new_expected_value
+        scope_where_children = new_node_child_map.setdefault(scope.where, [])
+        assert isinstance(
+            scope_where_children, list
+        ), f"Unexpected child for state node {scope.where}: {scope_where_children}"
+        scope_where_children.append(expect)
+
+    script.visit_script(
+        visit_where=replace_where, visit_expect=replace_expect, visit_then=replace_then
+    )
+
+    # Now rebuild the script object using the two maps.
+    def build_subscript(node):
+        """Returns the subset of the script object whose parent is the given node."""
+        assert isinstance(
+            node, (Expect, Where)
+        ), f"Unexpected script parent node: {node}"
+        if isinstance(node, Expect):
+            return new_node_child_map[node]
+        node_children = new_node_child_map[node]
+        if isinstance(node_children, Then):
+            return node_children
+        assert isinstance(
+            node_children, List
+        ), f"Unexpected child for state node {node}: {node_children}"
+        return {child: build_subscript(child) for child in node_children}
+
+    new_script_obj = {node: build_subscript(node) for node in script.script_obj}
+    return DexterScript(
+        script.context,
+        new_script_obj,
+        script.root_scope,
+        script.base_dir,
+        script.load_context,
+    )
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 26a38a604dbe5..50a5cb48a200e 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,16 @@ def get_known_file_for_where(self, where: Where) -> Optional[str]:
         return next_scope.file
 
 
+class ScriptLoadContext:
+    """Contains information about the context that the script was loaded from."""
+
+    def __init__(self, file: str, lines: List[str], start_line: int, stop_line: int):
+        self.file = file
+        self.lines = lines
+        self.start_line = start_line
+        self.stop_line = stop_line
+
+
 class DexterScript:
     def __init__(
         self,
@@ -146,10 +156,12 @@ def __init__(
         script_obj,
         scope: Scope,
         source_root_dir: Optional[str],
+        load_context: ScriptLoadContext,
     ):
         self.context = context
         self.script_obj = script_obj
         self.root_scope = scope
+        self.load_context = load_context
         self.label_dict = LabelDict()
         assert scope.file is not None
         self.base_dir = (
@@ -271,6 +283,7 @@ def get_script(context, file, loader, source_root_dir: Optional[str]) -> DexterS
                 try_load_yaml("\n".join(lines), loader),
                 root_scope,
                 source_root_dir,
+                ScriptLoadContext(file, lines, start_line=0, stop_line=len(lines)),
             )
         except (Error, yaml.YAMLError) as e:
             raise Error(f"File '{file}' was not a valid Dexter script:\n{e}")
@@ -293,6 +306,7 @@ def get_script(context, file, loader, source_root_dir: Optional[str]) -> DexterS
                 ),
                 root_scope,
                 source_root_dir,
+                ScriptLoadContext(file, lines, start_line, stop_line),
             )
         except (Error, yaml.YAMLError) as e:
             attempted_scripts.append((start_line, e))
@@ -330,3 +344,15 @@ def check_explicit_files(where: Where, _: Scope):
 
         script.visit_script(visit_where=check_explicit_files)
         return script, source_files
+
+
+def write_dexter_script_file(script: DexterScript) -> str:
+    load_context = script.load_context
+    script_lines = script.dump().splitlines(True)
+    write_lines = (
+        load_context.lines[: load_context.start_line]
+        + script_lines
+        + ["...\n"]
+        + load_context.lines[load_context.stop_line :]
+    )
+    return "".join(write_lines)
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/tools/ToolBase.py b/cross-project-tests/debuginfo-tests/dexter/dex/tools/ToolBase.py
index 4b09c134a1b6e..d54dd3924e407 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/tools/ToolBase.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/tools/ToolBase.py
@@ -11,6 +11,7 @@
 import tempfile
 
 from dex import __version__
+from dex.tools.Main import Context
 from dex.utils import ExtArgParse
 from dex.utils import PrettyOutput
 from dex.utils.ReturnCode import ReturnCode
@@ -18,7 +19,7 @@
 
 class ToolBase(object, metaclass=abc.ABCMeta):
     def __init__(self, context):
-        self.context = context
+        self.context: Context = context
         self.parser = None
 
     @abc.abstractproperty
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/tools/test/Tool.py b/cross-project-tests/debuginfo-tests/dexter/dex/tools/test/Tool.py
index 0c028773ec56c..ed14a95aece75 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/tools/test/Tool.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/tools/test/Tool.py
@@ -24,8 +24,13 @@
 )
 from dex.dextIR.DextIR import DextIR
 from dex.evaluation import DebuggerRunMatch
+from dex.evaluation.ExpectWriter import ScriptExpectWriter
 from dex.heuristic import Heuristic
-from dex.test_script.Script import get_dexter_script
+from dex.test_script.Script import (
+    DexterScript,
+    get_dexter_script,
+    write_dexter_script_file,
+)
 from dex.tools import TestToolBase
 from dex.utils.Exceptions import DebuggerException
 from dex.utils.Exceptions import BuildScriptException, HeuristicException
@@ -155,10 +160,6 @@ def _init_debugger_controller(self):
 
         self.context.options.source_files.extend(list(new_source_files))
 
-        # If we are not running a debugger, return the DextIR instead of a DebuggerController.
-        if self.context.options.skip_run:
-            return step_collection
-
         if self.context.options.use_script:
             debugger_controller = ScriptDebuggerController(
                 self.context, step_collection
@@ -180,7 +181,8 @@ def _get_steps(self):
 
         if self.context.options.skip_run:
             self.context.logger.warning("Skipping run...")
-            return debugger_controller
+            assert isinstance(debugger_controller.step_collection, DextIR)
+            return debugger_controller.step_collection
         debugger_controller = run_debugger_subprocess(
             debugger_controller, self.context.working_directory.path
         )
@@ -254,6 +256,16 @@ def _record_structured_script_metric_results(
             with open(output_json_path, "w") as fp:
                 json.dump(run_match.get_metric_json_output(), fp)
 
+    def _write_updated_structured_script(
+        self, test_name, rewritten_script: DexterScript
+    ):
+        """Write out the original script file, modified to replace any unknown expects with the actual observed
+        values."""
+        if self.context.options.results_directory:
+            output_text_path = self._get_results_path(test_name)
+            with open(output_text_path, "w", encoding="utf-8") as fp:
+                fp.write(write_dexter_script_file(rewritten_script))
+
     def _record_test_and_display(self, test_case):
         """Output test case to o stream and record test case internally for
         handling later.
@@ -313,6 +325,22 @@ def _run_test(self, test_name):
                 return
             self._record_steps(test_name, steps)
             if self.context.options.use_script:
+                # Before evaluating, the script may contain "unknown" expects; if they should be rewritten, then do so
+                # first, and then use the rewritten script to evaluate.
+                script_writer = ScriptExpectWriter(self.context, steps)
+                if script_writer.new_script:
+                    self.context.logger.note(
+                        f"Rewrote script to add {script_writer.num_successful_rewrites} expected values."
+                    )
+                    if script_writer.num_unsuccessful_rewrites:
+                        self.context.logger.warning(
+                            f"Failed to rewrite {script_writer.num_unsuccessful_rewrites} expected values."
+                        )
+                    self._write_updated_structured_script(
+                        test_name, script_writer.new_script
+                    )
+                    steps.script = script_writer.new_script
+                # Then evaluate, using the new script if any was produced.
                 run_match = DebuggerRunMatch(self.context, steps)
                 self._record_structured_script_metric_results(test_name, run_match)
                 self._record_successful_test_match(test_name, steps, run_match)
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_expect_list_expected.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_expect_list_expected.cpp
new file mode 100644
index 0000000000000..cec451fdd69c9
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_expect_list_expected.cpp
@@ -0,0 +1,128 @@
+// 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} %S/Inputs/rewrite_expect_list_expected.cpp
+
+/// Test that Dexter can write lists of expected values for simple scalar
+/// variables.
+
+/// 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.
+
+// CHECK: Rewrote script to add 3 expected values.
+
+// CHECK: total_watched_steps: 90
+// CHECK: correct_steps: 90
+// CHECK: incorrect_steps: 0
+// CHECK: seen_values: 86
+// CHECK: missing_values: 0
+
+int main() {
+  int prev = 0;
+  int current = 0;
+  int next = 1;
+  for (int i = 0; i < 30; ++i) {
+    prev = current; // !dex_label loop
+    current = next;
+    next = prev + current;
+  }
+  return current;
+}
+
+/*
+---
+? !where {lines: !label 'loop'}
+: !value 'prev':
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '5'
+  - '8'
+  - '13'
+  - '21'
+  - '34'
+  - '55'
+  - '89'
+  - '144'
+  - '233'
+  - '377'
+  - '610'
+  - '987'
+  - '1597'
+  - '2584'
+  - '4181'
+  - '6765'
+  - '10946'
+  - '17711'
+  - '28657'
+  - '46368'
+  - '75025'
+  - '121393'
+  - '196418'
+  - '317811'
+  !value 'current':
+  - '0'
+  - '1'
+  - '2'
+  - '3'
+  - '5'
+  - '8'
+  - '13'
+  - '21'
+  - '34'
+  - '55'
+  - '89'
+  - '144'
+  - '233'
+  - '377'
+  - '610'
+  - '987'
+  - '1597'
+  - '2584'
+  - '4181'
+  - '6765'
+  - '10946'
+  - '17711'
+  - '28657'
+  - '46368'
+  - '75025'
+  - '121393'
+  - '196418'
+  - '317811'
+  - '514229'
+  !value 'next':
+  - '1'
+  - '2'
+  - '3'
+  - '5'
+  - '8'
+  - '13'
+  - '21'
+  - '34'
+  - '55'
+  - '89'
+  - '144'
+  - '233'
+  - '377'
+  - '610'
+  - '987'
+  - '1597'
+  - '2584'
+  - '4181'
+  - '6765'
+  - '10946'
+  - '17711'
+  - '28657'
+  - '46368'
+  - '75025'
+  - '121393'
+  - '196418'
+  - '317811'
+  - '514229'
+  - '832040'
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_expects_expected.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_expects_expected.cpp
new file mode 100644
index 0000000000000..2c04bcdc0fe0c
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_expects_expected.cpp
@@ -0,0 +1,51 @@
+// 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} %S/Inputs/rewrite_expects_expected.cpp
+
+/// Test that when we have a Dexter test with missing/unknown expected values,
+/// Dexter produces a modified test file that is identical except for a modified
+/// script section.
+
+/// 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.
+
+// CHECK: Rewrote script to add 6 expected values.
+// CHECK: Failed to rewrite 2 expected values.
+
+// CHECK: total_watched_steps: 7
+// CHECK: correct_steps: 6
+// CHECK: incorrect_steps: 1
+// CHECK: seen_values: 6
+// CHECK: missing_values: 2
+
+int multiply(int b, int a) {
+  int result = a * b;
+  return result; // !dex_label mul_ret
+}
+
+int main() {
+  int a = 6;
+  int b = 7;
+  int c = multiply(a, b);
+  return c; // !dex_label main_ret
+}
+// !dex_label never_reached
+/*
+---
+? !where {lines: !label 'mul_ret'}
+: !value 'a': '7'
+  !value 'b': '6'
+  !value 'result': '42'
+? !where {lines: !label 'main_ret'}
+: !value 'a': '6'
+  !value 'b': '7'
+  !value 'c': '42'
+  !value 'not_real': null
+? !where {lines: !label 'never_reached'}
+: !value 'a': null
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_multiple_scripts_expected.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_multiple_scripts_expected.cpp
new file mode 100644
index 0000000000000..525889107895b
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_multiple_scripts_expected.cpp
@@ -0,0 +1,46 @@
+// 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_multiple_scripts_expected.cpp
+
+/// Test that when a file contains more than one valid YAML script (but only one
+/// Dexter script), the existing YAML is printed correctly.
+
+/// 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.
+
+// CHECK: Rewrote script to add 1 expected values.
+
+// CHECK: total_watched_steps: 1
+// CHECK: correct_steps: 1
+// CHECK: incorrect_steps: 0
+// CHECK: seen_values: 1
+// CHECK: missing_values: 0
+
+/*
+---
+hr: # 1998 hr ranking
+- Mark McGwire
+- Sammy Sosa
+# 1998 rbi ranking
+rbi:
+- Sammy Sosa
+- Ken Griffey
+...
+*/
+
+int main() {
+    int ret = 0;
+    return ret; // !dex_label ret
+}
+
+/*
+---
+? !where {lines: !label 'ret'}
+: !value 'ret': '0'
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/simple_prog.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/simple_prog.cpp
new file mode 100644
index 0000000000000..177289e1c3ff2
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/simple_prog.cpp
@@ -0,0 +1,7 @@
+
+int main() {
+    int i = 0;
+    i += 1; // !dex_label start
+    i += 1;
+    return i; // !dex_label end
+}
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/whole_file_test.dex b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/whole_file_test.dex
new file mode 100644
index 0000000000000..4501532871cc9
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/whole_file_test.dex
@@ -0,0 +1,2 @@
+!where {file: 'simple_prog.cpp', lines: !range [!label start, !label end]}:
+  ? !value i
\ No newline at end of file
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/whole_file_test_expected.dex b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/whole_file_test_expected.dex
new file mode 100644
index 0000000000000..c1cf5e87915a5
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/whole_file_test_expected.dex
@@ -0,0 +1,6 @@
+? !where {file: simple_prog.cpp, lines: !range [!label 'start', !label 'end']}
+: !value 'i':
+  - '0'
+  - '1'
+  - '2'
+...
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_expect_list.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_expect_list.cpp
new file mode 100644
index 0000000000000..dee5c936522e5
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_expect_list.cpp
@@ -0,0 +1,42 @@
+// 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} %S/Inputs/rewrite_expect_list_expected.cpp
+
+/// Test that Dexter can write lists of expected values for simple scalar
+/// variables.
+
+/// 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.
+
+// CHECK: Rewrote script to add 3 expected values.
+
+// CHECK: total_watched_steps: 90
+// CHECK: correct_steps: 90
+// CHECK: incorrect_steps: 0
+// CHECK: seen_values: 86
+// CHECK: missing_values: 0
+
+int main() {
+  int prev = 0;
+  int current = 0;
+  int next = 1;
+  for (int i = 0; i < 30; ++i) {
+    prev = current; // !dex_label loop
+    current = next;
+    next = prev + current;
+  }
+  return current;
+}
+
+/*
+---
+!where {lines: !label loop}:
+    ? !value prev
+    ? !value current
+    ? !value next
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_expects.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_expects.cpp
new file mode 100644
index 0000000000000..256804e114ad7
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_expects.cpp
@@ -0,0 +1,52 @@
+// 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} %S/Inputs/rewrite_expects_expected.cpp
+
+/// Test that when we have a Dexter test with missing/unknown expected values,
+/// Dexter produces a modified test file that is identical except for a modified
+/// script section.
+
+/// 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.
+
+// CHECK: Rewrote script to add 6 expected values.
+// CHECK: Failed to rewrite 2 expected values.
+
+// CHECK: total_watched_steps: 7
+// CHECK: correct_steps: 6
+// CHECK: incorrect_steps: 1
+// CHECK: seen_values: 6
+// CHECK: missing_values: 2
+
+int multiply(int b, int a) {
+  int result = a * b;
+  return result; // !dex_label mul_ret
+}
+
+int main() {
+  int a = 6;
+  int b = 7;
+  int c = multiply(a, b);
+  return c; // !dex_label main_ret
+}
+// !dex_label never_reached
+/*
+---
+# Comments in the Dexter script are not preserved.
+!where {lines: !label mul_ret}:
+    ? !value a
+    ? !value b
+    ? !value result
+!where {lines: !label main_ret}:
+    ? !value a
+    ? !value b
+    ? !value c
+    ? !value not_real
+!where {lines: !label never_reached}:
+    ? !value a
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_multiple_scripts.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_multiple_scripts.cpp
new file mode 100644
index 0000000000000..f11a6d619e1ff
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_multiple_scripts.cpp
@@ -0,0 +1,46 @@
+// 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_multiple_scripts_expected.cpp
+
+/// Test that when a file contains more than one valid YAML script (but only one
+/// Dexter script), the existing YAML is printed correctly.
+
+/// 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.
+
+// CHECK: Rewrote script to add 1 expected values.
+
+// CHECK: total_watched_steps: 1
+// CHECK: correct_steps: 1
+// CHECK: incorrect_steps: 0
+// CHECK: seen_values: 1
+// CHECK: missing_values: 0
+
+/*
+---
+hr: # 1998 hr ranking
+- Mark McGwire
+- Sammy Sosa
+# 1998 rbi ranking
+rbi:
+- Sammy Sosa
+- Ken Griffey
+...
+*/
+
+int main() {
+    int ret = 0;
+    return ret; // !dex_label ret
+}
+
+/*
+---
+!where {lines: !label ret}:
+    ? !value ret
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/whole_file.test b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/whole_file.test
new file mode 100644
index 0000000000000..7168e4a7dd8ac
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/whole_file.test
@@ -0,0 +1,22 @@
+RUN: rm -rf %t
+RUN: mkdir %t
+RUN: %dexter_regression_test_cxx_build %S/Inputs/simple_prog.cpp -o %t/test
+RUN: %dexter_regression_test_run --use-script --binary %t/test \
+RUN:   --results-directory %t/results --source-root-dir %S/Inputs -- \
+RUN:   %S/Inputs/whole_file_test.dex 2>&1 | FileCheck %s
+RUN: diff %t/results/whole_file_test.dex %S/Inputs/whole_file_test_expected.dex
+
+Test that script rewriting still works when the test file is pure YAML, rather
+than a YAML Dexter script embedded in another file.
+
+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.
+
+CHECK: Rewrote script to add 1 expected values.
+
+CHECK: total_watched_steps: 3
+CHECK: correct_steps: 3
+CHECK: incorrect_steps: 0
+CHECK: seen_values: 3
+CHECK: missing_values: 0
\ No newline at end of file

>From 9d0538ac6347549a76e94879d0c9e384ec1baed9 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Wed, 10 Jun 2026 13:23:51 +0100
Subject: [PATCH 2/2] format

---
 .../Inputs/rewrite_multiple_scripts_expected.cpp          | 4 ++--
 .../scripts/rewriting/Inputs/simple_prog.cpp              | 8 ++++----
 .../scripts/rewriting/rewrite_multiple_scripts.cpp        | 4 ++--
 3 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_multiple_scripts_expected.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_multiple_scripts_expected.cpp
index 525889107895b..134d32f93a5a4 100644
--- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_multiple_scripts_expected.cpp
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_multiple_scripts_expected.cpp
@@ -34,8 +34,8 @@ hr: # 1998 hr ranking
 */
 
 int main() {
-    int ret = 0;
-    return ret; // !dex_label ret
+  int ret = 0;
+  return ret; // !dex_label ret
 }
 
 /*
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/simple_prog.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/simple_prog.cpp
index 177289e1c3ff2..72567b9db3e51 100644
--- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/simple_prog.cpp
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/simple_prog.cpp
@@ -1,7 +1,7 @@
 
 int main() {
-    int i = 0;
-    i += 1; // !dex_label start
-    i += 1;
-    return i; // !dex_label end
+  int i = 0;
+  i += 1; // !dex_label start
+  i += 1;
+  return i; // !dex_label end
 }
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_multiple_scripts.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_multiple_scripts.cpp
index f11a6d619e1ff..ce3b8f6b2db38 100644
--- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_multiple_scripts.cpp
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_multiple_scripts.cpp
@@ -34,8 +34,8 @@ hr: # 1998 hr ranking
 */
 
 int main() {
-    int ret = 0;
-    return ret; // !dex_label ret
+  int ret = 0;
+  return ret; // !dex_label ret
 }
 
 /*



More information about the llvm-branch-commits mailing list