diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt index 65f84a48e8..3f8f6a8efb 100644 --- a/COPYRIGHT.txt +++ b/COPYRIGHT.txt @@ -293,6 +293,12 @@ Comment: Graphite engine Copyright: 2010, SIL International License: Expat +Files: thirdparty/grisu2/grisu2.h +Comment: Grisu2 float serialization algorithm +Copyright: 2009, Florian Loitsch + 2018-2023, The simdjson authors +License: Expat and Apache + Files: thirdparty/harfbuzz/* Comment: HarfBuzz text shaping library Copyright: 2010-2022, Google, Inc. diff --git a/core/doc_data.cpp b/core/doc_data.cpp index d19040258c..4f72a14430 100644 --- a/core/doc_data.cpp +++ b/core/doc_data.cpp @@ -31,10 +31,24 @@ #include "doc_data.h" String DocData::get_default_value_string(const Variant &p_value) { - if (p_value.get_type() == Variant::ARRAY) { + const Variant::Type type = p_value.get_type(); + if (type == Variant::ARRAY) { return Variant(Array(p_value, 0, StringName(), Variant())).get_construct_string().replace_char('\n', ' '); - } else if (p_value.get_type() == Variant::DICTIONARY) { + } else if (type == Variant::DICTIONARY) { return Variant(Dictionary(p_value, 0, StringName(), Variant(), 0, StringName(), Variant())).get_construct_string().replace_char('\n', ' '); + } else if (type == Variant::INT) { + return itos(p_value); + } else if (type == Variant::FLOAT) { + // Since some values are 32-bit internally, use 32-bit for all + // documentation values to avoid garbage digits at the end. + const String s = String::num_scientific((float)p_value); + // Use float literals for floats in the documentation for clarity. + if (s != "inf" && s != "-inf" && s != "nan") { + if (!s.contains_char('.') && !s.contains_char('e')) { + return s + ".0"; + } + } + return s; } else { return p_value.get_construct_string().replace_char('\n', ' '); } diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index daa3daa0b1..9d3fe51a2c 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -43,6 +43,8 @@ #include "core/variant/variant.h" #include "core/version_generated.gen.h" +#include "thirdparty/grisu2/grisu2.h" + #ifdef _MSC_VER #define _CRT_SECURE_NO_WARNINGS // to disable build-time warning which suggested to use strcpy_s instead strcpy #endif @@ -1656,28 +1658,18 @@ String String::num_scientific(double p_num) { if (Math::is_nan(p_num) || Math::is_inf(p_num)) { return num(p_num, 0); } + char buffer[256]; + char *last = grisu2::to_chars(buffer, p_num); + return String::ascii(Span(buffer, last - buffer)); +} - char buf[256]; - -#if defined(__GNUC__) || defined(_MSC_VER) - -#if defined(__MINGW32__) && defined(_TWO_DIGIT_EXPONENT) && !defined(_UCRT) - // MinGW requires _set_output_format() to conform to C99 output for printf - unsigned int old_exponent_format = _set_output_format(_TWO_DIGIT_EXPONENT); -#endif - snprintf(buf, 256, "%lg", p_num); - -#if defined(__MINGW32__) && defined(_TWO_DIGIT_EXPONENT) && !defined(_UCRT) - _set_output_format(old_exponent_format); -#endif - -#else - sprintf(buf, "%.16lg", p_num); -#endif - - buf[255] = 0; - - return buf; +String String::num_scientific(float p_num) { + if (Math::is_nan(p_num) || Math::is_inf(p_num)) { + return num(p_num, 0); + } + char buffer[256]; + char *last = grisu2::to_chars(buffer, p_num); + return String::ascii(Span(buffer, last - buffer)); } String String::md5(const uint8_t *p_md5) { diff --git a/core/string/ustring.h b/core/string/ustring.h index 6c4d0fe7c5..58a575adfb 100644 --- a/core/string/ustring.h +++ b/core/string/ustring.h @@ -446,6 +446,7 @@ public: String unquote() const; static String num(double p_num, int p_decimals = -1); static String num_scientific(double p_num); + static String num_scientific(float p_num); static String num_real(double p_num, bool p_trailing = true); static String num_real(float p_num, bool p_trailing = true); static String num_int64(int64_t p_num, int base = 10, bool capitalize_hex = false); diff --git a/core/variant/variant_call.cpp b/core/variant/variant_call.cpp index c32e747a31..279b742669 100644 --- a/core/variant/variant_call.cpp +++ b/core/variant/variant_call.cpp @@ -1697,6 +1697,16 @@ StringName Variant::get_enum_for_enumeration(Variant::Type p_type, const StringN register_builtin_method(sarray(), m_default_args); #endif // DEBUG_ENABLED +#ifdef DEBUG_ENABLED +#define bind_static_methodv(m_type, m_name, m_method, m_arg_names, m_default_args) \ + STATIC_METHOD_CLASS(m_type, m_name, m_method); \ + register_builtin_method(m_arg_names, m_default_args); +#else +#define bind_static_methodv(m_type, m_name, m_method, m_arg_names, m_default_args) \ + STATIC_METHOD_CLASS(m_type, m_name, m_method); \ + register_builtin_method(sarray(), m_default_args); +#endif + #ifdef DEBUG_ENABLED #define bind_methodv(m_type, m_name, m_method, m_arg_names, m_default_args) \ METHOD_CLASS(m_type, m_name, m_method); \ @@ -1882,7 +1892,7 @@ static void _register_variant_builtin_methods_string() { bind_string_method(to_multibyte_char_buffer, sarray("encoding"), varray(String())); bind_string_method(hex_decode, sarray(), varray()); - bind_static_method(String, num_scientific, sarray("number"), varray()); + bind_static_methodv(String, num_scientific, static_cast(&String::num_scientific), sarray("number"), varray()); bind_static_method(String, num, sarray("number", "decimals"), varray(-1)); bind_static_method(String, num_int64, sarray("number", "base", "capitalize_hex"), varray(10, false)); bind_static_method(String, num_uint64, sarray("number", "base", "capitalize_hex"), varray(10, false)); diff --git a/core/variant/variant_parser.cpp b/core/variant/variant_parser.cpp index 5631f6d242..ea24eeae0e 100644 --- a/core/variant/variant_parser.cpp +++ b/core/variant/variant_parser.cpp @@ -1934,22 +1934,30 @@ Error VariantParser::parse(Stream *p_stream, Variant &r_ret, String &r_err_str, ////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// +// These two functions serialize floats or doubles using num_scientific to ensure +// it can be read back in the same way (except collapsing -0 to 0, and NaN values). +static String rtos_fix(float p_value, bool p_compat) { + if (p_value == 0.0f) { + return "0"; // Avoid negative zero (-0) being written, which may annoy git, svn, etc. for changes when they don't exist. + } else if (p_compat) { + // Write old inf_neg for compatibility. + if (std::isinf(p_value) && p_value < 0.0f) { + return "inf_neg"; + } + } + return String::num_scientific(p_value); +} + static String rtos_fix(double p_value, bool p_compat) { if (p_value == 0.0) { - return "0"; //avoid negative zero (-0) being written, which may annoy git, svn, etc. for changes when they don't exist. - } else if (std::isnan(p_value)) { - return "nan"; - } else if (std::isinf(p_value)) { - if (p_value > 0) { - return "inf"; - } else if (p_compat) { + return "0"; // Avoid negative zero (-0) being written, which may annoy git, svn, etc. for changes when they don't exist. + } else if (p_compat) { + // Write old inf_neg for compatibility. + if (std::isinf(p_value) && p_value < 0.0) { return "inf_neg"; - } else { - return "-inf"; } - } else { - return rtoss(p_value); } + return String::num_scientific(p_value); } Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_string_func, void *p_store_string_ud, EncodeResourceFunc p_encode_res_func, void *p_encode_res_ud, int p_recursion_count, bool p_compat) { @@ -1964,11 +1972,17 @@ Error VariantWriter::write(const Variant &p_variant, StoreStringFunc p_store_str p_store_string_func(p_store_string_ud, itos(p_variant.operator int64_t())); } break; case Variant::FLOAT: { - String s = rtos_fix(p_variant.operator double(), p_compat); - if (s != "inf" && s != "-inf" && s != "nan") { - if (!s.contains_char('.') && !s.contains_char('e') && !s.contains_char('E')) { - s += ".0"; - } + const double value = p_variant.operator double(); + String s; + // Hack to avoid garbage digits when the underlying float is 32-bit. + if ((double)(float)value == value) { + s = rtos_fix((float)value, p_compat); + } else { + s = rtos_fix(value, p_compat); + } + // Append ".0" to floats to ensure they are float literals. + if (s != "inf" && s != "-inf" && s != "nan" && !s.contains_char('.') && !s.contains_char('e') && !s.contains_char('E')) { + s += ".0"; } p_store_string_func(p_store_string_ud, s); } break; diff --git a/doc/classes/Animation.xml b/doc/classes/Animation.xml index 609d7eff39..99aa349946 100644 --- a/doc/classes/Animation.xml +++ b/doc/classes/Animation.xml @@ -689,7 +689,7 @@ Determines the behavior of both ends of the animation timeline during animation playback. This is used for correct interpolation of animation cycles, and for hinting the player that it must restart the animation. - + The animation step value. diff --git a/doc/classes/CharacterBody2D.xml b/doc/classes/CharacterBody2D.xml index 99f7181f2c..bd52d3b4ef 100644 --- a/doc/classes/CharacterBody2D.xml +++ b/doc/classes/CharacterBody2D.xml @@ -154,7 +154,7 @@ If [code]false[/code] (by default), the body will move faster on downward slopes and slower on upward slopes. If [code]true[/code], the body will always move at the same speed on the ground no matter the slope. Note that you need to use [member floor_snap_length] to stick along a downward slope at constant speed. - + Maximum angle (in radians) where a slope is still considered a floor (or a ceiling), rather than a wall, when calling [method move_and_slide]. The default value equals 45 degrees. @@ -195,7 +195,7 @@ Current velocity vector in pixels per second, used and modified during calls to [method move_and_slide]. - + Minimum angle (in radians) where the body is allowed to slide when it encounters a wall. The default value equals 15 degrees. This property only affects movement when [member motion_mode] is [constant MOTION_MODE_FLOATING]. diff --git a/doc/classes/CharacterBody3D.xml b/doc/classes/CharacterBody3D.xml index 0307ea67f7..96de443a5a 100644 --- a/doc/classes/CharacterBody3D.xml +++ b/doc/classes/CharacterBody3D.xml @@ -145,7 +145,7 @@ If [code]false[/code] (by default), the body will move faster on downward slopes and slower on upward slopes. If [code]true[/code], the body will always move at the same speed on the ground no matter the slope. Note that you need to use [member floor_snap_length] to stick along a downward slope at constant speed. - + Maximum angle (in radians) where a slope is still considered a floor (or a ceiling), rather than a wall, when calling [method move_and_slide]. The default value equals 45 degrees. @@ -186,7 +186,7 @@ Current velocity vector (typically meters per second), used and modified during calls to [method move_and_slide]. - + Minimum angle (in radians) where the body is allowed to slide when it encounters a wall. The default value equals 15 degrees. When [member motion_mode] is [constant MOTION_MODE_GROUNDED], it only affects movement if [member floor_block_on_wall] is [code]true[/code]. diff --git a/doc/classes/Color.xml b/doc/classes/Color.xml index 5ceb222057..a05daef366 100644 --- a/doc/classes/Color.xml +++ b/doc/classes/Color.xml @@ -538,343 +538,343 @@ - + Alice blue color. - + Antique white color. Aqua color. - + Aquamarine color. - + Azure color. - + Beige color. - + Bisque color. Black color. In GDScript, this is the default value of any color. - + Blanched almond color. Blue color. - + Blue violet color. - + Brown color. - + Burlywood color. - + Cadet blue color. - + Chartreuse color. - + Chocolate color. - + Coral color. - + Cornflower blue color. - + Cornsilk color. - + Crimson color. Cyan color. - + Dark blue color. - + Dark cyan color. - + Dark goldenrod color. - + Dark gray color. - + Dark green color. - + Dark khaki color. - + Dark magenta color. - + Dark olive green color. - + Dark orange color. - + Dark orchid color. - + Dark red color. - + Dark salmon color. - + Dark sea green color. - + Dark slate blue color. - + Dark slate gray color. - + Dark turquoise color. - + Dark violet color. - + Deep pink color. - + Deep sky blue color. - + Dim gray color. - + Dodger blue color. - + Firebrick color. - + Floral white color. - + Forest green color. Fuchsia color. - + Gainsboro color. Ghost white color. - + Gold color. - + Goldenrod color. - + Gray color. Green color. - + Green yellow color. - + Honeydew color. - + Hot pink color. - + Indian red color. - + Indigo color. - + Ivory color. - + Khaki color. - + Lavender color. - + Lavender blush color. - + Lawn green color. - + Lemon chiffon color. - + Light blue color. - + Light coral color. - + Light cyan color. - + Light goldenrod color. Light gray color. - + Light green color. - + Light pink color. - + Light salmon color. - + Light sea green color. - + Light sky blue color. - + Light slate gray color. - + Light steel blue color. - + Light yellow color. Lime color. - + Lime green color. - + Linen color. Magenta color. - + Maroon color. - + Medium aquamarine color. - + Medium blue color. - + Medium orchid color. - + Medium purple color. - + Medium sea green color. - + Medium slate blue color. - + Medium spring green color. - + Medium turquoise color. - + Medium violet red color. - + Midnight blue color. - + Mint cream color. - + Misty rose color. - + Moccasin color. - + Navajo white color. - + Navy blue color. - + Old lace color. - + Olive color. - + Olive drab color. - + Orange color. - + Orange red color. - + Orchid color. - + Pale goldenrod color. - + Pale green color. - + Pale turquoise color. - + Pale violet red color. - + Papaya whip color. - + Peach puff color. - + Peru color. - + Pink color. - + Plum color. - + Powder blue color. - + Purple color. @@ -883,97 +883,97 @@ Red color. - + Rosy brown color. - + Royal blue color. - + Saddle brown color. - + Salmon color. - + Sandy brown color. - + Sea green color. - + Seashell color. - + Sienna color. - + Silver color. - + Sky blue color. - + Slate blue color. - + Slate gray color. - + Snow color. - + Spring green color. - + Steel blue color. - + Tan color. - + Teal color. - + Thistle color. - + Tomato color. Transparent color (white with zero alpha). - + Turquoise color. - + Violet color. - + Web gray color. - + Web green color. - + Web maroon color. - + Web purple color. - + Wheat color. White color. - + White smoke color. Yellow color. - + Yellow green color. diff --git a/doc/classes/ConeTwistJoint3D.xml b/doc/classes/ConeTwistJoint3D.xml index 632b512497..d95c9eb1f1 100644 --- a/doc/classes/ConeTwistJoint3D.xml +++ b/doc/classes/ConeTwistJoint3D.xml @@ -36,13 +36,13 @@ The ease with which the joint starts to twist. If it's too low, it takes more force to start twisting the joint. - + Swing is rotation from side to side, around the axis perpendicular to the twist axis. The swing span defines, how much rotation will not get corrected along the swing axis. Could be defined as looseness in the [ConeTwistJoint3D]. If below 0.05, this behavior is locked. - + Twist is the rotation around the twist axis, this value defined how far the joint can twist. Twist is locked if below 0.05. diff --git a/doc/classes/GraphEdit.xml b/doc/classes/GraphEdit.xml index 39d3af920f..aa4fb33d0c 100644 --- a/doc/classes/GraphEdit.xml +++ b/doc/classes/GraphEdit.xml @@ -397,10 +397,10 @@ The current zoom value. - + The upper zoom limit. - + The lower zoom limit. diff --git a/doc/classes/HingeJoint3D.xml b/doc/classes/HingeJoint3D.xml index f794853caf..72594713e0 100644 --- a/doc/classes/HingeJoint3D.xml +++ b/doc/classes/HingeJoint3D.xml @@ -47,7 +47,7 @@ If [code]true[/code], the hinges maximum and minimum rotation, defined by [member angular_limit/lower] and [member angular_limit/upper] has effects. - + The minimum rotation. Only active if [member angular_limit/enable] is [code]true[/code]. @@ -55,7 +55,7 @@ - + The maximum rotation. Only active if [member angular_limit/enable] is [code]true[/code]. diff --git a/doc/classes/Parallax2D.xml b/doc/classes/Parallax2D.xml index 4c9722d295..18953521d1 100644 --- a/doc/classes/Parallax2D.xml +++ b/doc/classes/Parallax2D.xml @@ -20,10 +20,10 @@ If [code]true[/code], [Parallax2D]'s position is not affected by the position of the camera. - + Top-left limits for scrolling to begin. If the camera is outside of this limit, the [Parallax2D] stops scrolling. Must be lower than [member limit_end] minus the viewport size to work. - + Bottom-right limits for scrolling to end. If the camera is outside of this limit, the [Parallax2D] will stop scrolling. Must be higher than [member limit_begin] and the viewport size combined to work. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index df7401ccd6..d44d4edb13 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -2403,7 +2403,7 @@ If [code]true[/code], the 2D physics server runs on a separate thread, making better use of multi-core CPUs. If [code]false[/code], the 2D physics server runs on the main thread. Running the physics server on a separate thread can increase performance, but restricts API access to only physics process. - + Threshold angular velocity under which a 2D physics body will be considered inactive. See [constant PhysicsServer2D.SPACE_PARAM_BODY_ANGULAR_VELOCITY_SLEEP_THRESHOLD]. @@ -2486,7 +2486,7 @@ If [code]true[/code], the 3D physics server runs on a separate thread, making better use of multi-core CPUs. If [code]false[/code], the 3D physics server runs on the main thread. Running the physics server on a separate thread can increase performance, but restricts API access to only physics process. [b]Note:[/b] When [member physics/3d/physics_engine] is set to [code]Jolt Physics[/code], enabling this setting will prevent the 3D physics server from being able to provide any context when reporting errors and warnings, and will instead always refer to nodes as [code]<unknown>[/code]. - + Threshold angular velocity under which a 3D physics body will be considered inactive. See [constant PhysicsServer3D.SPACE_PARAM_BODY_ANGULAR_VELOCITY_SLEEP_THRESHOLD]. @@ -2535,7 +2535,7 @@ [b]Note:[/b] This property is only read when the project starts. To change the physics FPS at runtime, set [member Engine.physics_ticks_per_second] instead. [b]Note:[/b] Only [member physics/common/max_physics_steps_per_frame] physics ticks may be simulated per rendered frame at most. If more physics ticks have to be simulated per rendered frame to keep up with rendering, the project will appear to slow down (even if [code]delta[/code] is used consistently in physics calculations). Therefore, it is recommended to also increase [member physics/common/max_physics_steps_per_frame] if increasing [member physics/common/physics_ticks_per_second] significantly above its default value. - + The maximum angle, in radians, between two adjacent triangles in a [ConcavePolygonShape3D] or [HeightMapShape3D] for which the edge between those triangles is considered inactive. Collisions against an inactive edge will have its normal overridden to instead be the surface normal of the triangle. This can help alleviate ghost collisions. [b]Note:[/b] Setting this too high can result in objects not depenetrating properly. @@ -2551,7 +2551,7 @@ Which of the two nodes bound by a joint should represent the world when one of the two is omitted, as either [member Joint3D.node_a] or [member Joint3D.node_b]. This can be thought of as having the omitted node be a [StaticBody3D] at the joint's position. Joint limits are more easily expressed when [member Joint3D.node_a] represents the world. [b]Note:[/b] In Godot Physics, only [member Joint3D.node_b] can represent the world. - + The maximum angular velocity that a [RigidBody3D] can reach, in radians per second. This is mainly used as a fail-safe, to prevent the simulation from exploding, as fast-moving objects colliding with complex physics structures can otherwise cause them to go out of control. Fast-moving objects can also cause a lot of stress on the collision detection system, which can slow down the simulation considerably. @@ -2611,7 +2611,7 @@ How much of the position error of a [RigidBody3D] to fix during a physics step, where [code]0.0[/code] is none and [code]1.0[/code] is the full amount. This affects things like how quickly bodies depenetrate. [b]Note:[/b] Setting this value too high can make [RigidBody3D] nodes unstable. - + The maximum relative angle by which a body pair can move and still reuse the collision results from the previous physics step, in radians. diff --git a/editor/doc_tools.cpp b/editor/doc_tools.cpp index 80ccfebce6..9a20b0114e 100644 --- a/editor/doc_tools.cpp +++ b/editor/doc_tools.cpp @@ -33,6 +33,7 @@ #include "core/config/engine.h" #include "core/config/project_settings.h" #include "core/core_constants.h" +#include "core/doc_data.h" #include "core/io/compression.h" #include "core/io/dir_access.h" #include "core/io/resource_importer.h" @@ -930,7 +931,7 @@ void DocTools::generate(BitField p_flags) { DocData::ConstantDoc constant; constant.name = E; Variant value = Variant::get_constant_value(Variant::Type(i), E); - constant.value = value.get_type() == Variant::INT ? itos(value) : value.get_construct_string().replace_char('\n', ' '); + constant.value = DocData::get_default_value_string(value); constant.is_value_valid = true; constant.type = Variant::get_type_name(value.get_type()); c.constants.push_back(constant); diff --git a/misc/extension_api_validation/4.4-stable.expected b/misc/extension_api_validation/4.4-stable.expected index 33e335ff50..33e021c399 100644 --- a/misc/extension_api_validation/4.4-stable.expected +++ b/misc/extension_api_validation/4.4-stable.expected @@ -111,3 +111,143 @@ Validate extension JSON: Error: Field 'classes/EditorUndoRedoManager/methods/cre Validate extension JSON: Error: Field 'classes/EditorUndoRedoManager/methods/create_action/arguments': size changed value in new API, from 3 to 5. New argument added. Compatibility method registered. + + +GH-98750 +-------- +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/ALICE_BLUE': value changed value in new API, from "Color(0.941176, 0.972549, 1, 1)" to "Color(0.9411765, 0.972549, 1, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/ANTIQUE_WHITE': value changed value in new API, from "Color(0.980392, 0.921569, 0.843137, 1)" to "Color(0.98039216, 0.92156863, 0.84313726, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/AQUAMARINE': value changed value in new API, from "Color(0.498039, 1, 0.831373, 1)" to "Color(0.49803922, 1, 0.83137256, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/AZURE': value changed value in new API, from "Color(0.941176, 1, 1, 1)" to "Color(0.9411765, 1, 1, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/BEIGE': value changed value in new API, from "Color(0.960784, 0.960784, 0.862745, 1)" to "Color(0.9607843, 0.9607843, 0.8627451, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/BISQUE': value changed value in new API, from "Color(1, 0.894118, 0.768627, 1)" to "Color(1, 0.89411765, 0.76862746, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/BLANCHED_ALMOND': value changed value in new API, from "Color(1, 0.921569, 0.803922, 1)" to "Color(1, 0.92156863, 0.8039216, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/BLUE_VIOLET': value changed value in new API, from "Color(0.541176, 0.168627, 0.886275, 1)" to "Color(0.5411765, 0.16862746, 0.8862745, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/BROWN': value changed value in new API, from "Color(0.647059, 0.164706, 0.164706, 1)" to "Color(0.64705884, 0.16470589, 0.16470589, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/BURLYWOOD': value changed value in new API, from "Color(0.870588, 0.721569, 0.529412, 1)" to "Color(0.87058824, 0.72156864, 0.5294118, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/CADET_BLUE': value changed value in new API, from "Color(0.372549, 0.619608, 0.627451, 1)" to "Color(0.37254903, 0.61960787, 0.627451, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/CHARTREUSE': value changed value in new API, from "Color(0.498039, 1, 0, 1)" to "Color(0.49803922, 1, 0, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/CHOCOLATE': value changed value in new API, from "Color(0.823529, 0.411765, 0.117647, 1)" to "Color(0.8235294, 0.4117647, 0.11764706, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/CORAL': value changed value in new API, from "Color(1, 0.498039, 0.313726, 1)" to "Color(1, 0.49803922, 0.3137255, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/CORNFLOWER_BLUE': value changed value in new API, from "Color(0.392157, 0.584314, 0.929412, 1)" to "Color(0.39215687, 0.58431375, 0.92941177, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/CORNSILK': value changed value in new API, from "Color(1, 0.972549, 0.862745, 1)" to "Color(1, 0.972549, 0.8627451, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/CRIMSON': value changed value in new API, from "Color(0.862745, 0.0784314, 0.235294, 1)" to "Color(0.8627451, 0.078431375, 0.23529412, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_BLUE': value changed value in new API, from "Color(0, 0, 0.545098, 1)" to "Color(0, 0, 0.54509807, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_CYAN': value changed value in new API, from "Color(0, 0.545098, 0.545098, 1)" to "Color(0, 0.54509807, 0.54509807, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_GOLDENROD': value changed value in new API, from "Color(0.721569, 0.52549, 0.0431373, 1)" to "Color(0.72156864, 0.5254902, 0.043137256, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_GRAY': value changed value in new API, from "Color(0.662745, 0.662745, 0.662745, 1)" to "Color(0.6627451, 0.6627451, 0.6627451, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_GREEN': value changed value in new API, from "Color(0, 0.392157, 0, 1)" to "Color(0, 0.39215687, 0, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_KHAKI': value changed value in new API, from "Color(0.741176, 0.717647, 0.419608, 1)" to "Color(0.7411765, 0.7176471, 0.41960785, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_MAGENTA': value changed value in new API, from "Color(0.545098, 0, 0.545098, 1)" to "Color(0.54509807, 0, 0.54509807, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_OLIVE_GREEN': value changed value in new API, from "Color(0.333333, 0.419608, 0.184314, 1)" to "Color(0.33333334, 0.41960785, 0.18431373, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_ORANGE': value changed value in new API, from "Color(1, 0.54902, 0, 1)" to "Color(1, 0.54901963, 0, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_ORCHID': value changed value in new API, from "Color(0.6, 0.196078, 0.8, 1)" to "Color(0.6, 0.19607843, 0.8, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_RED': value changed value in new API, from "Color(0.545098, 0, 0, 1)" to "Color(0.54509807, 0, 0, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_SALMON': value changed value in new API, from "Color(0.913725, 0.588235, 0.478431, 1)" to "Color(0.9137255, 0.5882353, 0.47843137, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_SEA_GREEN': value changed value in new API, from "Color(0.560784, 0.737255, 0.560784, 1)" to "Color(0.56078434, 0.7372549, 0.56078434, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_SLATE_BLUE': value changed value in new API, from "Color(0.282353, 0.239216, 0.545098, 1)" to "Color(0.28235295, 0.23921569, 0.54509807, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_SLATE_GRAY': value changed value in new API, from "Color(0.184314, 0.309804, 0.309804, 1)" to "Color(0.18431373, 0.30980393, 0.30980393, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_TURQUOISE': value changed value in new API, from "Color(0, 0.807843, 0.819608, 1)" to "Color(0, 0.80784315, 0.81960785, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DARK_VIOLET': value changed value in new API, from "Color(0.580392, 0, 0.827451, 1)" to "Color(0.5803922, 0, 0.827451, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DEEP_PINK': value changed value in new API, from "Color(1, 0.0784314, 0.576471, 1)" to "Color(1, 0.078431375, 0.5764706, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DEEP_SKY_BLUE': value changed value in new API, from "Color(0, 0.74902, 1, 1)" to "Color(0, 0.7490196, 1, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DIM_GRAY': value changed value in new API, from "Color(0.411765, 0.411765, 0.411765, 1)" to "Color(0.4117647, 0.4117647, 0.4117647, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/DODGER_BLUE': value changed value in new API, from "Color(0.117647, 0.564706, 1, 1)" to "Color(0.11764706, 0.5647059, 1, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/FIREBRICK': value changed value in new API, from "Color(0.698039, 0.133333, 0.133333, 1)" to "Color(0.69803923, 0.13333334, 0.13333334, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/FLORAL_WHITE': value changed value in new API, from "Color(1, 0.980392, 0.941176, 1)" to "Color(1, 0.98039216, 0.9411765, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/FOREST_GREEN': value changed value in new API, from "Color(0.133333, 0.545098, 0.133333, 1)" to "Color(0.13333334, 0.54509807, 0.13333334, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/GAINSBORO': value changed value in new API, from "Color(0.862745, 0.862745, 0.862745, 1)" to "Color(0.8627451, 0.8627451, 0.8627451, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/GOLD': value changed value in new API, from "Color(1, 0.843137, 0, 1)" to "Color(1, 0.84313726, 0, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/GOLDENROD': value changed value in new API, from "Color(0.854902, 0.647059, 0.12549, 1)" to "Color(0.85490197, 0.64705884, 0.1254902, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/GRAY': value changed value in new API, from "Color(0.745098, 0.745098, 0.745098, 1)" to "Color(0.74509805, 0.74509805, 0.74509805, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/GREEN_YELLOW': value changed value in new API, from "Color(0.678431, 1, 0.184314, 1)" to "Color(0.6784314, 1, 0.18431373, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/HONEYDEW': value changed value in new API, from "Color(0.941176, 1, 0.941176, 1)" to "Color(0.9411765, 1, 0.9411765, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/HOT_PINK': value changed value in new API, from "Color(1, 0.411765, 0.705882, 1)" to "Color(1, 0.4117647, 0.7058824, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/INDIAN_RED': value changed value in new API, from "Color(0.803922, 0.360784, 0.360784, 1)" to "Color(0.8039216, 0.36078432, 0.36078432, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/INDIGO': value changed value in new API, from "Color(0.294118, 0, 0.509804, 1)" to "Color(0.29411766, 0, 0.50980395, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/IVORY': value changed value in new API, from "Color(1, 1, 0.941176, 1)" to "Color(1, 1, 0.9411765, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/KHAKI': value changed value in new API, from "Color(0.941176, 0.901961, 0.54902, 1)" to "Color(0.9411765, 0.9019608, 0.54901963, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LAVENDER': value changed value in new API, from "Color(0.901961, 0.901961, 0.980392, 1)" to "Color(0.9019608, 0.9019608, 0.98039216, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LAVENDER_BLUSH': value changed value in new API, from "Color(1, 0.941176, 0.960784, 1)" to "Color(1, 0.9411765, 0.9607843, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LAWN_GREEN': value changed value in new API, from "Color(0.486275, 0.988235, 0, 1)" to "Color(0.4862745, 0.9882353, 0, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LEMON_CHIFFON': value changed value in new API, from "Color(1, 0.980392, 0.803922, 1)" to "Color(1, 0.98039216, 0.8039216, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_BLUE': value changed value in new API, from "Color(0.678431, 0.847059, 0.901961, 1)" to "Color(0.6784314, 0.84705883, 0.9019608, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_CORAL': value changed value in new API, from "Color(0.941176, 0.501961, 0.501961, 1)" to "Color(0.9411765, 0.5019608, 0.5019608, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_CYAN': value changed value in new API, from "Color(0.878431, 1, 1, 1)" to "Color(0.8784314, 1, 1, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_GOLDENROD': value changed value in new API, from "Color(0.980392, 0.980392, 0.823529, 1)" to "Color(0.98039216, 0.98039216, 0.8235294, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_GREEN': value changed value in new API, from "Color(0.564706, 0.933333, 0.564706, 1)" to "Color(0.5647059, 0.93333334, 0.5647059, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_PINK': value changed value in new API, from "Color(1, 0.713726, 0.756863, 1)" to "Color(1, 0.7137255, 0.75686276, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_SALMON': value changed value in new API, from "Color(1, 0.627451, 0.478431, 1)" to "Color(1, 0.627451, 0.47843137, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_SEA_GREEN': value changed value in new API, from "Color(0.12549, 0.698039, 0.666667, 1)" to "Color(0.1254902, 0.69803923, 0.6666667, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_SKY_BLUE': value changed value in new API, from "Color(0.529412, 0.807843, 0.980392, 1)" to "Color(0.5294118, 0.80784315, 0.98039216, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_SLATE_GRAY': value changed value in new API, from "Color(0.466667, 0.533333, 0.6, 1)" to "Color(0.46666667, 0.53333336, 0.6, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_STEEL_BLUE': value changed value in new API, from "Color(0.690196, 0.768627, 0.870588, 1)" to "Color(0.6901961, 0.76862746, 0.87058824, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIGHT_YELLOW': value changed value in new API, from "Color(1, 1, 0.878431, 1)" to "Color(1, 1, 0.8784314, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LIME_GREEN': value changed value in new API, from "Color(0.196078, 0.803922, 0.196078, 1)" to "Color(0.19607843, 0.8039216, 0.19607843, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/LINEN': value changed value in new API, from "Color(0.980392, 0.941176, 0.901961, 1)" to "Color(0.98039216, 0.9411765, 0.9019608, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MAROON': value changed value in new API, from "Color(0.690196, 0.188235, 0.376471, 1)" to "Color(0.6901961, 0.1882353, 0.3764706, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MEDIUM_AQUAMARINE': value changed value in new API, from "Color(0.4, 0.803922, 0.666667, 1)" to "Color(0.4, 0.8039216, 0.6666667, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MEDIUM_BLUE': value changed value in new API, from "Color(0, 0, 0.803922, 1)" to "Color(0, 0, 0.8039216, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MEDIUM_ORCHID': value changed value in new API, from "Color(0.729412, 0.333333, 0.827451, 1)" to "Color(0.7294118, 0.33333334, 0.827451, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MEDIUM_PURPLE': value changed value in new API, from "Color(0.576471, 0.439216, 0.858824, 1)" to "Color(0.5764706, 0.4392157, 0.85882354, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MEDIUM_SEA_GREEN': value changed value in new API, from "Color(0.235294, 0.701961, 0.443137, 1)" to "Color(0.23529412, 0.7019608, 0.44313726, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MEDIUM_SLATE_BLUE': value changed value in new API, from "Color(0.482353, 0.407843, 0.933333, 1)" to "Color(0.48235294, 0.40784314, 0.93333334, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MEDIUM_SPRING_GREEN': value changed value in new API, from "Color(0, 0.980392, 0.603922, 1)" to "Color(0, 0.98039216, 0.6039216, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MEDIUM_TURQUOISE': value changed value in new API, from "Color(0.282353, 0.819608, 0.8, 1)" to "Color(0.28235295, 0.81960785, 0.8, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MEDIUM_VIOLET_RED': value changed value in new API, from "Color(0.780392, 0.0823529, 0.521569, 1)" to "Color(0.78039217, 0.08235294, 0.52156866, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MIDNIGHT_BLUE': value changed value in new API, from "Color(0.0980392, 0.0980392, 0.439216, 1)" to "Color(0.09803922, 0.09803922, 0.4392157, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MINT_CREAM': value changed value in new API, from "Color(0.960784, 1, 0.980392, 1)" to "Color(0.9607843, 1, 0.98039216, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MISTY_ROSE': value changed value in new API, from "Color(1, 0.894118, 0.882353, 1)" to "Color(1, 0.89411765, 0.88235295, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/MOCCASIN': value changed value in new API, from "Color(1, 0.894118, 0.709804, 1)" to "Color(1, 0.89411765, 0.70980394, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/NAVAJO_WHITE': value changed value in new API, from "Color(1, 0.870588, 0.678431, 1)" to "Color(1, 0.87058824, 0.6784314, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/NAVY_BLUE': value changed value in new API, from "Color(0, 0, 0.501961, 1)" to "Color(0, 0, 0.5019608, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/OLD_LACE': value changed value in new API, from "Color(0.992157, 0.960784, 0.901961, 1)" to "Color(0.99215686, 0.9607843, 0.9019608, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/OLIVE': value changed value in new API, from "Color(0.501961, 0.501961, 0, 1)" to "Color(0.5019608, 0.5019608, 0, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/OLIVE_DRAB': value changed value in new API, from "Color(0.419608, 0.556863, 0.137255, 1)" to "Color(0.41960785, 0.5568628, 0.13725491, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/ORANGE': value changed value in new API, from "Color(1, 0.647059, 0, 1)" to "Color(1, 0.64705884, 0, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/ORANGE_RED': value changed value in new API, from "Color(1, 0.270588, 0, 1)" to "Color(1, 0.27058825, 0, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/ORCHID': value changed value in new API, from "Color(0.854902, 0.439216, 0.839216, 1)" to "Color(0.85490197, 0.4392157, 0.8392157, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/PALE_GOLDENROD': value changed value in new API, from "Color(0.933333, 0.909804, 0.666667, 1)" to "Color(0.93333334, 0.9098039, 0.6666667, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/PALE_GREEN': value changed value in new API, from "Color(0.596078, 0.984314, 0.596078, 1)" to "Color(0.59607846, 0.9843137, 0.59607846, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/PALE_TURQUOISE': value changed value in new API, from "Color(0.686275, 0.933333, 0.933333, 1)" to "Color(0.6862745, 0.93333334, 0.93333334, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/PALE_VIOLET_RED': value changed value in new API, from "Color(0.858824, 0.439216, 0.576471, 1)" to "Color(0.85882354, 0.4392157, 0.5764706, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/PAPAYA_WHIP': value changed value in new API, from "Color(1, 0.937255, 0.835294, 1)" to "Color(1, 0.9372549, 0.8352941, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/PEACH_PUFF': value changed value in new API, from "Color(1, 0.854902, 0.72549, 1)" to "Color(1, 0.85490197, 0.7254902, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/PERU': value changed value in new API, from "Color(0.803922, 0.521569, 0.247059, 1)" to "Color(0.8039216, 0.52156866, 0.24705882, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/PINK': value changed value in new API, from "Color(1, 0.752941, 0.796078, 1)" to "Color(1, 0.7529412, 0.79607844, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/PLUM': value changed value in new API, from "Color(0.866667, 0.627451, 0.866667, 1)" to "Color(0.8666667, 0.627451, 0.8666667, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/POWDER_BLUE': value changed value in new API, from "Color(0.690196, 0.878431, 0.901961, 1)" to "Color(0.6901961, 0.8784314, 0.9019608, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/PURPLE': value changed value in new API, from "Color(0.627451, 0.12549, 0.941176, 1)" to "Color(0.627451, 0.1254902, 0.9411765, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/ROSY_BROWN': value changed value in new API, from "Color(0.737255, 0.560784, 0.560784, 1)" to "Color(0.7372549, 0.56078434, 0.56078434, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/ROYAL_BLUE': value changed value in new API, from "Color(0.254902, 0.411765, 0.882353, 1)" to "Color(0.25490198, 0.4117647, 0.88235295, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SADDLE_BROWN': value changed value in new API, from "Color(0.545098, 0.270588, 0.0745098, 1)" to "Color(0.54509807, 0.27058825, 0.07450981, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SALMON': value changed value in new API, from "Color(0.980392, 0.501961, 0.447059, 1)" to "Color(0.98039216, 0.5019608, 0.44705883, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SANDY_BROWN': value changed value in new API, from "Color(0.956863, 0.643137, 0.376471, 1)" to "Color(0.95686275, 0.6431373, 0.3764706, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SEASHELL': value changed value in new API, from "Color(1, 0.960784, 0.933333, 1)" to "Color(1, 0.9607843, 0.93333334, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SEA_GREEN': value changed value in new API, from "Color(0.180392, 0.545098, 0.341176, 1)" to "Color(0.18039216, 0.54509807, 0.34117648, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SIENNA': value changed value in new API, from "Color(0.627451, 0.321569, 0.176471, 1)" to "Color(0.627451, 0.32156864, 0.1764706, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SILVER': value changed value in new API, from "Color(0.752941, 0.752941, 0.752941, 1)" to "Color(0.7529412, 0.7529412, 0.7529412, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SKY_BLUE': value changed value in new API, from "Color(0.529412, 0.807843, 0.921569, 1)" to "Color(0.5294118, 0.80784315, 0.92156863, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SLATE_BLUE': value changed value in new API, from "Color(0.415686, 0.352941, 0.803922, 1)" to "Color(0.41568628, 0.3529412, 0.8039216, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SLATE_GRAY': value changed value in new API, from "Color(0.439216, 0.501961, 0.564706, 1)" to "Color(0.4392157, 0.5019608, 0.5647059, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SNOW': value changed value in new API, from "Color(1, 0.980392, 0.980392, 1)" to "Color(1, 0.98039216, 0.98039216, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/SPRING_GREEN': value changed value in new API, from "Color(0, 1, 0.498039, 1)" to "Color(0, 1, 0.49803922, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/STEEL_BLUE': value changed value in new API, from "Color(0.27451, 0.509804, 0.705882, 1)" to "Color(0.27450982, 0.50980395, 0.7058824, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/TAN': value changed value in new API, from "Color(0.823529, 0.705882, 0.54902, 1)" to "Color(0.8235294, 0.7058824, 0.54901963, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/TEAL': value changed value in new API, from "Color(0, 0.501961, 0.501961, 1)" to "Color(0, 0.5019608, 0.5019608, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/THISTLE': value changed value in new API, from "Color(0.847059, 0.74902, 0.847059, 1)" to "Color(0.84705883, 0.7490196, 0.84705883, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/TOMATO': value changed value in new API, from "Color(1, 0.388235, 0.278431, 1)" to "Color(1, 0.3882353, 0.2784314, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/TURQUOISE': value changed value in new API, from "Color(0.25098, 0.878431, 0.815686, 1)" to "Color(0.2509804, 0.8784314, 0.8156863, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/VIOLET': value changed value in new API, from "Color(0.933333, 0.509804, 0.933333, 1)" to "Color(0.93333334, 0.50980395, 0.93333334, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/WEB_GRAY': value changed value in new API, from "Color(0.501961, 0.501961, 0.501961, 1)" to "Color(0.5019608, 0.5019608, 0.5019608, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/WEB_GREEN': value changed value in new API, from "Color(0, 0.501961, 0, 1)" to "Color(0, 0.5019608, 0, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/WEB_MAROON': value changed value in new API, from "Color(0.501961, 0, 0, 1)" to "Color(0.5019608, 0, 0, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/WEB_PURPLE': value changed value in new API, from "Color(0.501961, 0, 0.501961, 1)" to "Color(0.5019608, 0, 0.5019608, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/WHEAT': value changed value in new API, from "Color(0.960784, 0.870588, 0.701961, 1)" to "Color(0.9607843, 0.87058824, 0.7019608, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/WHITE_SMOKE': value changed value in new API, from "Color(0.960784, 0.960784, 0.960784, 1)" to "Color(0.9607843, 0.9607843, 0.9607843, 1)". +Validate extension JSON: Error: Field 'builtin_classes/Color/constants/YELLOW_GREEN': value changed value in new API, from "Color(0.603922, 0.803922, 0.196078, 1)" to "Color(0.6039216, 0.8039216, 0.19607843, 1)". +Validate extension JSON: Error: Field 'classes/InputMap/methods/add_action/arguments/1': default_value changed value in new API, from "0.2" to "0.20000000298023224". +Validate extension JSON: Error: Field 'classes/InputMap/methods/add_action/arguments/1': default_value changed value in new API, from "0.5" to "0.20000000298023224". +Validate extension JSON: Error: Field 'global_enums/KeyModifierMask/values/KEY_MODIFIER_MASK': value changed value in new API, from 532676600.0 to 2130706432. + +Precision of string-serialized Variant constants increased. diff --git a/modules/gltf/doc_classes/GLTFCamera.xml b/modules/gltf/doc_classes/GLTFCamera.xml index 12334683ba..57ddd0bde4 100644 --- a/modules/gltf/doc_classes/GLTFCamera.xml +++ b/modules/gltf/doc_classes/GLTFCamera.xml @@ -46,7 +46,7 @@ The distance to the near culling boundary for this camera relative to its local Z axis, in meters. This maps to glTF's [code]znear[/code] property. - + The FOV of the camera. This class and glTF define the camera FOV in radians, while Godot uses degrees. This maps to glTF's [code]yfov[/code] property. This value is only used for perspective cameras, when [member perspective] is [code]true[/code]. diff --git a/modules/gltf/doc_classes/GLTFLight.xml b/modules/gltf/doc_classes/GLTFLight.xml index e87c123b03..062125e2f5 100644 --- a/modules/gltf/doc_classes/GLTFLight.xml +++ b/modules/gltf/doc_classes/GLTFLight.xml @@ -66,7 +66,7 @@ The type of the light. The values accepted by Godot are "point", "spot", and "directional", which correspond to Godot's [OmniLight3D], [SpotLight3D], and [DirectionalLight3D] respectively. - + The outer angle of the cone in a spotlight. Must be greater than or equal to the inner angle. At this angle, the light drops off to zero brightness. Between the inner and outer cone angles, there is a transition from full brightness to zero brightness. If this angle is a half turn, then the spotlight emits in all directions. When creating a Godot [SpotLight3D], the outer cone angle is used as the angle of the spotlight. diff --git a/modules/openxr/doc_classes/OpenXRCompositionLayerCylinder.xml b/modules/openxr/doc_classes/OpenXRCompositionLayerCylinder.xml index dd8a11e2b9..7e2e6b62fd 100644 --- a/modules/openxr/doc_classes/OpenXRCompositionLayerCylinder.xml +++ b/modules/openxr/doc_classes/OpenXRCompositionLayerCylinder.xml @@ -12,7 +12,7 @@ The aspect ratio of the slice. Used to set the height relative to the width. - + The central angle of the cylinder. Used to set the width. diff --git a/modules/openxr/doc_classes/OpenXRCompositionLayerEquirect.xml b/modules/openxr/doc_classes/OpenXRCompositionLayerEquirect.xml index 716ea72854..945eab2a04 100644 --- a/modules/openxr/doc_classes/OpenXRCompositionLayerEquirect.xml +++ b/modules/openxr/doc_classes/OpenXRCompositionLayerEquirect.xml @@ -9,19 +9,19 @@ - + The central horizontal angle of the sphere. Used to set the width. The number of segments to use in the fallback mesh. - + The lower vertical angle of the sphere. Used (together with [member upper_vertical_angle]) to set the height. The radius of the sphere. - + The upper vertical angle of the sphere. Used (together with [member lower_vertical_angle]) to set the height. diff --git a/modules/openxr/doc_classes/OpenXRDpadBindingModifier.xml b/modules/openxr/doc_classes/OpenXRDpadBindingModifier.xml index 247141723d..4e55ff5512 100644 --- a/modules/openxr/doc_classes/OpenXRDpadBindingModifier.xml +++ b/modules/openxr/doc_classes/OpenXRDpadBindingModifier.xml @@ -36,7 +36,7 @@ When our input value falls below this, our output becomes [code]false[/code]. - + The angle of each wedge that identifies the 4 directions of the emulated dpad. diff --git a/tests/core/string/test_string.h b/tests/core/string/test_string.h index 5595f2ed5d..1cc507213f 100644 --- a/tests/core/string/test_string.h +++ b/tests/core/string/test_string.h @@ -546,7 +546,6 @@ TEST_CASE("[String] Number to string") { CHECK(String::num(3.141593) == "3.141593"); CHECK(String::num(3.141593, 3) == "3.142"); CHECK(String::num(42.100023, 4) == "42.1"); // No trailing zeros. - CHECK(String::num_scientific(30000000) == "3e+07"); // String::num_int64 tests. CHECK(String::num_int64(3141593) == "3141593"); @@ -567,6 +566,20 @@ TEST_CASE("[String] Number to string") { CHECK(String::num_uint64(4294967295, 37) == ""); // Invalid base > 36. ERR_PRINT_ON; + // String::num_scientific tests. + CHECK(String::num_scientific(30000000.0) == "30000000"); + CHECK(String::num_scientific(1234567890.0) == "1234567890"); + CHECK(String::num_scientific(3e100) == "3e+100"); + CHECK(String::num_scientific(7e-100) == "7e-100"); + CHECK(String::num_scientific(Math::TAU) == "6.283185307179586"); + CHECK(String::num_scientific(Math::INF) == "inf"); + CHECK(String::num_scientific(-Math::INF) == "-inf"); + CHECK(String::num_scientific(Math::NaN) == "nan"); + CHECK(String::num_scientific(2.0) == "2"); + CHECK(String::num_scientific(1.0) == "1"); + CHECK(String::num_scientific(0.0) == "0"); + CHECK(String::num_scientific(-0.0) == "-0"); + // String::num_real tests. CHECK(String::num_real(1.0) == "1.0"); CHECK(String::num_real(1.0, false) == "1"); diff --git a/tests/core/variant/test_variant.h b/tests/core/variant/test_variant.h index 384b8cebeb..e5b92744ad 100644 --- a/tests/core/variant/test_variant.h +++ b/tests/core/variant/test_variant.h @@ -101,7 +101,7 @@ TEST_CASE("[Variant] Writer and parser Variant::FLOAT") { String a64_str; VariantWriter::write_to_string(a64, a64_str); - CHECK_MESSAGE(a64_str == "1.79769e+308", "Writes in scientific notation."); + CHECK_MESSAGE(a64_str == "1.7976931348623157e+308", "Writes in scientific notation."); CHECK_MESSAGE(a64_str != "inf", "Should not overflow."); CHECK_MESSAGE(a64_str != "nan", "The result should be defined."); @@ -115,7 +115,7 @@ TEST_CASE("[Variant] Writer and parser Variant::FLOAT") { VariantParser::parse(&bss, variant_parsed, errs, line); float_parsed = variant_parsed; // Loses precision, but that's alright. - CHECK_MESSAGE(float_parsed == 1.79769e+308, "Should parse back."); + CHECK_MESSAGE(float_parsed == 1.797693134862315708145274237317e+308, "Should parse back."); // Approximation of Googol with a double-precision float. VariantParser::StreamString css; @@ -1754,6 +1754,38 @@ TEST_CASE("[Variant] array initializer list") { CHECK(packed_arr[2] == 0); } +TEST_CASE("[Variant] Writer and parser Vector2") { + Variant vec2_parsed; + String vec2_str; + String errs; + int line; + // Variant::VECTOR2 and Vector2 can be either 32-bit or 64-bit depending on the precision level of real_t. + { + Vector2 vec2 = Vector2(1.2, 3.4); + VariantWriter::write_to_string(vec2, vec2_str); + // Reminder: "1.2" and "3.4" are not exactly those decimal numbers. They are the closest float to them. + CHECK_MESSAGE(vec2_str == "Vector2(1.2, 3.4)", "Should write with enough digits to ensure parsing back is exact."); + VariantParser::StreamString stream; + stream.s = vec2_str; + VariantParser::parse(&stream, vec2_parsed, errs, line); + CHECK_MESSAGE(Vector2(vec2_parsed) == vec2, "Should parse back to the same Vector2."); + } + // Check with big numbers and small numbers. + { + Vector2 vec2 = Vector2(1.234567898765432123456789e30, 1.234567898765432123456789e-10); + VariantWriter::write_to_string(vec2, vec2_str); +#ifdef REAL_T_IS_DOUBLE + CHECK_MESSAGE(vec2_str == "Vector2(1.2345678987654322e+30, 1.2345678987654322e-10)", "Should write with enough digits to ensure parsing back is exact."); +#else + CHECK_MESSAGE(vec2_str == "Vector2(1.2345679e+30, 1.2345679e-10)", "Should write with enough digits to ensure parsing back is exact."); +#endif + VariantParser::StreamString stream; + stream.s = vec2_str; + VariantParser::parse(&stream, vec2_parsed, errs, line); + CHECK_MESSAGE(Vector2(vec2_parsed) == vec2, "Should parse back to the same Vector2."); + } +} + TEST_CASE("[Variant] Writer and parser array") { Array a = build_array(1, String("hello"), build_array(Variant())); String a_str; diff --git a/thirdparty/README.md b/thirdparty/README.md index 5cce218f43..90f516090d 100644 --- a/thirdparty/README.md +++ b/thirdparty/README.md @@ -415,6 +415,21 @@ Files extracted from upstream source: - `COPYING` +## grisu2 + +- Upstream: https://github.com/simdjson/simdjson/blob/master/src/to_chars.cpp +- Version: git (4f4e81668ecb9d4d37fd5f59a1556d492507421d, 2023) +- License: Apache and MIT + +Files extracted from upstream source: + +- The `src/to_chars.cpp` file renamed to `grisu2.h` and slightly modified. + +Patches: + +- `0001-godot-changes.patch` (GH-98750) + + ## harfbuzz - Upstream: https://github.com/harfbuzz/harfbuzz diff --git a/thirdparty/grisu2/LICENSE b/thirdparty/grisu2/LICENSE new file mode 100644 index 0000000000..491c6b1dbe --- /dev/null +++ b/thirdparty/grisu2/LICENSE @@ -0,0 +1,47 @@ +The Grisu2 algorithm is by Florian Loitsch, based on the work of Robert G. Burger and R. Kent Dybvig: + + [1] Loitsch, "Printing Floating-Point Numbers Quickly and Accurately with Integers", Proceedings of the ACM SIGPLAN 2010 Conference on Programming Language Design and Implementation, PLDI 2010 + [2] Burger, Dybvig, "Printing Floating-Point Numbers Quickly and Accurately", Proceedings of the ACM SIGPLAN 1996 Conference on Programming Language Design and Implementation, PLDI 1996 + + +The original C implementation is by Florian Loitsch: + + Copyright (c) 2009 Florian Loitsch + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + +The implementation simplified and adapted to JSON and C++11 by Daniel Lemire as part of simdjson: + + Copyright 2018-2023 The simdjson authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/thirdparty/grisu2/README.md b/thirdparty/grisu2/README.md new file mode 100644 index 0000000000..0cbb8f1ae1 --- /dev/null +++ b/thirdparty/grisu2/README.md @@ -0,0 +1,20 @@ +# Grisu2 + +This is a C++11 implementation of the Grisu2 algorithm for converting floating-point numbers to decimal strings. + +The Grisu2 algorithm is by Florian Loitsch, based on the work of Robert G. Burger and R. Kent Dybvig: + +- https://dl.acm.org/doi/10.1145/1806596.1806623 [1] Loitsch, "Printing Floating-Point Numbers Quickly and Accurately with Integers", Proceedings of the ACM SIGPLAN 2010 Conference on Programming Language Design and Implementation, PLDI 2010 +- https://dl.acm.org/doi/10.1145/231379.231397 [2] Burger, Dybvig, "Printing Floating-Point Numbers Quickly and Accurately", Proceedings of the ACM SIGPLAN 1996 Conference on Programming Language Design and Implementation, PLDI 1996 + +The original C implementation is by Florian Loitsch: +- https://drive.google.com/file/d/0BwvYOx00EwKmejFIMjRORTFLcTA/view?resourcekey=0-1Lg8tXTC_JAODUcFpMcaTA + +The implementation simplified and adapted to JSON and C++11 by Daniel Lemire as part of simdjson: +- https://github.com/simdjson/simdjson/blob/master/src/to_chars.cpp + +The `grisu2.h` file is the same as `to_chars.cpp` but with `godot.patch` applied to it, with the following changes: +- Simplify namespaces to just be one `grisu2` namespace. +- Rename functions to ensure their names are unique. +- Make `to_chars` handle both float and double types instead of just double. +- Remove the trailing `.0` logic to match Godot's existing `String::num_scientific` behavior. diff --git a/thirdparty/grisu2/grisu2.h b/thirdparty/grisu2/grisu2.h new file mode 100644 index 0000000000..4a1300c17e --- /dev/null +++ b/thirdparty/grisu2/grisu2.h @@ -0,0 +1,936 @@ +#pragma once + +#include +#include +#include +#include + +namespace grisu2 { +/*! +implements the Grisu2 algorithm for binary to decimal floating-point +conversion. +Adapted from JSON for Modern C++ + +This implementation is a slightly modified version of the reference +implementation which may be obtained from +http://florian.loitsch.com/publications (bench.tar.gz). +The code is distributed under the MIT license, Copyright (c) 2009 Florian +Loitsch. For a detailed description of the algorithm see: [1] Loitsch, "Printing +Floating-Point Numbers Quickly and Accurately with Integers", Proceedings of the +ACM SIGPLAN 2010 Conference on Programming Language Design and Implementation, +PLDI 2010 [2] Burger, Dybvig, "Printing Floating-Point Numbers Quickly and +Accurately", Proceedings of the ACM SIGPLAN 1996 Conference on Programming +Language Design and Implementation, PLDI 1996 +*/ + +template +Target reinterpret_bits(const Source source) { + static_assert(sizeof(Target) == sizeof(Source), "size mismatch"); + + Target target; + std::memcpy(&target, &source, sizeof(Source)); + return target; +} + +struct diyfp // f * 2^e +{ + static constexpr int kPrecision = 64; // = q + + std::uint64_t f = 0; + int e = 0; + + constexpr diyfp(std::uint64_t f_, int e_) noexcept : f(f_), e(e_) {} + + /*! + @brief returns x - y + @pre x.e == y.e and x.f >= y.f + */ + static diyfp sub(const diyfp &x, const diyfp &y) noexcept { + + return {x.f - y.f, x.e}; + } + + /*! + @brief returns x * y + @note The result is rounded. (Only the upper q bits are returned.) + */ + static diyfp mul(const diyfp &x, const diyfp &y) noexcept { + static_assert(kPrecision == 64, "internal error"); + + // Computes: + // f = round((x.f * y.f) / 2^q) + // e = x.e + y.e + q + + // Emulate the 64-bit * 64-bit multiplication: + // + // p = u * v + // = (u_lo + 2^32 u_hi) (v_lo + 2^32 v_hi) + // = (u_lo v_lo ) + 2^32 ((u_lo v_hi ) + (u_hi v_lo )) + + // 2^64 (u_hi v_hi ) = (p0 ) + 2^32 ((p1 ) + (p2 )) + // + 2^64 (p3 ) = (p0_lo + 2^32 p0_hi) + 2^32 ((p1_lo + + // 2^32 p1_hi) + (p2_lo + 2^32 p2_hi)) + 2^64 (p3 ) = + // (p0_lo ) + 2^32 (p0_hi + p1_lo + p2_lo ) + 2^64 (p1_hi + + // p2_hi + p3) = (p0_lo ) + 2^32 (Q ) + 2^64 (H ) = (p0_lo ) + + // 2^32 (Q_lo + 2^32 Q_hi ) + 2^64 (H ) + // + // (Since Q might be larger than 2^32 - 1) + // + // = (p0_lo + 2^32 Q_lo) + 2^64 (Q_hi + H) + // + // (Q_hi + H does not overflow a 64-bit int) + // + // = p_lo + 2^64 p_hi + + const std::uint64_t u_lo = x.f & 0xFFFFFFFFu; + const std::uint64_t u_hi = x.f >> 32u; + const std::uint64_t v_lo = y.f & 0xFFFFFFFFu; + const std::uint64_t v_hi = y.f >> 32u; + + const std::uint64_t p0 = u_lo * v_lo; + const std::uint64_t p1 = u_lo * v_hi; + const std::uint64_t p2 = u_hi * v_lo; + const std::uint64_t p3 = u_hi * v_hi; + + const std::uint64_t p0_hi = p0 >> 32u; + const std::uint64_t p1_lo = p1 & 0xFFFFFFFFu; + const std::uint64_t p1_hi = p1 >> 32u; + const std::uint64_t p2_lo = p2 & 0xFFFFFFFFu; + const std::uint64_t p2_hi = p2 >> 32u; + + std::uint64_t Q = p0_hi + p1_lo + p2_lo; + + // The full product might now be computed as + // + // p_hi = p3 + p2_hi + p1_hi + (Q >> 32) + // p_lo = p0_lo + (Q << 32) + // + // But in this particular case here, the full p_lo is not required. + // Effectively we only need to add the highest bit in p_lo to p_hi (and + // Q_hi + 1 does not overflow). + + Q += std::uint64_t{1} << (64u - 32u - 1u); // round, ties up + + const std::uint64_t h = p3 + p2_hi + p1_hi + (Q >> 32u); + + return {h, x.e + y.e + 64}; + } + + /*! + @brief normalize x such that the significand is >= 2^(q-1) + @pre x.f != 0 + */ + static diyfp normalize(diyfp x) noexcept { + + while ((x.f >> 63u) == 0) { + x.f <<= 1u; + x.e--; + } + + return x; + } + + /*! + @brief normalize x such that the result has the exponent E + @pre e >= x.e and the upper e - x.e bits of x.f must be zero. + */ + static diyfp normalize_to(const diyfp &x, + const int target_exponent) noexcept { + const int delta = x.e - target_exponent; + + return {x.f << delta, target_exponent}; + } +}; + +struct boundaries { + diyfp w; + diyfp minus; + diyfp plus; +}; + +/*! +Compute the (normalized) diyfp representing the input number 'value' and its +boundaries. +@pre value must be finite and positive +*/ +template boundaries compute_boundaries(FloatType value) { + + // Convert the IEEE representation into a diyfp. + // + // If v is denormal: + // value = 0.F * 2^(1 - bias) = ( F) * 2^(1 - bias - (p-1)) + // If v is normalized: + // value = 1.F * 2^(E - bias) = (2^(p-1) + F) * 2^(E - bias - (p-1)) + + static_assert(std::numeric_limits::is_iec559, + "internal error: dtoa_short requires an IEEE-754 " + "floating-point implementation"); + + constexpr int kPrecision = + std::numeric_limits::digits; // = p (includes the hidden bit) + constexpr int kBias = + std::numeric_limits::max_exponent - 1 + (kPrecision - 1); + constexpr int kMinExp = 1 - kBias; + constexpr std::uint64_t kHiddenBit = std::uint64_t{1} + << (kPrecision - 1); // = 2^(p-1) + + using bits_type = typename std::conditional::type; + + const std::uint64_t bits = reinterpret_bits(value); + const std::uint64_t E = bits >> (kPrecision - 1); + const std::uint64_t F = bits & (kHiddenBit - 1); + + const bool is_denormal = E == 0; + const diyfp v = is_denormal + ? diyfp(F, kMinExp) + : diyfp(F + kHiddenBit, static_cast(E) - kBias); + + // Compute the boundaries m- and m+ of the floating-point value + // v = f * 2^e. + // + // Determine v- and v+, the floating-point predecessor and successor if v, + // respectively. + // + // v- = v - 2^e if f != 2^(p-1) or e == e_min (A) + // = v - 2^(e-1) if f == 2^(p-1) and e > e_min (B) + // + // v+ = v + 2^e + // + // Let m- = (v- + v) / 2 and m+ = (v + v+) / 2. All real numbers _strictly_ + // between m- and m+ round to v, regardless of how the input rounding + // algorithm breaks ties. + // + // ---+-------------+-------------+-------------+-------------+--- (A) + // v- m- v m+ v+ + // + // -----------------+------+------+-------------+-------------+--- (B) + // v- m- v m+ v+ + + const bool lower_boundary_is_closer = F == 0 && E > 1; + const diyfp m_plus = diyfp(2 * v.f + 1, v.e - 1); + const diyfp m_minus = lower_boundary_is_closer + ? diyfp(4 * v.f - 1, v.e - 2) // (B) + : diyfp(2 * v.f - 1, v.e - 1); // (A) + + // Determine the normalized w+ = m+. + const diyfp w_plus = diyfp::normalize(m_plus); + + // Determine w- = m- such that e_(w-) = e_(w+). + const diyfp w_minus = diyfp::normalize_to(m_minus, w_plus.e); + + return {diyfp::normalize(v), w_minus, w_plus}; +} + +// Given normalized diyfp w, Grisu needs to find a (normalized) cached +// power-of-ten c, such that the exponent of the product c * w = f * 2^e lies +// within a certain range [alpha, gamma] (Definition 3.2 from [1]) +// +// alpha <= e = e_c + e_w + q <= gamma +// +// or +// +// f_c * f_w * 2^alpha <= f_c 2^(e_c) * f_w 2^(e_w) * 2^q +// <= f_c * f_w * 2^gamma +// +// Since c and w are normalized, i.e. 2^(q-1) <= f < 2^q, this implies +// +// 2^(q-1) * 2^(q-1) * 2^alpha <= c * w * 2^q < 2^q * 2^q * 2^gamma +// +// or +// +// 2^(q - 2 + alpha) <= c * w < 2^(q + gamma) +// +// The choice of (alpha,gamma) determines the size of the table and the form of +// the digit generation procedure. Using (alpha,gamma)=(-60,-32) works out well +// in practice: +// +// The idea is to cut the number c * w = f * 2^e into two parts, which can be +// processed independently: An integral part p1, and a fractional part p2: +// +// f * 2^e = ( (f div 2^-e) * 2^-e + (f mod 2^-e) ) * 2^e +// = (f div 2^-e) + (f mod 2^-e) * 2^e +// = p1 + p2 * 2^e +// +// The conversion of p1 into decimal form requires a series of divisions and +// modulos by (a power of) 10. These operations are faster for 32-bit than for +// 64-bit integers, so p1 should ideally fit into a 32-bit integer. This can be +// achieved by choosing +// +// -e >= 32 or e <= -32 := gamma +// +// In order to convert the fractional part +// +// p2 * 2^e = p2 / 2^-e = d[-1] / 10^1 + d[-2] / 10^2 + ... +// +// into decimal form, the fraction is repeatedly multiplied by 10 and the digits +// d[-i] are extracted in order: +// +// (10 * p2) div 2^-e = d[-1] +// (10 * p2) mod 2^-e = d[-2] / 10^1 + ... +// +// The multiplication by 10 must not overflow. It is sufficient to choose +// +// 10 * p2 < 16 * p2 = 2^4 * p2 <= 2^64. +// +// Since p2 = f mod 2^-e < 2^-e, +// +// -e <= 60 or e >= -60 := alpha + +constexpr int kAlpha = -60; +constexpr int kGamma = -32; + +struct cached_power // c = f * 2^e ~= 10^k +{ + std::uint64_t f; + int e; + int k; +}; + +/*! +For a normalized diyfp w = f * 2^e, this function returns a (normalized) cached +power-of-ten c = f_c * 2^e_c, such that the exponent of the product w * c +satisfies (Definition 3.2 from [1]) + alpha <= e_c + e + q <= gamma. +*/ +inline cached_power get_cached_power_for_binary_exponent(int e) { + // Now + // + // alpha <= e_c + e + q <= gamma (1) + // ==> f_c * 2^alpha <= c * 2^e * 2^q + // + // and since the c's are normalized, 2^(q-1) <= f_c, + // + // ==> 2^(q - 1 + alpha) <= c * 2^(e + q) + // ==> 2^(alpha - e - 1) <= c + // + // If c were an exact power of ten, i.e. c = 10^k, one may determine k as + // + // k = ceil( log_10( 2^(alpha - e - 1) ) ) + // = ceil( (alpha - e - 1) * log_10(2) ) + // + // From the paper: + // "In theory the result of the procedure could be wrong since c is rounded, + // and the computation itself is approximated [...]. In practice, however, + // this simple function is sufficient." + // + // For IEEE double precision floating-point numbers converted into + // normalized diyfp's w = f * 2^e, with q = 64, + // + // e >= -1022 (min IEEE exponent) + // -52 (p - 1) + // -52 (p - 1, possibly normalize denormal IEEE numbers) + // -11 (normalize the diyfp) + // = -1137 + // + // and + // + // e <= +1023 (max IEEE exponent) + // -52 (p - 1) + // -11 (normalize the diyfp) + // = 960 + // + // This binary exponent range [-1137,960] results in a decimal exponent + // range [-307,324]. One does not need to store a cached power for each + // k in this range. For each such k it suffices to find a cached power + // such that the exponent of the product lies in [alpha,gamma]. + // This implies that the difference of the decimal exponents of adjacent + // table entries must be less than or equal to + // + // floor( (gamma - alpha) * log_10(2) ) = 8. + // + // (A smaller distance gamma-alpha would require a larger table.) + + // NB: + // Actually this function returns c, such that -60 <= e_c + e + 64 <= -34. + + constexpr int kCachedPowersMinDecExp = -300; + constexpr int kCachedPowersDecStep = 8; + + static constexpr std::array kCachedPowers = {{ + {0xAB70FE17C79AC6CA, -1060, -300}, {0xFF77B1FCBEBCDC4F, -1034, -292}, + {0xBE5691EF416BD60C, -1007, -284}, {0x8DD01FAD907FFC3C, -980, -276}, + {0xD3515C2831559A83, -954, -268}, {0x9D71AC8FADA6C9B5, -927, -260}, + {0xEA9C227723EE8BCB, -901, -252}, {0xAECC49914078536D, -874, -244}, + {0x823C12795DB6CE57, -847, -236}, {0xC21094364DFB5637, -821, -228}, + {0x9096EA6F3848984F, -794, -220}, {0xD77485CB25823AC7, -768, -212}, + {0xA086CFCD97BF97F4, -741, -204}, {0xEF340A98172AACE5, -715, -196}, + {0xB23867FB2A35B28E, -688, -188}, {0x84C8D4DFD2C63F3B, -661, -180}, + {0xC5DD44271AD3CDBA, -635, -172}, {0x936B9FCEBB25C996, -608, -164}, + {0xDBAC6C247D62A584, -582, -156}, {0xA3AB66580D5FDAF6, -555, -148}, + {0xF3E2F893DEC3F126, -529, -140}, {0xB5B5ADA8AAFF80B8, -502, -132}, + {0x87625F056C7C4A8B, -475, -124}, {0xC9BCFF6034C13053, -449, -116}, + {0x964E858C91BA2655, -422, -108}, {0xDFF9772470297EBD, -396, -100}, + {0xA6DFBD9FB8E5B88F, -369, -92}, {0xF8A95FCF88747D94, -343, -84}, + {0xB94470938FA89BCF, -316, -76}, {0x8A08F0F8BF0F156B, -289, -68}, + {0xCDB02555653131B6, -263, -60}, {0x993FE2C6D07B7FAC, -236, -52}, + {0xE45C10C42A2B3B06, -210, -44}, {0xAA242499697392D3, -183, -36}, + {0xFD87B5F28300CA0E, -157, -28}, {0xBCE5086492111AEB, -130, -20}, + {0x8CBCCC096F5088CC, -103, -12}, {0xD1B71758E219652C, -77, -4}, + {0x9C40000000000000, -50, 4}, {0xE8D4A51000000000, -24, 12}, + {0xAD78EBC5AC620000, 3, 20}, {0x813F3978F8940984, 30, 28}, + {0xC097CE7BC90715B3, 56, 36}, {0x8F7E32CE7BEA5C70, 83, 44}, + {0xD5D238A4ABE98068, 109, 52}, {0x9F4F2726179A2245, 136, 60}, + {0xED63A231D4C4FB27, 162, 68}, {0xB0DE65388CC8ADA8, 189, 76}, + {0x83C7088E1AAB65DB, 216, 84}, {0xC45D1DF942711D9A, 242, 92}, + {0x924D692CA61BE758, 269, 100}, {0xDA01EE641A708DEA, 295, 108}, + {0xA26DA3999AEF774A, 322, 116}, {0xF209787BB47D6B85, 348, 124}, + {0xB454E4A179DD1877, 375, 132}, {0x865B86925B9BC5C2, 402, 140}, + {0xC83553C5C8965D3D, 428, 148}, {0x952AB45CFA97A0B3, 455, 156}, + {0xDE469FBD99A05FE3, 481, 164}, {0xA59BC234DB398C25, 508, 172}, + {0xF6C69A72A3989F5C, 534, 180}, {0xB7DCBF5354E9BECE, 561, 188}, + {0x88FCF317F22241E2, 588, 196}, {0xCC20CE9BD35C78A5, 614, 204}, + {0x98165AF37B2153DF, 641, 212}, {0xE2A0B5DC971F303A, 667, 220}, + {0xA8D9D1535CE3B396, 694, 228}, {0xFB9B7CD9A4A7443C, 720, 236}, + {0xBB764C4CA7A44410, 747, 244}, {0x8BAB8EEFB6409C1A, 774, 252}, + {0xD01FEF10A657842C, 800, 260}, {0x9B10A4E5E9913129, 827, 268}, + {0xE7109BFBA19C0C9D, 853, 276}, {0xAC2820D9623BF429, 880, 284}, + {0x80444B5E7AA7CF85, 907, 292}, {0xBF21E44003ACDD2D, 933, 300}, + {0x8E679C2F5E44FF8F, 960, 308}, {0xD433179D9C8CB841, 986, 316}, + {0x9E19DB92B4E31BA9, 1013, 324}, + }}; + + // This computation gives exactly the same results for k as + // k = ceil((kAlpha - e - 1) * 0.30102999566398114) + // for |e| <= 1500, but doesn't require floating-point operations. + // NB: log_10(2) ~= 78913 / 2^18 + const int f = kAlpha - e - 1; + const int k = (f * 78913) / (1 << 18) + static_cast(f > 0); + + const int index = (-kCachedPowersMinDecExp + k + (kCachedPowersDecStep - 1)) / + kCachedPowersDecStep; + + const cached_power cached = kCachedPowers[static_cast(index)]; + + return cached; +} + +/*! +For n != 0, returns k, such that pow10 := 10^(k-1) <= n < 10^k. +For n == 0, returns 1 and sets pow10 := 1. +*/ +inline int find_largest_pow10(const std::uint32_t n, std::uint32_t &pow10) { + // LCOV_EXCL_START + if (n >= 1000000000) { + pow10 = 1000000000; + return 10; + } + // LCOV_EXCL_STOP + else if (n >= 100000000) { + pow10 = 100000000; + return 9; + } else if (n >= 10000000) { + pow10 = 10000000; + return 8; + } else if (n >= 1000000) { + pow10 = 1000000; + return 7; + } else if (n >= 100000) { + pow10 = 100000; + return 6; + } else if (n >= 10000) { + pow10 = 10000; + return 5; + } else if (n >= 1000) { + pow10 = 1000; + return 4; + } else if (n >= 100) { + pow10 = 100; + return 3; + } else if (n >= 10) { + pow10 = 10; + return 2; + } else { + pow10 = 1; + return 1; + } +} + +inline void grisu2_round(char *buf, int len, std::uint64_t dist, + std::uint64_t delta, std::uint64_t rest, + std::uint64_t ten_k) { + + // <--------------------------- delta ----> + // <---- dist ---------> + // --------------[------------------+-------------------]-------------- + // M- w M+ + // + // ten_k + // <------> + // <---- rest ----> + // --------------[------------------+----+--------------]-------------- + // w V + // = buf * 10^k + // + // ten_k represents a unit-in-the-last-place in the decimal representation + // stored in buf. + // Decrement buf by ten_k while this takes buf closer to w. + + // The tests are written in this order to avoid overflow in unsigned + // integer arithmetic. + + while (rest < dist && delta - rest >= ten_k && + (rest + ten_k < dist || dist - rest > rest + ten_k - dist)) { + buf[len - 1]--; + rest += ten_k; + } +} + +/*! +Generates V = buffer * 10^decimal_exponent, such that M- <= V <= M+. +M- and M+ must be normalized and share the same exponent -60 <= e <= -32. +*/ +inline void grisu2_digit_gen(char *buffer, int &length, int &decimal_exponent, + diyfp M_minus, diyfp w, diyfp M_plus) { + static_assert(kAlpha >= -60, "internal error"); + static_assert(kGamma <= -32, "internal error"); + + // Generates the digits (and the exponent) of a decimal floating-point + // number V = buffer * 10^decimal_exponent in the range [M-, M+]. The diyfp's + // w, M- and M+ share the same exponent e, which satisfies alpha <= e <= + // gamma. + // + // <--------------------------- delta ----> + // <---- dist ---------> + // --------------[------------------+-------------------]-------------- + // M- w M+ + // + // Grisu2 generates the digits of M+ from left to right and stops as soon as + // V is in [M-,M+]. + + std::uint64_t delta = + diyfp::sub(M_plus, M_minus) + .f; // (significand of (M+ - M-), implicit exponent is e) + std::uint64_t dist = + diyfp::sub(M_plus, w) + .f; // (significand of (M+ - w ), implicit exponent is e) + + // Split M+ = f * 2^e into two parts p1 and p2 (note: e < 0): + // + // M+ = f * 2^e + // = ((f div 2^-e) * 2^-e + (f mod 2^-e)) * 2^e + // = ((p1 ) * 2^-e + (p2 )) * 2^e + // = p1 + p2 * 2^e + + const diyfp one(std::uint64_t{1} << -M_plus.e, M_plus.e); + + auto p1 = static_cast( + M_plus.f >> + -one.e); // p1 = f div 2^-e (Since -e >= 32, p1 fits into a 32-bit int.) + std::uint64_t p2 = M_plus.f & (one.f - 1); // p2 = f mod 2^-e + + // 1) + // + // Generate the digits of the integral part p1 = d[n-1]...d[1]d[0] + + std::uint32_t pow10; + const int k = find_largest_pow10(p1, pow10); + + // 10^(k-1) <= p1 < 10^k, pow10 = 10^(k-1) + // + // p1 = (p1 div 10^(k-1)) * 10^(k-1) + (p1 mod 10^(k-1)) + // = (d[k-1] ) * 10^(k-1) + (p1 mod 10^(k-1)) + // + // M+ = p1 + p2 * 2^e + // = d[k-1] * 10^(k-1) + (p1 mod 10^(k-1)) + p2 * 2^e + // = d[k-1] * 10^(k-1) + ((p1 mod 10^(k-1)) * 2^-e + p2) * 2^e + // = d[k-1] * 10^(k-1) + ( rest) * 2^e + // + // Now generate the digits d[n] of p1 from left to right (n = k-1,...,0) + // + // p1 = d[k-1]...d[n] * 10^n + d[n-1]...d[0] + // + // but stop as soon as + // + // rest * 2^e = (d[n-1]...d[0] * 2^-e + p2) * 2^e <= delta * 2^e + + int n = k; + while (n > 0) { + // Invariants: + // M+ = buffer * 10^n + (p1 + p2 * 2^e) (buffer = 0 for n = k) + // pow10 = 10^(n-1) <= p1 < 10^n + // + const std::uint32_t d = p1 / pow10; // d = p1 div 10^(n-1) + const std::uint32_t r = p1 % pow10; // r = p1 mod 10^(n-1) + // + // M+ = buffer * 10^n + (d * 10^(n-1) + r) + p2 * 2^e + // = (buffer * 10 + d) * 10^(n-1) + (r + p2 * 2^e) + // + buffer[length++] = static_cast('0' + d); // buffer := buffer * 10 + d + // + // M+ = buffer * 10^(n-1) + (r + p2 * 2^e) + // + p1 = r; + n--; + // + // M+ = buffer * 10^n + (p1 + p2 * 2^e) + // pow10 = 10^n + // + + // Now check if enough digits have been generated. + // Compute + // + // p1 + p2 * 2^e = (p1 * 2^-e + p2) * 2^e = rest * 2^e + // + // Note: + // Since rest and delta share the same exponent e, it suffices to + // compare the significands. + const std::uint64_t rest = (std::uint64_t{p1} << -one.e) + p2; + if (rest <= delta) { + // V = buffer * 10^n, with M- <= V <= M+. + + decimal_exponent += n; + + // We may now just stop. But instead look if the buffer could be + // decremented to bring V closer to w. + // + // pow10 = 10^n is now 1 ulp in the decimal representation V. + // The rounding procedure works with diyfp's with an implicit + // exponent of e. + // + // 10^n = (10^n * 2^-e) * 2^e = ulp * 2^e + // + const std::uint64_t ten_n = std::uint64_t{pow10} << -one.e; + grisu2_round(buffer, length, dist, delta, rest, ten_n); + + return; + } + + pow10 /= 10; + // + // pow10 = 10^(n-1) <= p1 < 10^n + // Invariants restored. + } + + // 2) + // + // The digits of the integral part have been generated: + // + // M+ = d[k-1]...d[1]d[0] + p2 * 2^e + // = buffer + p2 * 2^e + // + // Now generate the digits of the fractional part p2 * 2^e. + // + // Note: + // No decimal point is generated: the exponent is adjusted instead. + // + // p2 actually represents the fraction + // + // p2 * 2^e + // = p2 / 2^-e + // = d[-1] / 10^1 + d[-2] / 10^2 + ... + // + // Now generate the digits d[-m] of p1 from left to right (m = 1,2,...) + // + // p2 * 2^e = d[-1]d[-2]...d[-m] * 10^-m + // + 10^-m * (d[-m-1] / 10^1 + d[-m-2] / 10^2 + ...) + // + // using + // + // 10^m * p2 = ((10^m * p2) div 2^-e) * 2^-e + ((10^m * p2) mod 2^-e) + // = ( d) * 2^-e + ( r) + // + // or + // 10^m * p2 * 2^e = d + r * 2^e + // + // i.e. + // + // M+ = buffer + p2 * 2^e + // = buffer + 10^-m * (d + r * 2^e) + // = (buffer * 10^m + d) * 10^-m + 10^-m * r * 2^e + // + // and stop as soon as 10^-m * r * 2^e <= delta * 2^e + + int m = 0; + for (;;) { + // Invariant: + // M+ = buffer * 10^-m + 10^-m * (d[-m-1] / 10 + d[-m-2] / 10^2 + ...) + // * 2^e + // = buffer * 10^-m + 10^-m * (p2 ) + // * 2^e = buffer * 10^-m + 10^-m * (1/10 * (10 * p2) ) * 2^e = + // buffer * 10^-m + 10^-m * (1/10 * ((10*p2 div 2^-e) * 2^-e + + // (10*p2 mod 2^-e)) * 2^e + // + p2 *= 10; + const std::uint64_t d = p2 >> -one.e; // d = (10 * p2) div 2^-e + const std::uint64_t r = p2 & (one.f - 1); // r = (10 * p2) mod 2^-e + // + // M+ = buffer * 10^-m + 10^-m * (1/10 * (d * 2^-e + r) * 2^e + // = buffer * 10^-m + 10^-m * (1/10 * (d + r * 2^e)) + // = (buffer * 10 + d) * 10^(-m-1) + 10^(-m-1) * r * 2^e + // + buffer[length++] = static_cast('0' + d); // buffer := buffer * 10 + d + // + // M+ = buffer * 10^(-m-1) + 10^(-m-1) * r * 2^e + // + p2 = r; + m++; + // + // M+ = buffer * 10^-m + 10^-m * p2 * 2^e + // Invariant restored. + + // Check if enough digits have been generated. + // + // 10^-m * p2 * 2^e <= delta * 2^e + // p2 * 2^e <= 10^m * delta * 2^e + // p2 <= 10^m * delta + delta *= 10; + dist *= 10; + if (p2 <= delta) { + break; + } + } + + // V = buffer * 10^-m, with M- <= V <= M+. + + decimal_exponent -= m; + + // 1 ulp in the decimal representation is now 10^-m. + // Since delta and dist are now scaled by 10^m, we need to do the + // same with ulp in order to keep the units in sync. + // + // 10^m * 10^-m = 1 = 2^-e * 2^e = ten_m * 2^e + // + const std::uint64_t ten_m = one.f; + grisu2_round(buffer, length, dist, delta, p2, ten_m); + + // By construction this algorithm generates the shortest possible decimal + // number (Loitsch, Theorem 6.2) which rounds back to w. + // For an input number of precision p, at least + // + // N = 1 + ceil(p * log_10(2)) + // + // decimal digits are sufficient to identify all binary floating-point + // numbers (Matula, "In-and-Out conversions"). + // This implies that the algorithm does not produce more than N decimal + // digits. + // + // N = 17 for p = 53 (IEEE double precision) + // N = 9 for p = 24 (IEEE single precision) +} + +/*! +v = buf * 10^decimal_exponent +len is the length of the buffer (number of decimal digits) +The buffer must be large enough, i.e. >= max_digits10. +*/ +inline void grisu2_core(char *buf, int &len, int &decimal_exponent, diyfp m_minus, + diyfp v, diyfp m_plus) { + + // --------(-----------------------+-----------------------)-------- (A) + // m- v m+ + // + // --------------------(-----------+-----------------------)-------- (B) + // m- v m+ + // + // First scale v (and m- and m+) such that the exponent is in the range + // [alpha, gamma]. + + const cached_power cached = get_cached_power_for_binary_exponent(m_plus.e); + + const diyfp c_minus_k(cached.f, cached.e); // = c ~= 10^-k + + // The exponent of the products is = v.e + c_minus_k.e + q and is in the range + // [alpha,gamma] + const diyfp w = diyfp::mul(v, c_minus_k); + const diyfp w_minus = diyfp::mul(m_minus, c_minus_k); + const diyfp w_plus = diyfp::mul(m_plus, c_minus_k); + + // ----(---+---)---------------(---+---)---------------(---+---)---- + // w- w w+ + // = c*m- = c*v = c*m+ + // + // diyfp::mul rounds its result and c_minus_k is approximated too. w, w- and + // w+ are now off by a small amount. + // In fact: + // + // w - v * 10^k < 1 ulp + // + // To account for this inaccuracy, add resp. subtract 1 ulp. + // + // --------+---[---------------(---+---)---------------]---+-------- + // w- M- w M+ w+ + // + // Now any number in [M-, M+] (bounds included) will round to w when input, + // regardless of how the input rounding algorithm breaks ties. + // + // And digit_gen generates the shortest possible such number in [M-, M+]. + // Note that this does not mean that Grisu2 always generates the shortest + // possible number in the interval (m-, m+). + const diyfp M_minus(w_minus.f + 1, w_minus.e); + const diyfp M_plus(w_plus.f - 1, w_plus.e); + + decimal_exponent = -cached.k; // = -(-k) = k + + grisu2_digit_gen(buf, len, decimal_exponent, M_minus, w, M_plus); +} + +/*! +v = buf * 10^decimal_exponent +len is the length of the buffer (number of decimal digits) +The buffer must be large enough, i.e. >= max_digits10. +*/ +template +void grisu2_wrap(char *buf, int &len, int &decimal_exponent, FloatType value) { + static_assert(diyfp::kPrecision >= std::numeric_limits::digits + 3, + "internal error: not enough precision"); + + // If the neighbors (and boundaries) of 'value' are always computed for + // double-precision numbers, all float's can be recovered using strtod (and + // strtof). However, the resulting decimal representations are not exactly + // "short". + // + // The documentation for 'std::to_chars' + // (https://en.cppreference.com/w/cpp/utility/to_chars) says "value is + // converted to a string as if by std::sprintf in the default ("C") locale" + // and since sprintf promotes float's to double's, I think this is exactly + // what 'std::to_chars' does. On the other hand, the documentation for + // 'std::to_chars' requires that "parsing the representation using the + // corresponding std::from_chars function recovers value exactly". That + // indicates that single precision floating-point numbers should be recovered + // using 'std::strtof'. + // + // NB: If the neighbors are computed for single-precision numbers, there is a + // single float + // (7.0385307e-26f) which can't be recovered using strtod. The resulting + // double precision value is off by 1 ulp. +#if 0 + const boundaries w = compute_boundaries(static_cast(value)); +#else + const boundaries w = compute_boundaries(value); +#endif + + grisu2_core(buf, len, decimal_exponent, w.minus, w.w, w.plus); +} + +/*! +@brief appends a decimal representation of e to buf +@return a pointer to the element following the exponent. +@pre -1000 < e < 1000 +*/ +inline char *append_exponent(char *buf, int e) { + + if (e < 0) { + e = -e; + *buf++ = '-'; + } else { + *buf++ = '+'; + } + + auto k = static_cast(e); + if (k < 10) { + // Always print at least two digits in the exponent. + // This is for compatibility with printf("%g"). + *buf++ = '0'; + *buf++ = static_cast('0' + k); + } else if (k < 100) { + *buf++ = static_cast('0' + k / 10); + k %= 10; + *buf++ = static_cast('0' + k); + } else { + *buf++ = static_cast('0' + k / 100); + k %= 100; + *buf++ = static_cast('0' + k / 10); + k %= 10; + *buf++ = static_cast('0' + k); + } + + return buf; +} + +/*! +@brief prettify v = buf * 10^decimal_exponent +If v is in the range [10^min_exp, 10^max_exp) it will be printed in fixed-point +notation. Otherwise it will be printed in exponential notation. +@pre min_exp < 0 +@pre max_exp > 0 +*/ +inline char *format_buffer(char *buf, int len, int decimal_exponent, + int min_exp, int max_exp) { + + const int k = len; + const int n = len + decimal_exponent; + + // v = buf * 10^(n-k) + // k is the length of the buffer (number of decimal digits) + // n is the position of the decimal point relative to the start of the buffer. + + if (k <= n && n <= max_exp) { + // digits[000] + // len <= max_exp + 2 + + std::memset(buf + k, '0', static_cast(n) - static_cast(k)); + return buf + (static_cast(n)); + } + + if (0 < n && n <= max_exp) { + // dig.its + // len <= max_digits10 + 1 + std::memmove(buf + (static_cast(n) + 1), buf + n, + static_cast(k) - static_cast(n)); + buf[n] = '.'; + return buf + (static_cast(k) + 1U); + } + + if (min_exp < n && n <= 0) { + // 0.[000]digits + // len <= 2 + (-min_exp - 1) + max_digits10 + + std::memmove(buf + (2 + static_cast(-n)), buf, + static_cast(k)); + buf[0] = '0'; + buf[1] = '.'; + std::memset(buf + 2, '0', static_cast(-n)); + return buf + (2U + static_cast(-n) + static_cast(k)); + } + + if (k == 1) { + // dE+123 + // len <= 1 + 5 + + buf += 1; + } else { + // d.igitsE+123 + // len <= max_digits10 + 1 + 5 + + std::memmove(buf + 2, buf + 1, static_cast(k) - 1); + buf[1] = '.'; + buf += 1 + static_cast(k); + } + + *buf++ = 'e'; + return append_exponent(buf, n - 1); +} + +/*! +The format of the resulting decimal representation is similar to printf's %g +format. Returns an iterator pointing past-the-end of the decimal representation. +@note The input number must be finite, i.e. NaN's and Inf's are not supported. +@note The buffer must be large enough. +@note The result is NOT null-terminated. +*/ +template +char *to_chars(char *first, FloatType value) { + bool negative = std::signbit(value); + if (negative) { + value = -value; + *first++ = '-'; + } + if (value == 0) // +-0 + { + *first++ = '0'; + return first; + } + // Compute v = buffer * 10^decimal_exponent. + // The decimal digits are stored in the buffer, which needs to be interpreted + // as an unsigned decimal integer. + // len is the length of the buffer, i.e. the number of decimal digits. + int len = 0; + int decimal_exponent = 0; + grisu2_wrap(first, len, decimal_exponent, value); + // Format the buffer like printf("%.*g", prec, value) + constexpr int kMinExp = -4; + constexpr int kMaxExp = std::numeric_limits::digits10; + + return format_buffer(first, len, decimal_exponent, kMinExp, kMaxExp); +} +} // namespace grisu2 diff --git a/thirdparty/grisu2/patches/0001-godot-changes.patch b/thirdparty/grisu2/patches/0001-godot-changes.patch new file mode 100644 index 0000000000..d126c24f70 --- /dev/null +++ b/thirdparty/grisu2/patches/0001-godot-changes.patch @@ -0,0 +1,119 @@ +diff --git a/thirdparty/grisu2/grisu2.h b/thirdparty/grisu2/grisu2.h +index 19886cce47f..dbc09755fad 100644 +--- a/thirdparty/grisu2/grisu2.h ++++ b/thirdparty/grisu2/grisu2.h +@@ -1,15 +1,12 @@ +-#ifndef SIMDJSON_SRC_TO_CHARS_CPP +-#define SIMDJSON_SRC_TO_CHARS_CPP +- +-#include ++#pragma once + + #include + #include + #include + #include + +-namespace simdjson { +-namespace internal { ++namespace grisu2 { + /*! + implements the Grisu2 algorithm for binary to decimal floating-point + conversion. +@@ -26,7 +23,6 @@ PLDI 2010 [2] Burger, Dybvig, "Printing Floating-Point Numbers Quickly and + Accurately", Proceedings of the ACM SIGPLAN 1996 Conference on Programming + Language Design and Implementation, PLDI 1996 + */ +-namespace dtoa_impl { + + template + Target reinterpret_bits(const Source source) { +@@ -718,7 +714,7 @@ v = buf * 10^decimal_exponent + len is the length of the buffer (number of decimal digits) + The buffer must be large enough, i.e. >= max_digits10. + */ +-inline void grisu2(char *buf, int &len, int &decimal_exponent, diyfp m_minus, ++inline void grisu2_core(char *buf, int &len, int &decimal_exponent, diyfp m_minus, + diyfp v, diyfp m_plus) { + + // --------(-----------------------+-----------------------)-------- (A) +@@ -775,7 +771,7 @@ len is the length of the buffer (number of decimal digits) + The buffer must be large enough, i.e. >= max_digits10. + */ + template +-void grisu2(char *buf, int &len, int &decimal_exponent, FloatType value) { ++void grisu2_wrap(char *buf, int &len, int &decimal_exponent, FloatType value) { + static_assert(diyfp::kPrecision >= std::numeric_limits::digits + 3, + "internal error: not enough precision"); + +@@ -804,7 +800,7 @@ void grisu2(char *buf, int &len, int &decimal_exponent, FloatType value) { + const boundaries w = compute_boundaries(value); + #endif + +- grisu2(buf, len, decimal_exponent, w.minus, w.w, w.plus); ++ grisu2_core(buf, len, decimal_exponent, w.minus, w.w, w.plus); + } + + /*! +@@ -864,10 +860,7 @@ inline char *format_buffer(char *buf, int len, int decimal_exponent, + // len <= max_exp + 2 + + std::memset(buf + k, '0', static_cast(n) - static_cast(k)); +- // Make it look like a floating-point number (#362, #378) +- buf[n + 0] = '.'; +- buf[n + 1] = '0'; +- return buf + (static_cast(n)) + 2; ++ return buf + (static_cast(n)); + } + + if (0 < n && n <= max_exp) { +@@ -909,8 +902,6 @@ inline char *format_buffer(char *buf, int len, int decimal_exponent, + return append_exponent(buf, n - 1); + } + +-} // namespace dtoa_impl +- + /*! + The format of the resulting decimal representation is similar to printf's %g + format. Returns an iterator pointing past-the-end of the decimal representation. +@@ -918,19 +909,15 @@ format. Returns an iterator pointing past-the-end of the decimal representation. + @note The buffer must be large enough. + @note The result is NOT null-terminated. + */ +-char *to_chars(char *first, const char *last, double value) { +- static_cast(last); // maybe unused - fix warning ++template ++char *to_chars(char *first, FloatType value) { + bool negative = std::signbit(value); + if (negative) { + value = -value; + *first++ = '-'; + } +- + if (value == 0) // +-0 + { +- *first++ = '0'; +- // Make it look like a floating-point number (#362, #378) +- *first++ = '.'; + *first++ = '0'; + return first; + } +@@ -940,15 +927,13 @@ char *to_chars(char *first, const char *last, double value) { + // len is the length of the buffer, i.e. the number of decimal digits. + int len = 0; + int decimal_exponent = 0; +- dtoa_impl::grisu2(first, len, decimal_exponent, value); ++ grisu2_wrap(first, len, decimal_exponent, value); + // Format the buffer like printf("%.*g", prec, value) + constexpr int kMinExp = -4; + constexpr int kMaxExp = std::numeric_limits::digits10; + +- return dtoa_impl::format_buffer(first, len, decimal_exponent, kMinExp, +- kMaxExp); ++ return format_buffer(first, len, decimal_exponent, kMinExp, kMaxExp); + } +-} // namespace internal +-} // namespace simdjson ++} // namespace grisu2 + +-#endif // SIMDJSON_SRC_TO_CHARS_CPP