[llvm-branch-commits] [llvm] [Dexter] Add ability to check float values within a range (PR #204161)
Stephen Tozer via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Tue Jun 16 09:17:28 PDT 2026
https://github.com/SLTozer updated https://github.com/llvm/llvm-project/pull/204161
>From 2c1e04b4be01c2c14cd22925585252428367a151 Mon Sep 17 00:00:00 2001
From: Stephen Tozer <stephen.tozer at sony.com>
Date: Tue, 16 Jun 2026 13:00:50 +0100
Subject: [PATCH] [Dexter] Add ability to check float values within a range
Adds a new node type, !float, which can be used to match debugger ouptut as
float values rather than as strings, optionally allowing a range to be
specified for inexact matches. This new node allows a list of values to be
given, effectively a shorthand for a list of individual !float nodes.
---
.../dexter/dex/evaluation/ExpectMatch.py | 13 ++-
.../dexter/dex/test_script/Nodes.py | 110 +++++++++++++++++-
.../dexter/feature_tests/scripts/floats.cpp | 68 +++++++++++
3 files changed, 189 insertions(+), 2 deletions(-)
create mode 100644 cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/floats.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 1ef9a41c6c8e4..a7ba77c3233d3 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, Address
+from dex.test_script.Nodes import Expect, Address, Float
def get_expected_value_set(
@@ -49,6 +49,10 @@ def get_expected_value_set(
for sub_expect, sub_expected in expected.items():
next_prepend = prepend_tuple + (str(sub_expect),)
result.update(get_expected_value_set(sub_expected, next_prepend))
+ elif isinstance(expected, Float):
+ # Float nodes may themselves contain lists of values; we treat each of those as individual expected values.
+ for expected_float in expected.get_expected_values():
+ result[prepend_tuple + (expected_float,)] += 1
else:
result[prepend_tuple + (str(expected),)] += 1
return result
@@ -194,6 +198,13 @@ def _get_actual_result(
] = resolved_addr
return actual_result, MatchResult.TRUE
+ if isinstance(self.expected, Float):
+ matched_expected = self.expected.matches(actual_result)
+ if matched_expected is None:
+ return actual_result, MatchResult.FALSE
+ self.expected = matched_expected
+ return actual_result, MatchResult.TRUE
+
match_result = MatchResult.from_bools(str(self.expected) == actual_result)
return actual_result, match_result
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 1bd4ce65c6371..9bdb60fa1062a 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
@@ -11,7 +11,7 @@
import abc
from dataclasses import dataclass
import re
-from typing import Any, Dict, Optional, Union
+from typing import Any, Dict, List, Optional, Union
import yaml
from dex.dextIR.ValueIR import ValueIR
from dex.utils.Exceptions import Error
@@ -21,6 +21,7 @@ def setup_yaml_parser(loader):
reg_classes = [
Address,
DexRange,
+ Float,
Label,
Step,
Then,
@@ -515,3 +516,110 @@ def representer(dumper, data: "Label"):
def register_yaml(loader):
yaml.add_constructor("!label", Label.constructor, loader)
yaml.add_representer(Label, Label.representer)
+
+
+class Float:
+ """Used to match against float values that may have an approximate range.
+ There are four possible representations for a !float node, with/without a list of values and with/without a range:
+ - `!float <value>` - checks for an exact match using floating point equality, e.g. !float 10 will match 10.0.
+ - `!float <value>+-<range>` - checks for a match within the given range, e.g. !float 10 +- 0.2 will match any
+ value in the range [9.8, 10.2].
+ - `!float [<value>...] - checks for exact matches against any of the given values, e.g. !float [10, 11, 12] will
+ match 10.0, 11.0, or 12.0. This is effectively a shorthand for using a list of single float
+ values, e.g. `[!float 10, !float 11, !float 12]`.
+ - `!float {values: [<value>...], range: <range>} - checks for matches against any of the given values, each of which
+ will match a range of +-<range>. As with normal lists, this is a
+ shorthand, e.g. !float{values: [1, 2], range: 0.1} is equivalent
+ to [!float 1 +- 0.1, !float 2 +- 0.1].
+ """
+
+ def __init__(self, values, range):
+ try:
+ if isinstance(values, list):
+ values = [float(v) for v in values]
+ range = float(range) if range is not None else None
+ elif isinstance(values, str) and "+-" in values:
+ assert (
+ range is None
+ ), "Float has both an explicit range and a string-embedded range?"
+ values, range = (float(n) for n in values.split("+-", maxsplit=1))
+ else:
+ assert range is None, "Explicit range passed with single float value?"
+ values = float(values)
+ except ValueError as err:
+ raise DexterNodeError(self, f"!float received non-float value: {err}")
+ self.values: Union[float, List[float]] = values
+ self.range = range
+
+ def __repr__(self):
+ if self.range:
+ return f"Float(values={self.values}, range={self.range})"
+ return f"Float(values={self.values})"
+
+ def _format_result(self, expected: float) -> str:
+ """Formats an individual expected value for the purpose of comparing unique expected/seen values."""
+ return (
+ f"Float({expected}+-{self.range})"
+ if self.range is not None
+ else f"Float({expected})"
+ )
+
+ def get_expected_values(self) -> List[str]:
+ """Returns a list of expected values in the form 'Float(<value>[+-<range>])'."""
+ value_list = self.values if isinstance(self.values, list) else [self.values]
+ return [self._format_result(v) for v in value_list]
+
+ def matches(self, actual) -> Optional[str]:
+ """If 'actual' matches this node, return a string representation of the matching value (in the same format as
+ `get_expected_values` above), otherwise return None.
+ """
+ try:
+ actual = float(actual)
+ except ValueError:
+ return None
+
+ def float_match(expected: float) -> bool:
+ if self.range is None:
+ return expected == actual
+ return abs(expected - actual) <= self.range
+
+ if not isinstance(self.values, list):
+ return (
+ self._format_result(self.values) if float_match(self.values) else None
+ )
+ for expected in self.values:
+ if float_match(expected):
+ return self._format_result(expected)
+ return None
+
+ @staticmethod
+ def constructor(loader, node):
+ if isinstance(node, yaml.ScalarNode):
+ # `!float <value>` or `!float <value> +- <range>`
+ return Float(loader.construct_scalar(node), None)
+ if isinstance(node, yaml.SequenceNode):
+ # `!float [<value>...]`
+ return Float(loader.construct_sequence(node), None)
+ if isinstance(node, yaml.MappingNode):
+ # `!float {values: [<value>...], range: <range>}`
+ return Float(**loader.construct_mapping(node, deep=True))
+ raise Exception("Invalid args to !float")
+
+ @staticmethod
+ def representer(dumper: yaml.Dumper, data):
+ if data.range is None:
+ if isinstance(data.values, list):
+ return dumper.represent_sequence("!float", data.values, flow_style=True)
+ return dumper.represent_scalar("!float", data.values)
+ if not isinstance(data.values, list):
+ return dumper.represent_scalar("!float", f"{data.values} +- {data.range}")
+ mapping = {
+ "values": data.values,
+ "range": data.range,
+ }
+ return dumper.represent_mapping("!float", mapping, flow_style=True)
+
+ @staticmethod
+ def register_yaml(loader):
+ yaml.add_constructor("!float", Float.constructor, loader)
+ yaml.add_representer(Float, Float.representer)
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/floats.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/floats.cpp
new file mode 100644
index 0000000000000..66e24f7d57e7a
--- /dev/null
+++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/floats.cpp
@@ -0,0 +1,68 @@
+// RUN: %dexter_regression_test_cxx_build %s -o %t
+// RUN: %dexter_regression_test_run --use-script --binary %t -- %s \
+// RUN: | FileCheck %s
+
+/// Test that we correctly match float values against Float nodes, and also
+/// correctly tally up the number of seen values for Float nodes that contain
+/// expected value lists.
+
+float slowNonInverseSquareRoot(float N) {
+ float CurrentGuess = N * 0.5f;
+ for (int I = 0; I < 8; ++I) { // !dex_label start
+ float Complement = N / CurrentGuess;
+ CurrentGuess = (CurrentGuess + Complement) * 0.5f; // !dex_label mid_loop
+ }
+ return CurrentGuess; // !dex_label end
+}
+
+int main() {
+ slowNonInverseSquareRoot(50.0);
+ slowNonInverseSquareRoot(100.0);
+ return 0;
+}
+
+// CHECK: total_watched_steps: 94
+// CHECK: correct_steps: 78
+// CHECK: incorrect_steps: 16
+// CHECK: partial_step_correctness: 78.0
+// CHECK: missing_var_steps: 0
+// CHECK: unexpected_value_steps: 16
+// CHECK: correct_step_coverage: 83.0% (78/94)
+// CHECK: seen_values: 17
+// CHECK: missing_values: 7
+
+/*
+---
+!where {function: slowNonInverseSquareRoot}:
+ # All checks pass: 60 watched steps, 60 correct values, 12 seen values.
+ !and {conditions: "N == 50.0"}:
+ !and {lines: !range [!label start, !label end]}:
+ !value CurrentGuess: !float
+ values: [25, 13.5, 8.6, 7.2, 7.07]
+ range: 0.01
+ !value N: !float 50
+ !and {lines: !label mid_loop}:
+ !value Complement:
+ - !float 2
+ - !float 3.70 +- 0.01
+ - !float 5.81 +- 0.01
+ - !float 6.93 +- 0.01
+ - !float 7.070 +- 0.001
+ - !float 7.0710 +- 0.0001
+ # Some checks fail: 34 watched steps, 18 correct values, 12 seen values.
+ !and {conditions: "N == 100.0"}:
+ !and {lines: !range [!label start, !label end]}:
+ # 3 correct, 2 unseen, 4 unexpected
+ !value CurrentGuess: !float [50, 26, 15, 11, 10]
+ !and {lines: !label mid_loop}:
+ # 3 correct, 4 unseen, 4 unexpected
+ !value Complement:
+ - !float 2 +- 0.0001
+ - !float 3.84 +- 0.0001
+ - !float 6.70 +- 0.0001
+ - !float 9.24 +- 0.0001
+ - !float 9.96 +- 0.0001
+ - !float 9.99 +- 0.0001
+ - !float 10.0 +- 0.0001
+...
+*/
More information about the llvm-branch-commits
mailing list