Skip to content

Add short option to float_to_binary/list#2240

Merged
bettio merged 2 commits intoatomvm:release-0.7from
bettio:short-float-to
Mar 30, 2026
Merged

Add short option to float_to_binary/list#2240
bettio merged 2 commits intoatomvm:release-0.7from
bettio:short-float-to

Conversation

@bettio
Copy link
Copy Markdown
Collaborator

@bettio bettio commented Mar 27, 2026

  • Add [short] option for shortest-roundtrip float formatting, matching
    Erlang/OTP behavior. Uses Grisu3 for 64-bit doubles, best-effort snprintf
    for 32-bit floats.
  • Fix locale-dependent decimal separator bug (e.g. , instead of . in
    some locales) via unlocalized_snprintf.
  • Extend option ranges to {decimals, 0..253} and {scientific, 0..249}
    to match Erlang/OTP limits.

Fixes #2189

Why Grisu3 over Ryu

Erlang/OTP uses Ryu, which guarantees shortest output for all doubles.
Grisu3 produces the shortest output for ~99.5% of values and falls back
to a longer (but correct) 17-digit representation for the rest.

Grisu3 was chosen for its smaller binary footprint on embedded targets:

Stripped AtomVM binary (x86_64, -Os):

Before this PR: 653,768 bytes
With Grisu3:657,864 bytes(+4 KB)
With Ryu (full):756,168 bytes(+98 KB)
With Ryu (small): 747,976 bytes(+90 KB)

Differences with Erlang/OTP

For most values the output is identical. The Grisu3 fallback (~0.5% of
doubles) produces longer output:

%% Erlang/OTP (Ryu)AtomVM (Grisu3)
%% <<"1.0e23">><<"9.9999999999999992e22">>

Both representations are correct and roundtrip to the same IEEE 754 double.

With 32-bit floats (AVM_USE_32BIT_FLOAT), short uses a best-effort
algorithm (%.9g) since Grisu3 operates on 64-bit doubles only:

%% Erlang/OTP (always 64-bit)AtomVM 32-bit
%% <<"3.14">><<"3.1400001">>

The 32-bit output is longer but always roundtrips correctly.

Tests:

Grisu3 implementation has been tested against a dataset with milions of
doubles with no errors.


Compile matrix: 80/80 passed, 0 failed

=== UBSan ===
========================================
ALL PASSED

=== Functional tests ===
-O0: ALL PASSED
-O2: ALL PASSED
-O3: ALL PASSED
-Os: ALL PASSED
-O2 -ffast-math: ALL PASSED

ALL OK
=== float-data/number_files ===
  float-data/number_files/mobilenetv3_large.txt: 5507432 lines, 0 failures
  float-data/number_files/numbers.txt: 10001 lines, 0 failures
  float-data/number_files/marine_ik.txt: 114950 lines, 0 failures
  float-data/number_files/canada.txt: 111126 lines, 0 failures
  float-data/number_files/noaa_gfs_1p00.txt: 4841536 lines, 0 failures
  float-data/number_files/bitcoin.txt: 943 lines, 0 failures
  float-data/number_files/mesh.txt: 73019 lines, 0 failures
  float-data/number_files/gaia.txt: 3879638 lines, 0 failures
  float-data/number_files/noaa_global_hourly_2023.txt: 1000000 lines, 0 failures
  float-data/number_files/hellfloat64.txt: 1000000 lines, 0 failures

