https://github.com/SLTozer updated https://github.com/llvm/llvm-project/pull/204161
>From a7427fdde465df30db721715b409b89dea026dec Mon Sep 17 00:00:00 2001 From: Stephen Tozer <[email protected]> Date: Tue, 16 Jun 2026 13:00:50 +0100 Subject: [PATCH 1/2] [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 805a696ae65b7..9e86af365f7b0 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 @@ -142,6 +146,13 @@ def _get_actual_result( if isinstance(self.expected, Address): return self._get_address_actual_result(self.expected, actual_result) + 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 73c974343420d..f9046bdf17fc3 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, @@ -518,3 +519,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 +... +*/ >From ca06c85da7dd2ba46fa1a5af900765e85304cf76 Mon Sep 17 00:00:00 2001 From: Stephen Tozer <[email protected]> Date: Wed, 24 Jun 2026 14:03:47 +0100 Subject: [PATCH 2/2] Address review comments: extend docs, add test comment --- .../dexter/dex/test_script/Nodes.py | 3 ++- .../dexter/feature_tests/scripts/floats.cpp | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) 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 f9046bdf17fc3..6668b079fa856 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 @@ -526,7 +526,8 @@ class Float: 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]. + value in the range [9.8, 10.2]; range must also be a valid float value (and cannot be + omitted if "+-" is passed). - `!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]`. 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 index 66e24f7d57e7a..4c8e3e8e4cf76 100644 --- a/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/floats.cpp +++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/scripts/floats.cpp @@ -57,12 +57,12 @@ int main() { !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 + - !float 2 +- 0.0001 # Correct + - !float 3.84 +- 0.0001 # Incorrect, actual=3.84615374 + - !float 6.70 +- 0.0001 # Incorrect, actual=6.70103121 + - !float 9.24 +- 0.0001 # Incorrect, actual=9.24893665 + - !float 9.96 +- 0.0001 # Incorrect, actual=9.96959781 + - !float 9.99 +- 0.0001 # Correct + - !float 10.0 +- 0.0001 # Correct ... */ _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
