[llvm-branch-commits] [llvm] [Dexter] Add label nodes for line references (PR #202544)

Stephen Tozer via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Tue Jun 9 01:59:59 PDT 2026


https://github.com/SLTozer created https://github.com/llvm/llvm-project/pull/202544

This patch adds a !label node to Dexter scripts, which references lines at a position in the source program marked with "!dex_label <identifier>". Each label use can be given a positive or negative offset, and file lookup is based on the provided --source-root-dir (using the test file's directory if none is provided).

>From 14aae62077875be3ceb3a46285b4279654da23a4 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Thu, 4 Jun 2026 15:02:48 +0100
Subject: [PATCH] [Dexter] Add label nodes for line references

This patch adds a !label node to Dexter scripts, which references lines at
a position in the source program marked with "!dex_label <identifier>". Each
label use can be given a positive or negative offset, and file lookup is
based on the provided --source-root-dir (using the test file's directory if
none is provided).
---
 .../ScriptDebuggerController.py               |   2 +-
 .../dexter/dex/evaluation/StateMatch.py       |  24 ++--
 .../dexter/dex/test_script/Nodes.py           |  83 ++++++++++--
 .../dexter/dex/test_script/Script.py          | 118 ++++++++++++++++--
 .../scripts/labels/Inputs/header.h            |   8 ++
 .../scripts/labels/invalid_label.cpp          |  18 +++
 .../feature_tests/scripts/labels/offset.cpp   |  35 ++++++
 .../scripts/labels/simple_labels.cpp          |  38 ++++++
 .../scripts/labels/source_root_dir.cpp        |  41 ++++++
 9 files changed, 336 insertions(+), 31 deletions(-)
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/Inputs/header.h
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/invalid_label.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/offset.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/simple_labels.cpp
 create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/source_root_dir.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 94483eaaa002d..54e924edd40c2 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
@@ -68,7 +68,7 @@ def add_where_entry_bp(self, where: Where, default_file: Optional[str] = None):
             file = where.file or default_file
             assert file, "Cannot set line breakpoints without a valid file!"
             # If this Where covers a range of lines, we breakpoint each of them to ensure that we don't miss any lines.
-            for line in where.get_lines():
+            for line in where.get_lines(self.script.get_labels(file)):
                 added_ids.append(self.debugger.add_breakpoint(file, line))
         self._where_bps[where] = added_ids
         for id in added_ids:
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 2b4363cff38af..f9221043132f1 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/StateMatch.py
@@ -28,6 +28,7 @@ def is_subpath(subpath: str, superpath: str) -> bool:
 def match_where_to_frame(
     where: Where,
     frame: FrameIR,
+    labels: Dict[str, int],
 ) -> bool:
     """A very simple matcher, returns True iff `where` matches `frame`."""
     if where.file is not None and not is_subpath(where.file, frame.loc.path):
@@ -39,7 +40,7 @@ def match_where_to_frame(
         if where.function != fn:
             return False
     if where.lines is not None:
-        if frame.loc.lineno not in where.get_lines():
+        if frame.loc.lineno not in where.get_lines(labels):
             return False
     if (
         where.for_hit_count is not None
@@ -74,6 +75,7 @@ def get_active_where_matches(
 
     def get_active_wheres(where: Where, scope: Scope):
         # For nested !wheres, we must match a specific frame relative to the parent !where.
+        expected_file = scope.get_known_file_for_where(where)
         if scope.where:
             if scope.where not in active_where_expects:
                 # If the parent !where doesn't match any frame, then this !where cannot match any either.
@@ -87,18 +89,20 @@ def get_active_wheres(where: Where, scope: Scope):
                 # If the target frame is -1, we can't match the !where yet, but we should prepare to step into it.
                 active_where_expects[scope.where].pending_wheres.append(where)
                 return
-            if match_where_to_frame(where, step_info.frames[target_frame_idx]):
+            labels = script.get_labels(
+                expected_file or step_info.frames[target_frame_idx].loc.path
+            )
+            if match_where_to_frame(where, step_info.frames[target_frame_idx], labels):
                 active_where_expects[where] = WhereMatchResult(target_frame_idx)
             return
         # For this !where, search for the rootmost stack frame that matches it.
-        matching_frame_idx = next(
-            (
-                frame_idx
-                for frame_idx, frame in reversed(list(enumerate(step_info.frames)))
-                if match_where_to_frame(where, frame)
-            ),
-            None,
-        )
+        matching_frame_idx = None
+        for frame_idx, frame in reversed(list(enumerate(step_info.frames))):
+            labels = script.get_labels(expected_file or frame.loc.path)
+            if match_where_to_frame(where, frame, labels):
+                matching_frame_idx = frame_idx
+                break
+
         if matching_frame_idx is not None:
             active_where_expects[where] = WhereMatchResult(matching_frame_idx)
 
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 932475dff3493..b4650102beff4 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
@@ -10,6 +10,7 @@
 
 import abc
 from dataclasses import dataclass
+import re
 from typing import Any, Dict, Optional, Union
 import yaml
 from dex.dextIR.ValueIR import ValueIR
@@ -21,6 +22,7 @@ def setup_yaml_parser(loader):
         Where,
         Value,
         DexRange,
+        Label,
     ]
     for c in reg_classes:
         c.register_yaml(loader)
@@ -52,7 +54,10 @@ class Where:
     def __init__(self, attributes: dict, is_and: bool):
         self.file: Optional[str] = attributes.pop("file", None)
         self.function: Union[list[str], str, None] = attributes.pop("function", None)
-        self.lines: Union[int, DexRange, None] = attributes.pop("lines", None)
+        lines = attributes.pop("lines", None)
+        if isinstance(lines, (int, Label)):
+            lines = Line(lines)
+        self.lines: Union[Line, DexRange, None] = lines
         self.after_hit_count: Optional[int] = attributes.pop("after_hit_count", None)
         self.for_hit_count: Optional[int] = attributes.pop("for_hit_count", None)
         self.conditions: dict = attributes.pop("conditions", None)
@@ -83,7 +88,7 @@ def get_attrs(self) -> Dict[str, Any]:
         return {
             "file": self.file,
             "function": self.function,
-            "lines": self.lines,
+            "lines": self.lines.value if isinstance(self.lines, Line) else self.lines,
             "for_hit_count": self.for_hit_count,
             "after_hit_count": self.after_hit_count,
             "conditions": self.conditions,
@@ -110,17 +115,18 @@ def register_yaml(loader):
         yaml.add_constructor("!and", Where.get_constructor(True), loader)
         yaml.add_representer(Where, Where.representer)
 
-    def get_lines(self) -> range:
+    def get_lines(self, labels: Dict[str, int]) -> range:
         """Returns the range of line numbers that this Where references, returning an empty range if this Where does not
         refer to any lines."""
         if not self.lines:
             return range(-1)
-        if isinstance(self.lines, int):
-            return range(self.lines, self.lines + 1)
+        if isinstance(self.lines, Line):
+            line_num = self.lines.to_line(labels)
+            return range(line_num, line_num + 1)
         assert isinstance(
             self.lines, DexRange
         ), f"Invalid type for lines: {self.lines}: ({type(self.lines)})"
-        return self.lines.to_range()
+        return self.lines.to_range(labels)
 
 
 ###################
@@ -182,30 +188,81 @@ def register_yaml(loader):
 ## Utility Nodes: Can be used anywhere in a script as a form of syntactic sugar.
 
 
+ at dataclass(frozen=True)
+class Line:
+    """Union class between an int or a Label, used to represent lines inside of Nodes."""
+
+    value: Union[int, "Label"]
+
+    def to_line(self, labels: Dict[str, int]) -> int:
+        if isinstance(self.value, int):
+            return self.value
+        return self.value.to_line(labels)
+
+    def __repr__(self):
+        return str(self.value)
+
+
 @dataclass(frozen=True)
 class DexRange:
-    start: int
-    stop: int
+    start: Line
+    stop: Line
 
     def __repr__(self) -> str:
         return f"[{self.start} - {self.stop}]"
 
     # We use an inclusive range in Dexter scripts, while python ranges are exclusive.
-    def to_range(self) -> range:
-        return range(self.start, self.stop + 1)
+    def to_range(self, labels: Dict[str, int]) -> range:
+        return range(self.start.to_line(labels), self.stop.to_line(labels) + 1)
 
     @staticmethod
     def constructor(loader: yaml.Loader, node):
         range_seq = loader.construct_sequence(node)
-        if len(range_seq) != 2 or not all(isinstance(elt, int) for elt in range_seq):
+        if len(range_seq) != 2 or not all(
+            isinstance(elt, (int, Label)) for elt in range_seq
+        ):
             raise DexterNodeError(node, "range must have exactly 2 int elements")
-        return DexRange(range_seq[0], range_seq[1])
+        return DexRange(Line(range_seq[0]), Line(range_seq[1]))
 
     @staticmethod
     def representer(dumper, data: "DexRange"):
-        return dumper.represent_sequence("!range", [data.start, data.stop])
+        return dumper.represent_sequence("!range", [data.start.value, data.stop.value])
 
     @staticmethod
     def register_yaml(loader):
         yaml.add_constructor("!range", DexRange.constructor, loader)
         yaml.add_representer(DexRange, DexRange.representer)
+
+
+ at dataclass(frozen=True)
+class Label:
+    name: str
+
+    def to_line(self, labels: Dict[str, int]) -> int:
+        # Labels may contain offsets, which is accounted for here.
+        raw_label = self.name.strip()
+        label_str = raw_label
+        offset = 0
+        if match := re.match(r"^([a-zA-Z_]\w*)\s*([+-])\s*(\d+)$", raw_label):
+            identifier, sign, number = match.groups()
+            offset = int(number) if sign == "+" else -int(number)
+            label_str = identifier
+        if label_str not in labels:
+            raise DexterNodeError(self, f'Label "{label_str}" not found')
+        return labels[label_str] + offset
+
+    def __repr__(self):
+        return f"Label({self.name})"
+
+    @staticmethod
+    def constructor(loader: yaml.Loader, node):
+        return Label(loader.construct_scalar(node))
+
+    @staticmethod
+    def representer(dumper, data: "Label"):
+        return dumper.represent_scalar("!label", data.name)
+
+    @staticmethod
+    def register_yaml(loader):
+        yaml.add_constructor("!label", Label.constructor, loader)
+        yaml.add_representer(Label, Label.representer)
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 a338f62f95af7..6f087ca52db17 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
@@ -9,9 +9,11 @@
 results.
 """
 
-from pathlib import PurePath
+from collections import defaultdict
+from pathlib import Path, PurePath
 import os
-from typing import Any, Callable, Optional, Set
+import re
+from typing import Any, Callable, Dict, List, Optional, Set
 import yaml
 
 from dex.test_script.Nodes import (
@@ -20,6 +22,7 @@
     setup_yaml_parser,
 )
 
+from dex.tools.Main import Context
 from dex.utils.Exceptions import Error
 from dex.utils.Timer import Timer
 
@@ -28,6 +31,49 @@ class DexterScriptError(Error):
     pass
 
 
+class LabelDict:
+    def __init__(self):
+        self.file_to_labels_to_lines: Dict[str, Dict[str, int]] = {}
+
+    def set_labels_from_file(self, context: Context, file: str, base_dir: str):
+        """Given either an absolute filepath or a relative filepath and the directory it is relative to, searches the
+        file for !dex_labels and records the line numbers they appear on."""
+        # Check first whether we've already checked this file for labels - even if the file doesn't contain any labels
+        # we store an empty dict to record that fact.
+        # NB: We don't bother detecting cases where different file strings refer to the same underlying file, since this
+        #     is low cost.
+        if self.has_labels(file):
+            return
+        self.file_to_labels_to_lines[file] = {}
+        abs_path = file if os.path.isabs(file) else os.path.join(base_dir, file)
+        if not os.path.exists(abs_path):
+            context.logger.warning(f"Could not find !where file: {abs_path}")
+            return
+        with open(abs_path, "r", encoding="utf-8", errors="ignore") as r:
+            lines = r.readlines()
+        # Now that we have the lines from the file, search for labels.
+        dex_label_re = re.compile(r"!dex_label ([a-zA-Z_]\w*)")
+        for idx, line in enumerate(lines):
+            label_str_match = dex_label_re.search(line)
+            if not label_str_match:
+                continue
+            label = label_str_match.group(1)
+            line = idx + 1
+            if label in self.file_to_labels_to_lines[file]:
+                # Ignore duplicate labels.
+                original_line = self.file_to_labels_to_lines[file][label]
+                context.logger.warning(
+                    f'ignoring duplicate label "{label}" in "{file}"; original: {original_line}, new: {line}'
+                )
+            else:
+                self.file_to_labels_to_lines[file][label] = line
+
+    def has_labels(self, file: str) -> bool:
+        return file in self.file_to_labels_to_lines
+
+    def get_labels(self, file: str) -> Dict[str, int]:
+        return self.file_to_labels_to_lines[file]
+
 class Scope:
     """Helper class used to simplify queries about the context of a Node in the Dexter Script. The context for a given
     Node consists of some base context information in the root of the script, and then all Where nodes in the parent
@@ -66,17 +112,49 @@ def add_where(self, where: Where):
         """Adds `where` to this Scope's chain."""
         return Scope(where=where, parent_scope=self)
 
+    def get_known_file_for_where(self, where: Where) -> Optional[str]:
+        """For a `where` that exists directly in this Scope, determines whether there is a known file that `where`
+        expects - whether this is an explicitly-declared file, or implicitly the root scope file. There is no known file
+        for !where {function: ...} nodes, however."""
+        if where.file:
+            return where.file
+        if where.function:
+            return None
+        next_scope = self
+        while where.is_and:
+            assert (
+                next_scope.parent_scope and next_scope.where
+            ), "!and node at root scope?"
+            where = next_scope.where
+            if where.file:
+                return where.file
+            if where.function:
+                return None
+            next_scope = next_scope.parent_scope
+        while next_scope.file is None:
+            assert next_scope.parent_scope
+            next_scope = next_scope.parent_scope
+        return next_scope.file
+
 
 class DexterScript:
     def __init__(
         self,
-        context,
+        context: Context,
         script_obj,
         scope: Scope,
+        source_root_dir: Optional[str],
     ):
         self.context = context
         self.script_obj = script_obj
         self.root_scope = scope
+        self.label_dict = LabelDict()
+        assert scope.file is not None
+        self.base_dir = (
+            source_root_dir
+            if source_root_dir is not None
+            else os.path.dirname(scope.file)
+        )
         # `visit_script` will validate the structure of the script, as it traverses the full script and raises an
         # exception if it sees anything unexpected.
         self.visit_script()
@@ -130,6 +208,26 @@ def root_wheres(self) -> Set[Where]:
     def dump(self) -> str:
         return yaml.dump(self.script_obj)
 
+    def gather_labels(self):
+        """Pre-gather labels for all !where nodes with `lines` entries."""
+        assert self.root_scope.file is not None
+        add_labels = lambda file: self.label_dict.set_labels_from_file(
+            self.context, file, str(self.base_dir)
+        )
+
+        def collect_file(where: Where, scope: Scope):
+            # If we have !where nodes that check lines without an explicit file, we default to the test file.
+            if not where.is_and and where.lines and not where.file:
+                add_labels(self.root_scope.file)
+            elif where.file:
+                add_labels(where.file)
+
+        self.visit_script(visit_where=collect_file)
+
+    def get_labels(self, file: str) -> Dict[str, int]:
+        if not self.label_dict.has_labels(file):
+            self.label_dict.set_labels_from_file(self.context, file, self.base_dir)
+        return self.label_dict.get_labels(file)
 
 # Helper function to apply a line offset to the errors reported by YAML while loading, to account for the YAML documents
 # being embedded in part of a file.
@@ -160,7 +258,7 @@ def adjust_mark_loc(mark: Optional[yaml.Mark]) -> Optional[yaml.Mark]:
         raise e
 
 
-def get_script(context, file, loader) -> DexterScript:
+def get_script(context, file, loader, source_root_dir: Optional[str]) -> DexterScript:
     """Searches the given file for a valid Dexter script, and returns the first valid script that it finds or raises an
     Error if none is found."""
     if not os.path.exists(file):
@@ -182,6 +280,7 @@ def get_script(context, file, loader) -> DexterScript:
                 context,
                 try_load_yaml("\n".join(lines), loader),
                 root_scope,
+                source_root_dir,
             )
         except (Error, yaml.YAMLError) as e:
             raise Error(f"File '{file}' was not a valid Dexter script:\n{e}")
@@ -203,6 +302,7 @@ def get_script(context, file, loader) -> DexterScript:
                     "\n".join(lines[start_line:stop_line]), loader, start_line
                 ),
                 root_scope,
+                source_root_dir,
             )
         except (Error, yaml.YAMLError) as e:
             attempted_scripts.append((start_line, e))
@@ -218,13 +318,17 @@ def get_script(context, file, loader) -> DexterScript:
     )
 
 
-def get_dexter_script(context, test_file, source_root_dir):
+def get_dexter_script(context, test_file, source_root_dir: Optional[str]):
     setup_yaml_parser(yaml.CLoader)
     with Timer("parsing script"):
-        script = get_script(context, test_file, yaml.CLoader)
+        script = get_script(context, test_file, yaml.CLoader, source_root_dir)
         assert script.root_scope.file == test_file
         source_files = set()
-        source_dir = source_root_dir if source_root_dir else str(test_file)
+        source_dir = (
+            source_root_dir
+            if source_root_dir is not None
+            else os.path.basename(test_file)
+        )
 
         def check_explicit_files(where: Where, _: Scope):
             if not where.file:
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/Inputs/header.h b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/Inputs/header.h
new file mode 100644
index 0000000000000..353053eb19689
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/Inputs/header.h
@@ -0,0 +1,8 @@
+
+int multiply(int a, int b) {
+    int result = 0;
+    for (int i = 0; i < b; ++i) {
+        result += a;
+    }
+    return result; // !dex_label mul
+}
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/invalid_label.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/invalid_label.cpp
new file mode 100644
index 0000000000000..54418d0853bdd
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/invalid_label.cpp
@@ -0,0 +1,18 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: not %dexter_regression_test_run --source-root-dir %S/Inputs --use-script --binary %t -- %s 2>&1 | FileCheck %s
+
+int main() {
+    int a = 4;
+    int b = 4;
+    return b - a; // !dex_label unused
+}
+
+// CHECK: error: Error with node: Label(used): Label "used" not found
+
+/*
+---
+!where {lines: !label used}:
+    !value a: 4
+    !value b: 4
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/offset.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/offset.cpp
new file mode 100644
index 0000000000000..5c0bb7b995bcc
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/offset.cpp
@@ -0,0 +1,35 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --binary %t -- %s | FileCheck %s
+
+
+int main() {
+    int count = 0;
+    ++count; // !dex_label start
+    ++count;
+    ++count;
+    ++count;
+    return count;
+} // !dex_label end
+
+// CHECK: total_watched_steps: 5
+// CHECK: correct_steps: 5
+// CHECK: incorrect_steps: 0
+// CHECK: missing_var_steps: 0
+// CHECK: unexpected_value_steps: 0
+// CHECK: seen_values: 5
+// CHECK: missing_values: 0
+
+/*
+---
+!where {lines: !label start}:
+    !value count: 0
+!where {lines: !label start + 1}:
+    !value count: 1
+!where {lines: !label start+  2}:
+    !value count: 2
+!where {lines: !label start     +3}:
+    !value count: 3
+!where {lines: !label end-1}:
+    !value count: 4
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/simple_labels.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/simple_labels.cpp
new file mode 100644
index 0000000000000..c65a9c8fca77d
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/simple_labels.cpp
@@ -0,0 +1,38 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --binary %t -- %s | FileCheck %s
+
+#include "Inputs/header.h"
+
+int factorial(int n) {
+    int result = 1;
+    // !dex_label factorial_start
+    for (int i = 1; i <= n; ++i) {
+        result = multiply(result, i);  
+    }
+    return result; // !dex_label factorial_end
+}
+
+int main() {
+    int a = 4;
+    return factorial(a); // !dex_label call
+}
+
+// CHECK: total_watched_steps: 20
+// CHECK: correct_steps: 20
+// CHECK: incorrect_steps: 0
+// CHECK: missing_var_steps: 0
+// CHECK: unexpected_value_steps: 0
+// CHECK: seen_values: 9
+// CHECK: missing_values: 0
+
+/*
+---
+!where {lines: !label call}:
+    !value a: 4
+!where {function: factorial}:
+    !and {lines: !range [!label factorial_start, !label factorial_end]}:
+        !value result: [1, 2, 6, 24]
+    !where {file: "Inputs/header.h", lines: !label mul}:
+        !value result: [1, 2, 6, 24]
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/source_root_dir.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/source_root_dir.cpp
new file mode 100644
index 0000000000000..420d1b6b45e37
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/labels/source_root_dir.cpp
@@ -0,0 +1,41 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --source-root-dir %S/Inputs --use-script --binary %t -- %s | FileCheck %s
+
+// Check that when --source-root-dir is provided, labels will be checked
+// relative to that directory.
+
+#include "Inputs/header.h"
+
+int factorial(int n) {
+    int result = 1;
+    // !dex_label factorial_start
+    for (int i = 1; i <= n; ++i) {
+        result = multiply(result, i);  
+    }
+    return result; // !dex_label factorial_end
+}
+
+int main() {
+    int a = 4;
+    return factorial(a); // !dex_label call
+}
+
+// CHECK: total_watched_steps: 20
+// CHECK: correct_steps: 20
+// CHECK: incorrect_steps: 0
+// CHECK: missing_var_steps: 0
+// CHECK: unexpected_value_steps: 0
+// CHECK: seen_values: 9
+// CHECK: missing_values: 0
+
+/*
+---
+!where {lines: !label call}:
+    !value a: 4
+!where {function: factorial}:
+    !and {lines: !range [!label factorial_start, !label factorial_end]}:
+        !value result: [1, 2, 6, 24]
+    !where {file: "header.h", lines: !label mul}:
+        !value result: [1, 2, 6, 24]
+...
+*/



More information about the llvm-branch-commits mailing list