diff --git a/doc/modules/ROOT/images/dec32_debug.png b/doc/modules/ROOT/images/dec32_debug.png new file mode 100644 index 000000000..effc35db7 Binary files /dev/null and b/doc/modules/ROOT/images/dec32_debug.png differ diff --git a/doc/modules/ROOT/images/dec32_fast_debug.png b/doc/modules/ROOT/images/dec32_fast_debug.png new file mode 100644 index 000000000..86451f271 Binary files /dev/null and b/doc/modules/ROOT/images/dec32_fast_debug.png differ diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index c09895298..59a8ffa36 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -15,6 +15,7 @@ * xref:financial_examples.adoc[] ** xref:financial_examples.adoc#examples_money_parsing[Parsing Pricing Data from File] ** xref:financial_examples.adoc#examples_boost_math[Boost.Math Integration] +* xref:debugger.adoc[] * xref:api_reference.adoc[] ** xref:api_reference.adoc#api_ref_types[Types] ** xref:api_reference.adoc#api_ref_structs[Structs] diff --git a/doc/modules/ROOT/pages/debugger.adoc b/doc/modules/ROOT/pages/debugger.adoc new file mode 100644 index 000000000..b24dabe6e --- /dev/null +++ b/doc/modules/ROOT/pages/debugger.adoc @@ -0,0 +1,41 @@ +//// +Copyright 2025 Matt Borland +Distributed under the Boost Software License, Version 1.0. +https://www.boost.org/LICENSE_1_0.txt +//// + +[#debug] += Debugger Support +:idprefix: debug_ + +Currently, Boost.Decimal supports pretty printing with LLDB. +To load the pretty printer, add the following line to your `.lldbinit`: + +[source] +---- +command script import /path/to/decimal/extra/decimal_printer.py +---- + +Once you have loaded `decimal_printer.py` you can run the following example to see how different values are represented with the pretty printer. + +.The following example can be run with the debugger to show a variety of values +==== + +[source,c++] +---- +include::example$debugger.cpp[] +---- + +*_Expected Output for T = decimal32_t:_* + +image::dec32_debug.png[] + +*_Expected Output for T = decimal_fast32_t:_* + +image::dec32_fast_debug.png[] +==== + +The reason for the difference in how `decimal32_t` and `decimal_fast32_t` is displayed is due to xref:cohorts.adoc[Cohorts and Normalization] + +. `decimal32_t`, `decimal64_t` and `decimal128_t` debuggers are cohort preserving +. `decimal_fast32_t`, `decimal_fast64_t` and `decimal_fast128_t` debuggers are always normalized diff --git a/examples/debugger.cpp b/examples/debugger.cpp new file mode 100644 index 000000000..3864d30aa --- /dev/null +++ b/examples/debugger.cpp @@ -0,0 +1,59 @@ +// Copyright 2025 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt +// +// This example, when run with the pretty printers, shows how various values are represented + +#include // For type decimal32_t +#include // For type decimal64_t +#include // For type decimal128_t +#include // For type decimal_fast32_t +#include // For nan function to write payload to nans +#include + +template +void debug_values() +{ + // Displays the maximum and minimum values that the type can hold + // from numeric_limits + const T max {std::numeric_limits::max()}; + const T min {std::numeric_limits::min()}; + + // A number whose representation will change based on IEEE vs fast type + // In the IEEE case 3.140e+00 will be displayed as the pretty printer is cohort preserving + const T short_num {"3.140"}; + + // Shows how infinities will be displayed + const T pos_inf {std::numeric_limits::infinity()}; + const T neg_inf {-std::numeric_limits::infinity()}; + + // Shows how the different kinds of NANs will be displayed + const T qnan {std::numeric_limits::quiet_NaN()}; + const T snan {std::numeric_limits::signaling_NaN()}; + + // Shows how a payload added to a QNAN will be displayed + const T payload_nan {boost::decimal::nan("7")}; + + // Break Here: + static_cast(max); + static_cast(min); + static_cast(short_num); + static_cast(pos_inf); + static_cast(neg_inf); + static_cast(qnan); + static_cast(snan); + static_cast(payload_nan); +} + +int main() +{ + debug_values(); + debug_values(); + debug_values(); + + debug_values(); + debug_values(); + debug_values(); + + return 0; +} diff --git a/extra/decimal_printer.py b/extra/decimal_printer.py new file mode 100644 index 000000000..b1b7004c7 --- /dev/null +++ b/extra/decimal_printer.py @@ -0,0 +1,230 @@ +# Copyright 2025 Matt Borland +# Distributed under the Boost Software License, Version 1.0. +# https://www.boost.org/LICENSE_1_0.txt + +from detail.decode_ieee_type import decode_decimal32 +from detail.decode_ieee_type import decode_decimal64 +from detail.decode_ieee_type import decode_decimal128 +from detail.decode_fast_type import decode_decimal_fast32 +from detail.decode_fast_type import decode_decimal_fast64 +from detail.decode_fast_type import decode_decimal_fast128 + +import lldb + +def decimal32_summary(valobj, internal_dict): + """ + Custom summary for decimal32_t type + Displays in scientific notation with cohort preservation + """ + + try: + val = valobj.GetNonSyntheticValue() + bits = val.GetChildMemberWithName("bits_").GetValueAsUnsigned() + return decode_decimal32(bits) + + except Exception as e: + return f"" + +def decimal64_summary(valobj, internal_dict): + """ + Custom summary for decimal64_t type + Displays in scientific notation with cohort preservation + """ + + try: + val = valobj.GetNonSyntheticValue() + bits = val.GetChildMemberWithName("bits_").GetValueAsUnsigned() + return decode_decimal64(bits) + + except Exception as e: + return f"" + +def decimal128_summary(valobj, internal_dict): + """ + Custom summary for decimal128_t type + Displays in scientific notation with cohort preservation + """ + + try: + val = valobj.GetNonSyntheticValue() + bits = val.GetChildMemberWithName("bits_") + bits_high = bits.GetChildMemberWithName("high").GetValueAsUnsigned() + bits_low = bits.GetChildMemberWithName("low").GetValueAsUnsigned() + combined_bits = (bits_high << 64) | bits_low + return decode_decimal128(combined_bits) + + except Exception as e: + return f"" + +def decimal_fast32_summary(valobj, internal_dict): + """ + Custom summary for decimal_fast32_t type + Displays in scientific notation + """ + + try: + val = valobj.GetNonSyntheticValue() + significand = val.GetChildMemberWithName("significand_").GetValueAsUnsigned() + exp = val.GetChildMemberWithName("exponent_").GetValueAsUnsigned() + sign = val.GetChildMemberWithName("sign_").GetValueAsUnsigned() + return decode_decimal_fast32(significand, exp, sign) + + except Exception as e: + return f"" + +def decimal_fast64_summary(valobj, internal_dict): + """ + Custom summary for decimal_fast64_t type + Displays in scientific notation + """ + + try: + val = valobj.GetNonSyntheticValue() + significand = val.GetChildMemberWithName("significand_").GetValueAsUnsigned() + exp = val.GetChildMemberWithName("exponent_").GetValueAsUnsigned() + sign = val.GetChildMemberWithName("sign_").GetValueAsUnsigned() + return decode_decimal_fast64(significand, exp, sign) + + except Exception as e: + return f"" +def decimal_fast128_summary(valobj, internal_dict): + """ + Custom summary for decimal_fast128_t type + Displays in scientific notation + """ + + try: + val = valobj.GetNonSyntheticValue() + + significand = val.GetChildMemberWithName("significand_") + bits_high = significand.GetChildMemberWithName("high").GetValueAsUnsigned() + bits_low = significand.GetChildMemberWithName("low").GetValueAsUnsigned() + combined_bits = (bits_high << 64) | bits_low + + exp = val.GetChildMemberWithName("exponent_").GetValueAsUnsigned() + sign = val.GetChildMemberWithName("sign_").GetValueAsUnsigned() + + return decode_decimal_fast128(combined_bits, exp, sign) + + except Exception as e: + return f"" + +def __lldb_init_module(debugger, internal_dict): + decimal32_pattern = r"^(const )?(boost::decimal::decimal32_t|(\w+::)*decimal32_t)( &| \*)?$" + decimal64_pattern = r"^(const )?(boost::decimal::decimal64_t|(\w+::)*decimal64_t)( &| \*)?$" + decimal128_pattern = r"^(const )?(boost::decimal::decimal128_t|(\w+::)*decimal128_t)( &| \*)?$" + + decimal_fast32_pattern = r"^(const )?(boost::decimal::decimal_fast32_t|(\w+::)*decimal_fast32_t)( &| \*)?$" + decimal_fast64_pattern = r"^(const )?(boost::decimal::decimal_fast64_t|(\w+::)*decimal_fast64_t)( &| \*)?$" + decimal_fast128_pattern = r"^(const )?(boost::decimal::decimal_fast128_t|(\w+::)*decimal_fast128_t)( &| \*)?$" + + debugger.HandleCommand( + f'type summary add -x "{decimal32_pattern}" -e -F decimal_printer.decimal32_summary' + ) + debugger.HandleCommand( + f'type synthetic add -x "{decimal32_pattern}" -l decimal_printer.DecimalSyntheticProvider' + ) + + print("decimal32_t printer loaded successfully") + + debugger.HandleCommand( + f'type summary add -x "{decimal64_pattern}" -e -F decimal_printer.decimal64_summary' + ) + debugger.HandleCommand( + f'type synthetic add -x "{decimal64_pattern}" -l decimal_printer.DecimalSyntheticProvider' + ) + + print("decimal64_t printer loaded successfully") + + debugger.HandleCommand( + f'type summary add -x "{decimal128_pattern}" -e -F decimal_printer.decimal128_summary' + ) + debugger.HandleCommand( + f'type synthetic add -x "{decimal128_pattern}" -l decimal_printer.DecimalSyntheticProvider' + ) + + print("decimal128_t printer loaded successfully") + + debugger.HandleCommand( + f'type summary add -x "{decimal_fast32_pattern}" -e -F decimal_printer.decimal_fast32_summary' + ) + debugger.HandleCommand( + f'type synthetic add -x "{decimal_fast32_pattern}" -l decimal_printer.DecimalFastSyntheticProvider' + ) + + print("decimal_fast32_t printer loaded successfully") + + debugger.HandleCommand( + f'type summary add -x "{decimal_fast64_pattern}" -e -F decimal_printer.decimal_fast64_summary' + ) + debugger.HandleCommand( + f'type synthetic add -x "{decimal_fast64_pattern}" -l decimal_printer.DecimalFastSyntheticProvider' + ) + + print("decimal_fast64_t printer loaded successfully") + + debugger.HandleCommand( + f'type summary add -x "{decimal_fast128_pattern}" -e -F decimal_printer.decimal_fast128_summary' + ) + debugger.HandleCommand( + f'type synthetic add -x "{decimal_fast128_pattern}" -l decimal_printer.DecimalFastSyntheticProvider' + ) + + print("decimal_fast128_t printer loaded successfully") + +class DecimalSyntheticProvider: + def __init__(self, valobj, internal_dict): + self.valobj = valobj + + def num_children(self): + return 1 + + def get_child_index(self, name): + if name == "bits_": + return 0 + return -1 + + def get_child_at_index(self, index): + if index == 0: + return self.valobj.GetChildMemberWithName("bits_") + return None + + def update(self): + pass + + def has_children(self): + return True + +class DecimalFastSyntheticProvider: + def __init__(self, valobj, internal_dict): + self.valobj = valobj + + def num_children(self): + return 3 + + def get_child_index(self, name): + if name == "significand_": + return 0 + elif name == "exponent_": + return 1 + elif name == "sign_": + return 2 + else: + return -1 + + def get_child_at_index(self, index): + if index == 0: + return self.valobj.GetChildMemberWithName("significand_") + elif index == 1: + return self.valobj.GetChildMemberWithName("exponent_") + elif index == 2: + return self.valobj.GetChildMemberWithName("sign_") + else: + return None + + def update(self): + pass + + def has_children(self): + return True + diff --git a/extra/detail/decode_fast_type.py b/extra/detail/decode_fast_type.py new file mode 100644 index 000000000..c6205a4a5 --- /dev/null +++ b/extra/detail/decode_fast_type.py @@ -0,0 +1,95 @@ +# Copyright 2025 Matt Borland +# Distributed under the Boost Software License, Version 1.0. +# https://www.boost.org/LICENSE_1_0.txt + +import sys +import os +sys.path.insert(0, os.path.dirname(__file__)) + +from generate_string import generate_string + +def decode_decimal_fast32(significand, exp, sign): + + # See values of non-finite masks in decimal_fast32_t.hpp + if significand >= 536870912: + isnan = False + + if significand == 536870912: + result = "-INF" if sign else "INF" + elif significand >= 3758096384: + result = "-SNAN" if sign else "SNAN" + significand ^= 3758096384 + isnan = True + elif significand >= 1610612736: + result = "-QNAN" if sign else "QNAN" + significand ^= 1610612736 + isnan = True + else: + raise ValueError("Unknown Finite Value") + + if isnan and significand > 0: + result += '(' + str(significand) + ')' + + else: + exp -= 101 # Bias value + result = generate_string(significand, exp, sign) + + return result + +def decode_decimal_fast64(significand, exp, sign): + + # See values of non-finite masks in decimal_fast32_t.hpp + if significand >= 2305843009213693952: + isnan = False + + if significand == 2305843009213693952: + result = "-INF" if sign else "INF" + elif significand >= 16140901064495857664: + result = "-SNAN" if sign else "SNAN" + significand ^= 16140901064495857664 + isnan = True + elif significand >= 6917529027641081856: + result = "-QNAN" if sign else "QNAN" + significand ^= 6917529027641081856 + isnan = True + else: + raise ValueError("Unknown Finite Value") + + if isnan and significand > 0: + result += '(' + str(significand) + ')' + + else: + exp -= 398 # Bias value + result = generate_string(significand, exp, sign) + + return result + +def decode_decimal_fast128(significand, exp, sign): + + high_bits = significand >> 64 + + # See values of non-finite masks in decimal_fast32_t.hpp + if high_bits >= 2305843009213693952: + isnan = False + + if high_bits == 2305843009213693952: + result = "-INF" if sign else "INF" + elif high_bits >= 16140901064495857664: + result = "-SNAN" if sign else "SNAN" + significand ^= (16140901064495857664 << 64) + isnan = True + elif high_bits >= 6917529027641081856: + result = "-QNAN" if sign else "QNAN" + significand ^= (6917529027641081856 << 64) + isnan = True + else: + raise ValueError("Unknown Finite Value") + + if isnan and significand > 0: + result += '(' + str(significand) + ')' + + else: + exp -= 6176 # Bias value + result = generate_string(significand, exp, sign) + + return result diff --git a/extra/detail/decode_ieee_type.py b/extra/detail/decode_ieee_type.py new file mode 100644 index 000000000..e871dda7b --- /dev/null +++ b/extra/detail/decode_ieee_type.py @@ -0,0 +1,129 @@ +# Copyright 2025 Matt Borland +# Distributed under the Boost Software License, Version 1.0. +# https://www.boost.org/LICENSE_1_0.txt + +import sys +import os +sys.path.insert(0, os.path.dirname(__file__)) + +from generate_string import generate_string + +def decode_decimal32(bits): + sign = bits & 2147483648 != 0 + isnan = False + + if bits & 2013265920 == 2013265920: + + if bits & 2113929216 == 2113929216: + result = "-SNAN" if sign else "SNAN" + isnan = True + elif bits & 2080374784 == 2080374784: + result = "-QNAN" if sign else "QNAN" + isnan = True + elif bits & 2080374784 == 2013265920: + result = "-INF" if sign else "INF" + else: + raise ValueError("Unknown Finite Value") + + if isnan: + payload = bits & 8388607 + if payload > 0: + result += '(' + str(payload) + ')' + + else: + # See decimal32_t::to_components() + d32_comb_11_mask = 1610612736 + if bits & d32_comb_11_mask == d32_comb_11_mask: + implied_bit = 8388608 + significand = implied_bit | (bits & 2097151) + exp = (bits & 534773760) >> 21 + else: + significand = bits & 8388607 + exp = (bits & 2139095040) >> 23 + + exp -= 101 # Bias Value + + result = generate_string(significand, exp, sign) + + return result + +def decode_decimal64(bits): + sign = bits & 9223372036854775808 != 0 + isnan = False + + if bits & 8646911284551352320 == 8646911284551352320: + + if bits & 9079256848778919936 == 9079256848778919936: + result = "-SNAN" if sign else "SNAN" + isnan = True + elif bits & 8935141660703064064 == 8935141660703064064: + result = "-QNAN" if sign else "QNAN" + isnan = True + elif bits & 8935141660703064064 == 8646911284551352320: + result = "-INF" if sign else "INF" + else: + raise ValueError("Unknown Finite Value") + + if isnan: + payload = bits & 9007199254740991 + if payload > 0: + result += '(' + str(payload) + ')' + + else: + # See decimal64_t::to_components() + if bits & 6917529027641081856 == 6917529027641081856: + implied_bit = 9007199254740992 + significand = implied_bit | (bits & 2251799813685247) + exp = (bits & 2303591209400008704) >> 51 + else: + significand = bits & 9007199254740991 + exp = (bits & 9214364837600034816) >> 53 + + exp -= 398 # Bias Value + + result = generate_string(significand, exp, sign) + + return result + +def decode_decimal128(bits): + + bits_high = bits >> 64 + d128_not_11_significand_mask = (562949953421311 << 64) | 18446744073709551615 + d128_11_significand_mask = (140737488355327 << 64) | 18446744073709551615 + + sign = bits_high & 9223372036854775808 != 0 + isnan = False + + if bits_high & 8646911284551352320 == 8646911284551352320: + + if bits_high & 9079256848778919936 == 9079256848778919936: + result = "-SNAN" if sign else "SNAN" + isnan = True + elif bits_high & 8935141660703064064 == 8935141660703064064: + result = "-QNAN" if sign else "QNAN" + isnan = True + elif bits_high & 8935141660703064064 == 8646911284551352320: + result = "-INF" if sign else "INF" + else: + raise ValueError("Unknown Finite Value") + + if isnan: + payload = bits & d128_not_11_significand_mask + if payload > 0: + result += '(' + str(payload) + ')' + + else: + # See decimal128_t::to_components() + if bits_high & 6917529027641081856 == 6917529027641081856: + implied_bit = 562949953421312 << 64 + significand = implied_bit | (bits & d128_11_significand_mask) + exp = (bits_high & 2305702271725338624) >> 47 + else: + significand = bits & d128_not_11_significand_mask + exp = (bits_high & 9222809086901354496) >> 49 + + exp -= 6176 # Bias Value + + result = generate_string(significand, exp, sign) + + return result diff --git a/extra/detail/generate_string.py b/extra/detail/generate_string.py new file mode 100644 index 000000000..c530237a4 --- /dev/null +++ b/extra/detail/generate_string.py @@ -0,0 +1,21 @@ +# Copyright 2025 Matt Borland +# Distributed under the Boost Software License, Version 1.0. +# https://www.boost.org/LICENSE_1_0.txt + +def generate_string(significand, exp, sign): + if significand == 0: + result = "0e+00" + else: + sig_str = str(significand) + n_digits = len(sig_str) + + if n_digits == 1: + normalized_str = sig_str + total_exp = exp + else: + normalized_str = sig_str[0] + '.' + sig_str[1:] + total_exp = exp + n_digits - 1 + + result = f"{'-' if sign else ''}{normalized_str}e{total_exp:+03d}" + + return result diff --git a/test/Jamfile b/test/Jamfile index ec051520c..320551eff 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -214,6 +214,7 @@ run ../examples/first_example.cpp ; run ../examples/basic_arithmetic.cpp ; run ../examples/to_from_file.cpp ; run ../examples/addition.cpp ; +run ../examples/debugger.cpp ; # Test compilation of separate headers compile compile_tests/bid_conversion.cpp ;