=== parse-number-fxx-test-data ===
  parse-number-fxx-test-data/data/lemire-fast-float.txt: 3299 lines, 3176 tested, 123 skipped, 0 failures
  parse-number-fxx-test-data/data/remyoudompheng-fptest-3.txt: 885708 lines, 885708 tested, 0 skipped, 0 failures
  parse-number-fxx-test-data/data/more-test-cases.txt: 60 lines, 33 tested, 27 skipped, 0 failures
  parse-number-fxx-test-data/data/exhaustive-float16.txt: 31745 lines, 31745 tested, 0 skipped, 0 failures
  parse-number-fxx-test-data/data/freetype-2-7.txt: 3566 lines, 3561 tested, 5 skipped, 0 failures
  parse-number-fxx-test-data/data/remyoudompheng-fptest-0.txt: 1000000 lines, 1000000 tested, 0 skipped, 0 failures
  parse-number-fxx-test-data/data/ulfjack-ryu.txt: 599458 lines, 599231 tested, 227 skipped, 0 failures
  parse-number-fxx-test-data/data/google-wuffs.txt: 10744 lines, 10659 tested, 85 skipped, 0 failures
  parse-number-fxx-test-data/data/google-double-conversion.txt: 564745 lines, 564695 tested, 50 skipped, 0 failures
  parse-number-fxx-test-data/data/tencent-rapidjson.txt: 3563 lines, 3534 tested, 29 skipped, 0 failures
  parse-number-fxx-test-data/data/ibm-fpgen.txt: 102792 lines, 72726 tested, 30066 skipped, 0 failures
  parse-number-fxx-test-data/data/lemire-fast-double-parser.txt: 94313 lines, 94225 tested, 88 skipped, 0 failures
  parse-number-fxx-test-data/data/remyoudompheng-fptest-2.txt: 1000000 lines, 1000000 tested, 0 skipped, 0 failures
  parse-number-fxx-test-data/data/remyoudompheng-fptest-1.txt: 1000000 lines, 1000000 tested, 0 skipped, 0 failures

=== supplemental_test_files ===
  supplemental_test_files/data/lemire-fast-float.txt: 3299 lines, 3176 tested, 123 skipped, 0 failures
  supplemental_test_files/data/remyoudompheng-fptest-3.txt: 885708 lines, 885708 tested, 0 skipped, 0 failures
  supplemental_test_files/data/more-test-cases.txt: 3 lines, 3 tested, 0 skipped, 0 failures
  supplemental_test_files/data/freetype-2-7.txt: 3566 lines, 3561 tested, 5 skipped, 0 failures
  supplemental_test_files/data/remyoudompheng-fptest-0.txt: 1000000 lines, 1000000 tested, 0 skipped, 0 failures
  supplemental_test_files/data/ulfjack-ryu.txt: 599458 lines, 599231 tested, 227 skipped, 0 failures
  supplemental_test_files/data/google-wuffs.txt: 10744 lines, 10659 tested, 85 skipped, 0 failures
  supplemental_test_files/data/google-double-conversion.txt: 564745 lines, 564695 tested, 50 skipped, 0 failures
  supplemental_test_files/data/tencent-rapidjson.txt: 3563 lines, 3534 tested, 29 skipped, 0 failures
  supplemental_test_files/data/ibm-fpgen.txt: 102792 lines, 72726 tested, 30066 skipped, 0 failures
  supplemental_test_files/data/lemire-fast-double-parser.txt: 94313 lines, 94225 tested, 88 skipped, 0 failures
  supplemental_test_files/data/remyoudompheng-fptest-2.txt: 1000000 lines, 1000000 tested, 0 skipped, 0 failures
  supplemental_test_files/data/remyoudompheng-fptest-1.txt: 1000000 lines, 1000000 tested, 0 skipped, 0 failures

