From 12c9cdedd9108530bf5b5dbf76abec6880ce8eee Mon Sep 17 00:00:00 2001 From: Daniel Kukula Date: Thu, 6 Aug 2026 21:47:20 +0200 Subject: [PATCH 1/2] remove intermediate list in float parse --- lib/elixir/lib/float.ex | 74 +++++++++++++++++++-------- lib/elixir/test/elixir/float_test.exs | 13 +++++ 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 3c530b978f1..62865eee297 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -167,40 +167,70 @@ defmodule Float do parse_unsigned(binary) end - defp parse_unsigned(<>) when digit in ?0..?9, - do: parse_unsigned(rest, false, false, [digit]) + defp parse_unsigned(<> = binary) when digit in ?0..?9, + do: parse_mantissa(binary, rest, false) defp parse_unsigned(binary) when is_binary(binary), do: :error - defp parse_unsigned(<>, dot?, e?, acc) when digit in ?0..?9, - do: parse_unsigned(rest, dot?, e?, [digit | acc]) + defp parse_mantissa(binary, <>, dot?) when digit in ?0..?9, + do: parse_mantissa(binary, rest, dot?) - defp parse_unsigned(<>, false, false, acc) when digit in ?0..?9, - do: parse_unsigned(rest, true, false, [digit, ?. | acc]) + defp parse_mantissa(binary, <>, false) when digit in ?0..?9, + do: parse_mantissa(binary, rest, true) - defp parse_unsigned(<>, dot?, false, acc) + defp parse_mantissa(binary, <> = tail, dot?) when exp_marker in ~c"eE" and digit in ?0..?9, - do: parse_unsigned(rest, true, true, [digit, ?e | add_dot(acc, dot?)]) + do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?) - defp parse_unsigned(<>, dot?, false, acc) + defp parse_mantissa(binary, <> = tail, dot?) when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9, - do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)]) - - # :erlang.binary_to_float/1 can raise an ArgumentError if the e exponent is too big. For example, - # "1.0e400". Because of this, we rescue the ArgumentError here and return an error. - defp parse_unsigned(rest, dot?, _, acc) do - acc - |> add_dot(dot?) - |> :lists.reverse() - |> :erlang.list_to_float() + do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?) + + defp parse_mantissa(binary, rest, dot?), do: finish_mantissa(binary, rest, dot?) + + defp parse_exponent(binary, exp_pos, <>, dot?) when digit in ?0..?9, + do: parse_exponent(binary, exp_pos, rest, dot?) + + defp parse_exponent(binary, exp_pos, rest, dot?), + do: finish_exponent(binary, exp_pos, rest, dot?) + + defp finish_mantissa(binary, rest, _dot? = true) do + {:erlang.binary_to_float(consumed(binary, rest)), rest} + rescue + ArgumentError -> :error + end + + # Bare integer: * 1.0 casts to the nearest float without building a new binary, + # and raises ArithmeticError on overflow (for example a 400-digit integer). + defp finish_mantissa(binary, rest, _dot? = false) do + {:erlang.binary_to_integer(consumed(binary, rest)) * 1.0, rest} + rescue + ArithmeticError -> :error + end + + # binary_to_float/1 raises ArgumentError when the exponent is too big, e.g. "1.0e400". + defp finish_exponent(binary, _exp_pos, rest, _dot? = true) do + {:erlang.binary_to_float(consumed(binary, rest)), rest} + rescue + ArgumentError -> :error + end + + # No decimal point, so ".0" is spliced in before the exponent (at exp_pos) to + # form a valid float literal. + defp finish_exponent(binary, exp_pos, rest, _dot? = false) do + len = byte_size(binary) - byte_size(rest) + + literal = + <<:binary.part(binary, 0, exp_pos)::binary, ?., ?0, + :binary.part(binary, exp_pos, len - exp_pos)::binary>> + + {:erlang.binary_to_float(literal), rest} rescue ArgumentError -> :error - else - float -> {float, rest} end - defp add_dot(acc, true), do: acc - defp add_dot(acc, false), do: [?0, ?. | acc] + defp consumed(binary, ""), do: binary + defp consumed(binary, rest), do: :binary.part(binary, 0, byte_size(binary) - byte_size(rest)) @doc """ Rounds a float to the largest float less than or equal to `number`. diff --git a/lib/elixir/test/elixir/float_test.exs b/lib/elixir/test/elixir/float_test.exs index 607accbd4b4..e43d705002f 100644 --- a/lib/elixir/test/elixir/float_test.exs +++ b/lib/elixir/test/elixir/float_test.exs @@ -51,6 +51,19 @@ defmodule FloatTest do assert Float.parse(String.duplicate("9", 310) <> ".0") === :error assert Float.parse("-" <> String.duplicate("9", 310) <> ".0") === :error assert Float.parse(String.duplicate("9", 310) <> ".0foo") === :error + + # Integer without a decimal point (parsed as an integer and cast to float) + assert Float.parse("123456789012345678901234567890") === {1.2345678901234568e29, ""} + assert Float.parse(String.duplicate("9", 300)) === {1.0e300, ""} + assert Float.parse(String.duplicate("9", 310)) === :error + assert Float.parse("-" <> String.duplicate("9", 310)) === :error + assert Float.parse(String.duplicate("9", 310) <> "foo") === :error + + # Exponent notation without a decimal point, followed by trailing characters + assert Float.parse("1e10foo") === {1.0e10, "foo"} + assert Float.parse("8e1xy") === {80.0, "xy"} + assert Float.parse("5E+2q") === {500.0, "q"} + assert Float.parse("8E4 49") === {80000.0, " 49"} end test "floor/1" do From fb219ef3d1d45c9ed98be6ded52e433b1563759d Mon Sep 17 00:00:00 2001 From: Daniel Kukula Date: Thu, 6 Aug 2026 22:23:32 +0200 Subject: [PATCH 2/2] use iodata --- lib/elixir/lib/float.ex | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 62865eee297..8a39e301f09 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -221,8 +221,11 @@ defmodule Float do len = byte_size(binary) - byte_size(rest) literal = - <<:binary.part(binary, 0, exp_pos)::binary, ?., ?0, - :binary.part(binary, exp_pos, len - exp_pos)::binary>> + IO.iodata_to_binary([ + :binary.part(binary, 0, exp_pos), + ".0", + :binary.part(binary, exp_pos, len - exp_pos) + ]) {:erlang.binary_to_float(literal), rest} rescue