From 5caab38da80c7ef83187583058622745faf03981 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 25 Sep 2024 00:29:29 +0200 Subject: [PATCH 1/2] Elixir library: fix crash with `inspect(:atom)` inspect was returning a list instead of a binary. Also fix handling of atom special cases. Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + libs/exavmlib/lib/Kernel.ex | 26 +++++++++++++++++++++----- tests/libs/exavmlib/Tests.ex | 13 +++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f84fc057..59f02c9fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ instead - Fix memory corruption in `unicode:characters_to_binary` - Fix handling of large literal indexes - `unicode:characters_to_list`: fixed bogus out_of_memory error on some platforms such as ESP32 +- Fix crash in Elixir library when doing `inspect(:atom)` ## [0.6.4] - 2024-08-18 diff --git a/libs/exavmlib/lib/Kernel.ex b/libs/exavmlib/lib/Kernel.ex index 4d5088878..b7b241875 100644 --- a/libs/exavmlib/lib/Kernel.ex +++ b/libs/exavmlib/lib/Kernel.ex @@ -42,7 +42,7 @@ defmodule Kernel do def inspect(term, opts \\ []) when is_list(opts) do case term do t when is_atom(t) -> - [?:, atom_to_string(t)] + atom_to_string(t, ":") t when is_integer(t) -> :erlang.integer_to_binary(t) @@ -118,10 +118,26 @@ defmodule Kernel do ) end - defp atom_to_string(atom) do - # TODO: use unicode rather than plain latin1 - # handle spaces and special characters - :erlang.atom_to_binary(atom, :latin1) + defp atom_to_string(atom, prefix \\ "") do + case atom do + true -> + "true" + + false -> + "false" + + nil -> + "nil" + + any_atom -> + case :erlang.atom_to_binary(any_atom) do + <<"Elixir.", displayable::binary>> -> + displayable + + other -> + <> + end + end end @doc """ diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 98e4f7bd9..782da05d1 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -26,6 +26,7 @@ defmodule Tests do :ok = test_enum() :ok = test_exception() :ok = test_chars_protocol() + :ok = test_inspect() :ok = IO.puts("Finished Elixir tests") end @@ -234,6 +235,18 @@ defmodule Tests do :ok end + def test_inspect() do + "true" = inspect(true) + "false" = inspect(false) + "nil" = inspect(nil) + + ":test" = inspect(:test) + ":アトム" = inspect(:アトム) + "Test" = inspect(Test) + + :ok + end + defp fact(n) when n < 0, do: :test defp fact(0), do: 1 defp fact(n), do: fact(n - 1) * n From 36cb1b84bd75088a67284ac6202f2db874158af9 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 25 Sep 2024 01:34:34 +0200 Subject: [PATCH 2/2] Elixir library: make inspect() more similar to Elixir one inspect() now supports lists, improper lists, it adds quotes to printable lists, etc... Signed-off-by: Davide Bettio --- CHANGELOG.md | 1 + libs/exavmlib/lib/Kernel.ex | 51 ++++++++++++++++++++++++++++++++---- tests/libs/exavmlib/Tests.ex | 48 +++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f02c9fc..3dce17c03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ instead - Fix handling of large literal indexes - `unicode:characters_to_list`: fixed bogus out_of_memory error on some platforms such as ESP32 - Fix crash in Elixir library when doing `inspect(:atom)` +- General inspect() compliance with Elixir behavior (but there are still some minor differences) ## [0.6.4] - 2024-08-18 diff --git a/libs/exavmlib/lib/Kernel.ex b/libs/exavmlib/lib/Kernel.ex index b7b241875..491212d87 100644 --- a/libs/exavmlib/lib/Kernel.ex +++ b/libs/exavmlib/lib/Kernel.ex @@ -48,8 +48,13 @@ defmodule Kernel do :erlang.integer_to_binary(t) t when is_list(t) -> - # TODO: escape unprintable lists - :erlang.list_to_binary(t) + if is_printable_list(t) do + str = :erlang.list_to_binary(t) + <> + else + [?[ | t |> inspect_join(?])] + |> :erlang.list_to_binary() + end t when is_pid(t) -> :erlang.pid_to_list(t) @@ -64,15 +69,19 @@ defmodule Kernel do |> :erlang.list_to_binary() t when is_binary(t) -> - # TODO: escape unprintable binaries - t + if is_printable_binary(t) do + <> + else + ["<<" | t |> :erlang.binary_to_list() |> inspect_join(">>")] + |> :erlang.list_to_binary() + end t when is_reference(t) -> :erlang.ref_to_list(t) |> :erlang.list_to_binary() t when is_float(t) -> - :erlang.float_to_binary(t) + :erlang.float_to_binary(term, [{:decimals, 17}, :compact]) t when is_map(t) -> [?%, ?{ | t |> inspect_kv() |> join(?})] @@ -88,6 +97,10 @@ defmodule Kernel do [inspect(e), last] end + defp inspect_join([h | e], last) when not is_list(e) do + [inspect(h), " | ", inspect(e), last] + end + defp inspect_join([h | t], last) do [inspect(h), ?,, ?\s | inspect_join(t, last)] end @@ -140,6 +153,34 @@ defmodule Kernel do end end + defp is_printable_list([]), do: false + + defp is_printable_list([char]) do + is_printable_ascii(char) + end + + defp is_printable_list([char | t]) do + if is_printable_ascii(char) do + is_printable_list(t) + else + false + end + end + + defp is_printable_list(_any), do: false + + defp is_printable_ascii(char) do + is_integer(char) and char >= 32 and char < 127 and char != ?' + end + + defp is_printable_binary(<<>>), do: true + + defp is_printable_binary(<>) when char >= 32 do + is_printable_binary(rest) + end + + defp is_printable_binary(_any), do: false + @doc """ Returns the biggest of the two given terms according to Erlang's term ordering. diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex index 782da05d1..fe34c30e8 100644 --- a/tests/libs/exavmlib/Tests.ex +++ b/tests/libs/exavmlib/Tests.ex @@ -19,6 +19,11 @@ # defmodule Tests do + # defstruct [ + # :field1, + # field2: 42 + # ] + @compile {:no_warn_undefined, :undef} def start() do @@ -244,10 +249,53 @@ defmodule Tests do ":アトム" = inspect(:アトム) "Test" = inspect(Test) + "5" = inspect(5) + "5.0" = inspect(5.0) + + ~s[""] = inspect("") + ~s["hello"] = inspect("hello") + ~s["アトム"] = inspect("アトム") + + "<<10>>" = inspect("\n") + "<<0, 1, 2, 3>>" = inspect(<<0, 1, 2, 3>>) + "<<195, 168, 0>>" = inspect(<<195, 168, 0>>) + + "[]" = inspect([]) + "[0]" = inspect([0]) + "[9, 10]" = inspect([9, 10]) + ~s'["test"]' = inspect(["test"]) + "'hello'" = inspect('hello') + "[127]" = inspect([127]) + "[104, 101, 108, 108, 248]" = inspect('hellø') + + ~s([5 | "hello"]) = inspect([5 | "hello"]) + + "{}" = inspect({}) + "{1, 2}" = inspect({1, 2}) + "{:test, 1}" = inspect({:test, 1}) + + "%{}" = inspect(%{}) + either("%{a: 1, b: 2}", "%{b: 2, a: 1}", inspect(%{a: 1, b: 2})) + either(~s[%{"a" => 1, "b" => 2}], ~s[%{"b" => 2, "a" => 1}], inspect(%{"a" => 1, "b" => 2})) + + # TODO: structs are not yet supported + # either( + # ~s[%#{__MODULE__}{field1: nil, field2: 42}], + # ~s[%#{__MODULE__}{field2: 42, field1: nil}], + # inspect(%__MODULE__{}) + # ) + :ok end defp fact(n) when n < 0, do: :test defp fact(0), do: 1 defp fact(n), do: fact(n - 1) * n + + def either(a, b, value) do + case value do + ^a -> a + ^b -> b + end + end end