=== fptest IBM IEEE 754 ===
  fptest/test_suite/Sticky-Bit-Calculation.fptest: 335 tested, 0 skipped, 0 failures
  fptest/test_suite/Add-Shift.fptest: 342 tested, 0 skipped, 0 failures
  fptest/test_suite/Corner-Rounding.fptest: 786 tested, 0 skipped, 0 failures
  fptest/test_suite/MultiplyAdd-Special-Events-Overflow.fptest: 80 tested, 0 skipped, 0 failures
  fptest/test_suite/Overflow.fptest: 7527 tested, 0 skipped, 0 failures
  fptest/test_suite/Divide-Trailing-Zeros.fptest: 86 tested, 0 skipped, 0 failures
  fptest/test_suite/MultiplyAdd-Shift.fptest: 291 tested, 0 skipped, 0 failures
  fptest/test_suite/Add-Shift-And-Special-Significands.fptest: 98816 tested, 0 skipped, 0 failures
  fptest/test_suite/Underflow.fptest: 8709 tested, 0 skipped, 0 failures
  fptest/test_suite/MultiplyAdd-Special-Events-Underflow.fptest: 160 tested, 0 skipped, 0 failures
  fptest/test_suite/Basic-Types-Inputs.fptest: 58210 tested, 0 skipped, 0 failures
  fptest/test_suite/Hamming-Distance.fptest: 865 tested, 0 skipped, 0 failures
  fptest/test_suite/Rounding.fptest: 2023 tested, 0 skipped, 0 failures
  fptest/test_suite/MultiplyAdd-Shift-And-Special-Significands.fptest: 83948 tested, 0 skipped, 0 failures
  fptest/test_suite/Vicinity-Of-Rounding-Boundaries.fptest: 2192 tested, 0 skipped, 0 failures
  fptest/test_suite/Compare-Different-Input-Field-Relations.fptest: 951 tested, 0 skipped, 0 failures
  fptest/test_suite/Input-Special-Significand.fptest: 3086 tested, 0 skipped, 0 failures
  fptest/test_suite/Basic-Types-Intermediate.fptest: 526 tested, 0 skipped, 0 failures
  fptest/test_suite/MultiplyAdd-Cancellation.fptest: 385 tested, 0 skipped, 0 failures
  fptest/test_suite/MultiplyAdd-Special-Events-Inexact.fptest: 44 tested, 0 skipped, 0 failures
  fptest/test_suite/MultiplyAdd-Cancellation-And-Subnorm-Result.fptest: 9008 tested, 0 skipped, 0 failures
  fptest/test_suite/Divide-Divide-By-Zero-Exception.fptest: 18 tested, 0 skipped, 0 failures
  fptest/test_suite/Add-Cancellation.fptest: 156 tested, 0 skipped, 0 failures
  fptest/test_suite/Add-Cancellation-And-Subnorm-Result.fptest: 3576 tested, 0 skipped, 0 failures

========================================
ALL PASSED

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later

@bettio bettio added the popcorn label Mar 27, 2026
@bettio bettio added this to the v0.7.0 milestone Mar 28, 2026
@petermm
Copy link
Copy Markdown
Contributor

petermm commented Mar 28, 2026

Fwiw - ryu here https://github.com/petermm/AtomVM/tree/feature/ryu-float-formatting - it does fit, but do think a smaller implementation makes sense. maybe we want ryu in future for generic_unix/wasm.

ampcode review:

PR Review: Add short option to float_to_binary/list

Commits reviewed:

  • b8bd3d64e — Add short option to float_to_binary/list
  • 7b9658402 — exavmlib: Use short option for float formatting

Verdict: Approve with changes


🔴 Must Fix: Grisu3 fallback is not short-compatible

File: src/libAtomVM/float_utils.c:347-351

When Grisu3 fails (it doesn't guarantee success for all doubles), the fallback uses "%.17g" which:

  • Is not shortest representation — it's roundtrippable but often longer than necessary
  • Can emit output without a decimal point (e.g. "1", "1000", "1e+20")
  • Uses libc exponent formatting (uppercase E, + sign, leading zeros) instead of OTP conventions
  • Can break AtomVM's own binary_to_float roundtrip which expects a decimal point

Current code:

if (!success) {
    return (int32_t) unlocalized_snprintf(s2, 32, "%.17g", v) + (int32_t) (s2 - dst);
}

Suggested fix

Post-process the %.17g fallback output to match OTP short formatting invariants:

--- a/src/libAtomVM/float_utils.c
+++ b/src/libAtomVM/float_utils.c
@@ -324,6 +324,56 @@ static int32_t write_exponent(int32_t exp, char *dst)
     return (int32_t) exp_len;
 }
 
