[llvm-branch-commits] [llvm] [Dexter] Add !type and !type/all nodes to test variable types (PR #204159)
Stephen Tozer via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Wed Jun 24 07:50:47 PDT 2026
https://github.com/SLTozer updated https://github.com/llvm/llvm-project/pull/204159
>From 0dcd6a88d76df0910926df3d78e8efafa4daa1de Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Mon, 15 Jun 2026 16:18:18 +0100
Subject: [PATCH] [Dexter] Add !type and !type/all nodes to test variable types
This patch adds the second kind of variable expect, !type, which tests the
type of a variable as reported by the debugger. As with !value, this is a
string comparison of the debugger output with the script expected value -
this means that even if two types are identical (e.g. typedef), a !type node
will only match the one that the debugger displays by default.
Script writing and aggregates work the same for !type as for !value, and the
metrics reported are largely similar, with the exception that "unexpected",
"seen", and "missing" metrics are reported separately for values and types.
---
.../dexter/dex/evaluation/ExpectMatch.py | 2 +-
.../dexter/dex/evaluation/ExpectRewriter.py | 15 +--
.../dexter/dex/evaluation/Metrics.py | 11 +-
.../dexter/dex/evaluation/RunMatch.py | 8 +-
.../dexter/dex/test_script/Nodes.py | 100 +++++++++++++++++-
.../dexter/dex/test_script/Script.py | 4 +-
.../scripts/evaluation/eval_types.cpp | 54 ++++++++++
.../Inputs/rewrite_types_expected.cpp | 71 +++++++++++++
.../scripts/rewriting/rewrite_types.cpp | 55 ++++++++++
9 files changed, 301 insertions(+), 19 deletions(-)
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_types.cpp
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_types_expected.cpp
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_types.cpp
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectMatch.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectMatch.py
index a6901076ca115..c15313ac54c72 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectMatch.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectMatch.py
@@ -12,7 +12,7 @@
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from dex.dextIR import ValueIR
-from dex.test_script.Nodes import Expect, Value, Address
+from dex.test_script.Nodes import Expect, Address
def get_expected_value_set(
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 d1c31eb51df11..6327e89411e6c 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectRewriter.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/ExpectRewriter.py
@@ -14,11 +14,12 @@
from dex.test_script.Nodes import (
DexRange,
Expect,
+ ExpectAll,
Line,
Step,
Then,
+ Type,
Value,
- ValueAll,
Where,
)
from dex.test_script.Script import DexterScript, Scope
@@ -257,13 +258,15 @@ def collect_expects_to_rewrite(
):
if expected_value is not None:
return
- if isinstance(expect, ValueAll):
+ if expect.get_watched_scope() is not None:
self.scope_expect_rewrites[expect] = []
return
if isinstance(expect, Step):
self.step_expect_rewrites[expect] = []
return
- assert isinstance(expect, Value), f"Unexpected expect node kind {expect}"
+ assert (
+ expect.get_watched_expr() is not None
+ ), f"Unexpected expect node kind {expect}"
self.unknown_expect_rewrites[expect] = []
script.visit_script(visit_expect=collect_expects_to_rewrite)
@@ -393,7 +396,7 @@ def replace_expect(expect: Expect, expected_value, scope: Scope):
assert isinstance(
scope_where_children, list
), f"Unexpected child for state node {scope.where}: {scope_where_children}"
- if isinstance(expect, ValueAll):
+ if isinstance(expect, ExpectAll):
assert (
expect in expected_scope_rewrites
), "Script-rewriter error: Dexter missed rewriting !expect/all node."
@@ -430,11 +433,11 @@ def replace_expect(expect: Expect, expected_value, scope: Scope):
new_expect_parent, []
)
for var, expected_values in var_expected_values:
- new_expect = Value(var)
+ new_expect = expect.get_base_expect(var)
new_expect_sibling_list.append(new_expect)
new_node_child_map[new_expect] = expected_values
return
- assert isinstance(expect, (Step, Value))
+ assert isinstance(expect, (Step, Type, 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/evaluation/Metrics.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/Metrics.py
index 27859beab81c9..28df661bb1b24 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/Metrics.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/Metrics.py
@@ -14,7 +14,7 @@
MatchResult,
get_expected_value_set,
)
-from dex.test_script.Nodes import Expect, Step, Value
+from dex.test_script.Nodes import Expect, Step, Type, Value
class Metric:
@@ -100,7 +100,7 @@ def get_variable_metrics(
) -> Dict[str, Metric]:
"""Given an Expect node with its expected values and a list of all matches for that Expect in a debugger session,
returns the computed metrics for that Expect node."""
- assert isinstance(expect, Value), "Non-Value expects currently unsupported"
+ assert isinstance(expect, (Type, Value)), f"Unexpected non-variable expect {expect}"
if not isinstance(expected_values, list):
expected_values = [expected_values]
num_total_steps = len(matches)
@@ -129,6 +129,7 @@ def get_variable_metrics(
0 if ev in seen_expected_values else count
for ev, count in all_expected_values.items()
)
+ kind_string = "value" if isinstance(expect, Value) else "type"
# And finally produce the metrics map and add the new result to the list.
metrics = {
# The number of steps. Though this is not a useful metric in itself, it may be useful to see in tandem with
@@ -145,15 +146,15 @@ def get_variable_metrics(
# The number of steps where the watched variable/expression was not available in the debugger.
"missing_var_steps": ScalarMetric(num_missing_var_steps, improves_asc=False),
# The number of steps where the watched variable/expression had a value not in the set of expected values.
- "unexpected_value_steps": ScalarMetric(
+ f"unexpected_{kind_string}_steps": ScalarMetric(
num_unexpected_value_steps, improves_asc=False
),
# The % of steps where the expected value sequence was observed.
"correct_step_coverage": FractionMetric(num_correct_steps, num_total_steps),
# The number of expected values that were observed at least once.
- "seen_values": ScalarMetric(num_seen_values),
+ f"seen_{kind_string}s": ScalarMetric(num_seen_values),
# The number of expected values that were not observed.
- "missing_values": ScalarMetric(num_missing_values, improves_asc=False),
+ f"missing_{kind_string}s": ScalarMetric(num_missing_values, improves_asc=False),
}
return metrics
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py
index a3bd98344a5f7..94b6e90d3f7b7 100644
--- a/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py
+++ b/cross-project-tests/debuginfo-tests/dexter/dex/evaluation/RunMatch.py
@@ -28,7 +28,7 @@
)
from dex.evaluation.StateMatch import StateMatchContext, get_state_match
from dex.test_script import DexterScript, Scope
-from dex.test_script.Nodes import Expect, Line, Step, Value
+from dex.test_script.Nodes import Expect, Line, Step
class DebuggerStepMatch:
"""Class used to record the match between a DexterScript and a StepIR, including the state match, determining which
@@ -63,7 +63,9 @@ def add_expected_values(expect: Expect, expected_value: Any, scope: Scope):
expect_frame_idx
].loc.lineno
return
- assert isinstance(expect, Value), f"Unexpected expect node kind {expect}"
+ assert (
+ expect.get_watched_expr() is not None
+ ), f"Unexpected expect node kind {expect}"
self.var_expect_matches[expect] = get_expect_match(
expect,
expected_value,
@@ -100,7 +102,7 @@ def __init__(self, dex_context, dext_ir: DextIR):
def add_expected_values(expect: Expect, expected_value: Any, scope: Scope):
self.expected_values[expect] = expected_value
- if isinstance(expect, Value):
+ if expect.get_watched_expr() is not None:
self.per_var_expect_results[expect] = []
return
assert isinstance(expect, Step), f"Unexpected expect node kind {expect}"
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 bc8cd7ca2d500..73c974343420d 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
@@ -18,7 +18,18 @@
def setup_yaml_parser(loader):
- reg_classes = [Where, Value, DexRange, Label, Then, Address, ValueAll, Step]
+ reg_classes = [
+ Address,
+ DexRange,
+ Label,
+ Step,
+ Then,
+ Type,
+ TypeAll,
+ Value,
+ ValueAll,
+ Where,
+ ]
for c in reg_classes:
c.register_yaml(loader)
@@ -154,6 +165,16 @@ def get_watched_scope(self) -> Optional[str]:
return None
+class ExpectAll(Expect):
+ """An Expect for all variables within a named debugger scope; used only to generate scripts from debugger output,
+ cannot be used for testing debugger output directly.
+ """
+
+ @staticmethod
+ def get_base_expect(var_name: str) -> Expect:
+ raise NotImplementedError(f"No ExpectAll base type declared")
+
+
class Value(Expect):
def __init__(self, variable_name: str):
self.variable_name = variable_name
@@ -187,7 +208,7 @@ def register_yaml(loader):
yaml.add_representer(Value, Value.representer)
-class ValueAll(Expect):
+class ValueAll(ExpectAll):
"""Expect node used to write values for all variables within a particular debugger scope, as defined by the DAP
specification; see: https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Scopes.
@@ -202,6 +223,10 @@ def __init__(self, scope_name: str):
def __repr__(self):
return f"ValueAll({self.scope_name})"
+ @staticmethod
+ def get_base_expect(var_name: str) -> Expect:
+ return Value(var_name)
+
@staticmethod
def get_variable_result(value: ValueIR) -> Optional[str]:
return Value.get_variable_result(value)
@@ -223,6 +248,77 @@ def register_yaml(loader):
yaml.add_representer(ValueAll, ValueAll.representer)
+class Type(Expect):
+ def __init__(self, variable_name: str):
+ self.variable_name = variable_name
+ self.actual_values = None
+
+ @staticmethod
+ def get_variable_result(value: ValueIR) -> Optional[str]:
+ if value.could_evaluate:
+ return value.type_name
+ return None
+
+ def get_watched_expr(self) -> str:
+ return self.variable_name
+
+ def __repr__(self):
+ return f"Type({self.variable_name})"
+
+ @staticmethod
+ def constructor(loader: yaml.Loader, node):
+ return Type(loader.construct_scalar(node))
+
+ @staticmethod
+ def representer(dumper, data):
+ return dumper.represent_scalar("!type", data.variable_name)
+
+ @staticmethod
+ def register_yaml(loader):
+ yaml.add_constructor("!type", Type.constructor, loader)
+ yaml.add_representer(Type, Type.representer)
+
+
+class TypeAll(ExpectAll):
+ """Expect node used to write types for all variables within a particular debugger scope, as defined by the DAP
+ specification; see: https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Scopes.
+
+ This node is not directly evaluated; it must have no expected values, and when Dexter rewrites the original script,
+ this node will be replaced with !type nodes for each variable that was seen in its scope inserted under !and nodes
+ that cover that variable's live range(s).
+ """
+
+ def __init__(self, scope_name: str):
+ self.scope_name = scope_name
+
+ def __repr__(self):
+ return f"TypeAll({self.scope_name})"
+
+ @staticmethod
+ def get_base_expect(var_name: str) -> Expect:
+ return Type(var_name)
+
+ @staticmethod
+ def get_variable_result(value: ValueIR) -> Optional[str]:
+ return Type.get_variable_result(value)
+
+ def get_watched_scope(self) -> Optional[str]:
+ return self.scope_name
+
+ @staticmethod
+ def constructor(loader, node):
+ return TypeAll(loader.construct_scalar(node))
+
+ @staticmethod
+ def representer(dumper, data):
+ return dumper.represent_scalar("!type/all", data.scope_name)
+
+ @staticmethod
+ def register_yaml(loader):
+ yaml.add_constructor("!type/all", TypeAll.constructor, loader)
+ yaml.add_representer(TypeAll, TypeAll.representer)
+
+
class Step(Expect):
"""Sets an expectation for stepping behaviour, with the expected value being a list of integer lines:
- !step exactly: while this !expect is active, we expect see exactly the expected lines in-order as many times as
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 c5c44d295b4af..937c427097d8f 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
@@ -18,11 +18,11 @@
from dex.test_script.Nodes import (
Expect,
+ ExpectAll,
FileLabels,
Label,
Where,
Then,
- ValueAll,
Step,
setup_yaml_parser,
)
@@ -184,7 +184,7 @@ def __init__(
def _validate(self):
def validate_expect(expect: Expect, expected_value, scope: Scope):
- if isinstance(expect, ValueAll) and expected_value is not None:
+ if isinstance(expect, ExpectAll) and expected_value is not None:
raise DexterScriptError(
f"!expect/all node {expect} should not have an expected value."
)
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_types.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_types.cpp
new file mode 100644
index 0000000000000..02e8d5d2c3a82
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/evaluation/eval_types.cpp
@@ -0,0 +1,54 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --binary %t -- %s \
+// RUN: | FileCheck %s
+
+// Test evaluation of !type nodes in Dexter.
+
+// CHECK: correct_step_coverage: 100.0%
+// CHECK: seen_types: 9
+// CHECK: missing_types: 0
+
+using NormalInt = int;
+
+template <typename T> struct GenericDouble {
+ T First;
+ T Second;
+};
+
+template <typename T, typename U> struct Twople {
+ T First;
+ U Second;
+};
+
+struct NestedStruct {
+ GenericDouble<int> IntMembers;
+ Twople<Twople<float, NormalInt>, bool> ManyMembers;
+};
+
+int main() {
+ int a = 0;
+ NormalInt b = 1;
+ Twople<bool, bool> c = {true, false};
+ NestedStruct d = {{2, 4}, {{10, 11}, false}};
+ auto e = false;
+ return 0; // !dex_label ret
+}
+
+/*
+---
+!where {lines: !label ret}:
+ !type a: int
+ !type b: NormalInt
+ !type c: Twople<bool, bool>
+ !type d:
+ IntMembers:
+ First: int
+ Second: int
+ ManyMembers:
+ First:
+ First: float
+ Second: int
+ Second: bool
+ !type e: bool
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_types_expected.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_types_expected.cpp
new file mode 100644
index 0000000000000..1166cb8b8f6f9
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/Inputs/rewrite_types_expected.cpp
@@ -0,0 +1,71 @@
+// 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_types_expected.cpp
+
+/// Test that Dexter can rewrite types, for individual variables and for all
+/// variables in a scope.
+
+/// 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: correct_step_coverage: 100.0%
+// CHECK: seen_types: 12
+// CHECK: missing_types: 0
+
+using NormalInt = int;
+
+template <typename T> struct GenericDouble {
+ T First;
+ T Second;
+};
+
+template <typename T, typename U> struct Twople {
+ T First;
+ U Second;
+};
+
+struct NestedStruct {
+ GenericDouble<int> IntMembers;
+ Twople<Twople<float, NormalInt>, bool> ManyMembers;
+};
+
+GenericDouble<bool> GlobalDouble = {false, true};
+
+int main() {
+ int a = 0;
+ NormalInt b = 1;
+ Twople<bool, bool> c = {true, false};
+ NestedStruct d = {{2, 4}, {{10, 11}, false}};
+ auto e = false;
+ return 0; // !dex_label ret
+}
+
+/*
+---
+? !where {lines: !label 'ret'}
+: !type 'a': int
+ !type 'b': NormalInt
+ !type 'c':
+ First: bool
+ Second: bool
+ !type 'd':
+ IntMembers:
+ First: int
+ Second: int
+ ManyMembers:
+ First:
+ First: float
+ Second: int
+ Second: bool
+ !type 'e': bool
+ !type 'GlobalDouble':
+ First: bool
+ Second: bool
+...
+*/
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_types.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_types.cpp
new file mode 100644
index 0000000000000..119f348b3c22c
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/rewriting/rewrite_types.cpp
@@ -0,0 +1,55 @@
+// 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_types_expected.cpp
+
+/// Test that Dexter can rewrite types, for individual variables and for all
+/// variables in a scope.
+
+/// 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: correct_step_coverage: 100.0%
+// CHECK: seen_types: 12
+// CHECK: missing_types: 0
+
+using NormalInt = int;
+
+template <typename T> struct GenericDouble {
+ T First;
+ T Second;
+};
+
+template <typename T, typename U> struct Twople {
+ T First;
+ U Second;
+};
+
+struct NestedStruct {
+ GenericDouble<int> IntMembers;
+ Twople<Twople<float, NormalInt>, bool> ManyMembers;
+};
+
+GenericDouble<bool> GlobalDouble = {false, true};
+
+int main() {
+ int a = 0;
+ NormalInt b = 1;
+ Twople<bool, bool> c = {true, false};
+ NestedStruct d = {{2, 4}, {{10, 11}, false}};
+ auto e = false;
+ return 0; // !dex_label ret
+}
+
+/*
+---
+!where {lines: !label ret}:
+ ? !type/all Locals
+ ? !type GlobalDouble
+...
+*/
More information about the llvm-branch-commits
mailing list