+/*
+ * Post-process %.17g output to match OTP short format:
+ *  - Ensure decimal point with at least one fractional digit
+ *  - Normalize exponent: lowercase 'e', remove '+', remove leading zeros
+ *  - Force scientific notation for |v| >= 2^53
+ */
+static int32_t fixup_g_format(char *buf, int32_t len, double v)
+{
+    /* Find 'e' or 'E' */
+    char *e_pos = NULL;
+    for (int32_t i = 0; i < len; i++) {
+        if (buf[i] == 'e' || buf[i] == 'E') {
+            e_pos = buf + i;
+            break;
+        }
+    }
+
+    if (e_pos) {
+        *e_pos = 'e';
+
+        /* Ensure decimal point exists before 'e' */
+        if (!memchr(buf, '.', e_pos - buf)) {
+            int32_t e_len = len - (int32_t)(e_pos - buf);
+            memmove(e_pos + 2, e_pos, e_len + 1);
+            e_pos[0] = '.';
+            e_pos[1] = '0';
+            len += 2;
+            e_pos += 2;
+        }
+
+        /* Remove '+' from exponent */
+        if (e_pos[1] == '+') {
+            int32_t tail = len - (int32_t)(e_pos + 2 - buf);
+            memmove(e_pos + 1, e_pos + 2, tail + 1);
+            len--;
+        }
+
+        /* Remove leading zeros from exponent */
+        char *exp_start = e_pos + 1;
+        if (*exp_start == '-') exp_start++;
+        char *first_nonzero = exp_start;
+        while (*first_nonzero == '0' && first_nonzero[1] != '\0') first_nonzero++;
+        if (first_nonzero > exp_start) {
+            int32_t shift = (int32_t)(first_nonzero - exp_start);
+            int32_t tail = len - (int32_t)(first_nonzero - buf);
+            memmove(exp_start, first_nonzero, tail + 1);
+            len -= shift;
+        }
+    } else {
+        /* No exponent — ensure decimal point exists */
+        if (!memchr(buf, '.', len)) {
+            buf[len++] = '.';
+            buf[len++] = '0';
+            buf[len] = '\0';
+        }
+    }
+
+    (void) v; /* reserved for future 2^53 scientific enforcement */
+    return len;
+}
+
 static int32_t dtoa_grisu3(double v, char *dst)
 {
     int32_t d_exp, len, success, decimals, i;
@@ -347,7 +397,10 @@ static int32_t dtoa_grisu3(double v, char *dst)
     // If grisu3 was not able to convert the number to a string,
     // use unlocalized_snprintf (locale-independent).
     if (!success) {
-        return (int32_t) unlocalized_snprintf(s2, 32, "%.17g", v) + (int32_t) (s2 - dst);
+        int32_t fb_len = (int32_t) unlocalized_snprintf(s2, 64, "%.17g", v);
+        if (fb_len < 0 || fb_len >= 64) return -1;
+        fb_len = fixup_g_format(s2, fb_len, v);
+        return fb_len + (int32_t) (s2 - dst);
     }

Note: For full OTP parity, consider replacing Grisu3+fallback entirely with Ryu, which guarantees shortest output for all doubles without a fallback path.


🟡 Strongly Recommended: Reject non-finite floats

File: src/libAtomVM/nifs.c:2671-2674

The format_float helper and float_utils.h docs say the caller must ensure isfinite(value), but the NIFs don't check. If NaN/Inf ever reach formatting, the output is libc-defined and not roundtrippable.

Suggested fix

--- a/src/libAtomVM/nifs.c
+++ b/src/libAtomVM/nifs.c
@@ -2671,6 +2671,9 @@ static int format_float(term value, double_format_t format, int precision, char
 {
     avm_float_t float_value = term_to_float(value);
+    if (UNLIKELY(!isfinite(float_value))) {
+        return -1;
+    }
     return avm_float_write_to_ascii_buf(float_value, format, precision, out_buf);
 }

This will cause the existing if (UNLIKELY(len < 0)) RAISE_ERROR(BADARG_ATOM) to fire for NaN/Inf, which matches OTP behavior (erlang:float_to_binary(math:nan(), [short]) raises badarg on BEAM).

Don't forget to add #include <math.h> if not already included in nifs.c.


🟡 Strongly Recommended: Fix unlocalized_snprintf doc claims

File: src/libAtomVM/unlocalized_snprintf.h:22-27

The header claims "Thread-safe. Never calls setlocale()." but strategy 3 (pure C99 fallback) calls localeconv() and plain vsnprintf(), both of which read process-global locale state. Not thread-safe under concurrent setlocale().

Suggested fix

--- a/src/libAtomVM/unlocalized_snprintf.h
+++ b/src/libAtomVM/unlocalized_snprintf.h
@@ -22,8 +22,10 @@
  * @file unlocalized_snprintf.h
  * @brief Locale-independent snprintf for floating-point numbers.
  *
- * @details Guarantees '.' as the decimal separator regardless of LC_NUMERIC.
- * Thread-safe. Never calls setlocale().
+ * @details Guarantees '.' as the decimal separator regardless of LC_NUMERIC,
+ * for floating-point format specifiers only (%e, %f, %g and variants).
+ * Thread-safe when snprintf_l() or uselocale() is available.
+ * On pure C99 platforms, best-effort assuming no concurrent setlocale().
  */

🟡 Recommended: Expand test coverage

Files: tests/erlang_tests/float2bin_short.erl, tests/erlang_tests/float2list_short.erl

Current tests cover basic values and option combinations but miss important edge cases.

Missing test cases to add

%% Negative zero
<<"-0.0">> = erlang:float_to_binary(-0.0, [short]),

%% Notation boundaries
<<"0.001">> = erlang:float_to_binary(0.001, [short]),
%% 0.0001 should use scientific on OTP:
%% <<"1.0e-4">> = erlang:float_to_binary(0.0001, [short]),
<<"1000.0">> = erlang:float_to_binary(1000.0, [short]),
<<"999.0">> = erlang:float_to_binary(999.0, [short]),

%% 2^53 boundary (scientific notation threshold)
%% 9007199254740991.0 is 2^53-1, last integer exactly representable
%% 9007199254740992.0 is 2^53, should force scientific
<<"9.007199254740992e15">> = erlang:float_to_binary(9007199254740992.0, [short]),

%% Extremes
_ = erlang:float_to_binary(1.7976931348623157e308, [short]),  %% DBL_MAX
_ = erlang:float_to_binary(5.0e-324, [short]),                %% smallest subnormal
_ = erlang:float_to_binary(2.2250738585072014e-308, [short]), %% DBL_MIN

%% Classic roundtrip traps (verify roundtrip, exact string may vary)
F1 = 0.1 + 0.2,
F1 = erlang:binary_to_float(erlang:float_to_binary(F1, [short])),

Recommended: differential test against BEAM

For CI on 64-bit builds, generate a representative set of doubles and compare float_to_binary(F, [short]) output exactly against BEAM. This catches Grisu3 failures, formatting mismatches, and exponent normalization issues.


✅ Looks Good

  • Grisu3 core algorithm — Standard port of Loitsch's algorithm, formatting thresholds match OTP behavior
  • Option parsing (nifs.c:2677-2739) — short only applies when no explicit {decimals,N}/{scientific,N} is present; compact only affects decimals; coherent and OTP-compatible
  • Memory safety — Buffer size of 256 bytes is adequate for all paths; no overflow found
  • Elixir library updates — Clean replacement of [{:decimals, 17}, :compact] with [:short] in Kernel, String.Chars.Float, List.Chars.Float
  • unlocalized_snprintf implementation — Strategy selection logic is correct; snprintf_l and uselocale paths are properly thread-safe
  • New atom registrationSHORT_ATOM added correctly to defaultatoms.def
  • CMake integration — New source/header files properly registered

@bettio
Copy link
Copy Markdown
Collaborator Author

bettio commented Mar 28, 2026

Fwiw - ryu here https://github.com/petermm/AtomVM/tree/feature/ryu-float-formatting - it does fit, but do think a smaller implementation makes sense. maybe we want ryu in future for generic_unix/wasm.

Grisu3 requires a fallback the 0.5% of the times, while Ryu doesn't. I don't think it is worth the footprint in terms of binary size.

ampcode review:

PR Review: Add short option to float_to_binary/list

Is not shortest representation — it's roundtrippable but often longer than necessary

This happens around with the 0.5% of the conversions.

The format_float helper and float_utils.h docs say the caller must ensure isfinite(value), but the NIFs don't check. If NaN/Inf ever reach formatting, the output is libc-defined and not roundtrippable.

This is a non-issue in AtomVM. Terms never store NaNs, we awlays check isinite() before making a new float term.

@petermm
Copy link
Copy Markdown
Contributor

petermm commented Mar 28, 2026

I know it repeats the infinite thing - just ignore;-)

PR Review Round 2: Add short option to float_to_binary/list

Commits reviewed:

  • bf76d9d9c — Add short option to float_to_binary/list
  • 1b6695170 — exavmlib: Use short option for float formatting

Previous issues addressed:

  • ✅ Grisu3 fallback now post-processed via fixup_g_format()
  • fixup_g_format() shared between double fallback and float32 ftoa_short
  • ✅ Single-digit scientific notation fix (len == 1 branch)
  • ✅ Option parsing simplified — "last option wins" naturally
  • unlocalized_snprintf.h doc claims scoped correctly
  • ✅ Tests massively expanded (negative zero, 2^53 boundary, subnormals, extremes, Grisu3 fallback values, badarg combos)
  • erlang.erl docs now clearly describe Grisu3 vs Ryu differences and 32-bit behavior

Verdict: Close — one remaining issue, one optional improvement


🔴 Remaining Issue: Double fallback skips v >= 2^53 → scientific rule

File: src/libAtomVM/float_utils.c:412-420

When Grisu3 fails, the fallback returns early after fixup_g_format() — it never reaches the force_scientific = (v >= TWO_POW_53) logic below. With %.17g (precision 17), %g uses scientific only when exponent < -4 or >= 17, so values around 1e16 can legally print in fixed notation, violating the documented rule.

Current code:

if (!success) {
    int32_t fb_len = unlocalized_snprintf(s2, 64, "%.17g", v);
    if (fb_len < 0 || fb_len >= 64) {
        return -1;
    }
    fb_len = fixup_g_format(s2, fb_len);
    return fb_len + (int32_t) (s2 - dst);  // <-- returns before force_scientific check
}

/* Force scientific notation for values outside (-2^53, 2^53) */
int32_t force_scientific = (v >= TWO_POW_53);

Suggested fix

Force scientific notation in the fallback path when v >= 2^53:

--- a/src/libAtomVM/float_utils.c
+++ b/src/libAtomVM/float_utils.c
@@ -412,6 +412,19 @@ static int32_t dtoa_grisu3(double v, char *dst)
     if (!success) {
         int32_t fb_len = unlocalized_snprintf(s2, 64, "%.17g", v);
         if (fb_len < 0 || fb_len >= 64) {
             return -1;
         }
         fb_len = fixup_g_format(s2, fb_len);
+        /* Enforce scientific notation for |v| >= 2^53, same as main path */
+        if (v >= TWO_POW_53 && !memchr(s2, 'e', fb_len)) {
+            /* Output is in fixed notation (e.g. "19797401206166230.0")
+             * — re-format with scientific notation */
+            fb_len = unlocalized_snprintf(s2, 64, "%.17e", v);
+            if (fb_len < 0 || fb_len >= 64) {
+                return -1;
+            }
+            fb_len = fixup_g_format(s2, fb_len);
+        }
         return fb_len + (int32_t) (s2 - dst);
     }

Suggested test addition

The current fallback-near-2^53 tests only check roundtrip. Add notation shape assertions:

%% In test_short_edge_cases(), under "ATOM" branch, add:
%% Verify fallback values above 2^53 use scientific notation
true = lists:member($e, erlang:float_to_list(?MODULE:id(1.979740120616623e+16), [short])),
true = lists:member($e, erlang:float_to_list(?MODULE:id(2.204209551024847e+16), [short])),
true = lists:member($e, erlang:float_to_list(?MODULE:id(6.3098511070304936e+16), [short])),
true = lists:member($e, erlang:float_to_list(?MODULE:id(-1.8830736066034812e+16), [short])),

🟡 Still Missing: Non-finite float guard

File: src/libAtomVM/nifs.c:2671-2674

format_float() still doesn't check isfinite(). If NaN/Inf ever reaches formatting, fixup_g_format() would turn "inf" into "inf.0" or "nan.0".

--- a/src/libAtomVM/nifs.c
+++ b/src/libAtomVM/nifs.c
@@ -2671,6 +2671,9 @@ static int format_float(term value, double_format_t format, int precision, char
 {
     avm_float_t float_value = term_to_float(value);
+    if (UNLIKELY(!isfinite(float_value))) {
+        return -1;
+    }
     return avm_float_write_to_ascii_buf(float_value, format, precision, out_buf);
 }

This is lower severity — it depends on whether non-finite floats can be materialized in AtomVM today.


✅ Looks Good (all confirmed)

Area Status
fixup_g_format() as a %g normalizer ✅ Correct — handles Ee, ensures ., strips + and leading zeros
Single-digit scientific fix (len==1 else branch) ✅ Correct — "1" d_exp=3"1.0e3"
Option parsing ("last option wins") ✅ Matches OTP — [short, {decimals,4}] → decimals, [{decimals,4}, short] → short
fixup_g_format() shared for double+float32 ✅ Safe — float32 %.9g already uses scientific for large values
unlocalized_snprintf.h doc fix ✅ Properly scoped
Test coverage ✅ Much better — negative zero, extremes, fallback values, badarg combos
erlang.erl documentation ✅ Clear about Grisu3 limitations and 32-bit behavior
Memory safety ✅ No overflow found
Elixir library updates ✅ Clean

@bettio
Copy link
Copy Markdown
Collaborator Author

bettio commented Mar 28, 2026

I know it repeats the infinite thing - just ignore;-)

PR Review Round 2: Add short option to float_to_binary/list

Fixed the last issue.

Comment thread tests/erlang_tests/float2list_short.erl Outdated
% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
%

-module(float2list_short).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to duplicate the float2bin test here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I will merge the 2 tests

bettio added 2 commits March 29, 2026 14:27
Introduce shortest-roundtrip formatting for float_to_binary/2 and
float_to_list/2 via the [short] option, matching Erlang/OTP
behavior.

- Add float_utils with Grisu3 for doubles and best-effort snprintf
  for 32-bit floats. Grisu3 was chosen over Ryu (used by
  Erlang/OTP) for its compact code and small powers table, saving
  flash space on embedded targets.
- Add unlocalized_snprintf to fix locale-dependent decimal
  separator bug in float formatting.
- Extend option ranges to {decimals, 0..253} and
  {scientific, 0..249} to match Erlang/OTP limits.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Now that float_to_binary/2 and float_to_list/2 support [short],
replace the {decimals, 17} + compact workaround with [:short].

Signed-off-by: Davide Bettio <davide@uninstall.it>
@bettio bettio merged commit f26205f into atomvm:release-0.7 Mar 30, 2026
247 of 252 checks passed
bettio added a commit that referenced this pull request Mar 30, 2026
Merge fixes, features, and optimizations from release-0.7, including:
- Add signature-driven code loader (#2229)
- Add UART support on generic_unix via POSIX termios NIFs (#2243)
- Add erlang node/1 BIF (#2225)
- Add erts_internal:cmp_term/2 NIF and fix map type ordering (#2226)
- Add short option to float_to_binary/list (#2240)
- Add locale-independent float parsing and fix overflow (#2246)
- JIT: add RISC-V 64-bit backend (#2231)
- JIT: add DWARF debug information support (#1910)
- JIT x86: instruction encoding optimizations (#2234)
- JIT: remove redundant AND when untagging integers (#2235)
- Fix bug in bs_match get_tail handling (#2242)
- Fix binary encoding of int:24 and others (#2230)
- Fix cancel_timer/1 spec and documentation (#2244)
- Resurrect opcodes emitted with no_bs_create_bin (#2245)
- Fix flaky tests related to GitHub DNS Resolver (#2232)
- Add git guide (#2106)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants