diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index f6231f73d..74047161e 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -372,6 +372,16 @@ jobs: ./src/AtomVM ./tests/libs/alisp/test_alisp.avm valgrind ./src/AtomVM ./tests/libs/alisp/test_alisp.avm + - name: "Test: Tests.avm (Elixir)" + timeout-minutes: 10 + working-directory: build + run: | + if command -v elixirc &> /dev/null + then + ./src/AtomVM ./tests/libs/exavmlib/Tests.avm + valgrind ./src/AtomVM ./tests/libs/exavmlib/Tests.avm + fi + - name: "Install and smoke test" working-directory: build run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index d2fe23964..4f3ea72e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implement `gpio:init/1` on esp32 to initialize pins for GPIO usage, which some pins require depending on default function and bootloader code - Implement missing opcode 161 (raw_raise), that looks more likely to be generated with Elixir code +- Support for Elixir `Map.replace/3` and `Map.replace!/3` +- Support for Elixir `Kernel.struct` and `Kernel.struct!` +- Support for Elixir `IO.iodata_to_binary/1` +- Support for Elixir exceptions: `Exception` module and the other error related modules such as +`ArgumentError`, `UndefinedFunctionError`, etc... +- Support for Elixir `Enumerable` and `Collectable` protocol +- Support for Elixir `Enum` functions: `split_with`, `join`, `map_join`, `into`, `reverse`, +`slice` and `to_list` +- Support for Elixir `MapSet` module +- Support for Elixir `Range` module +- Support for Elixir `Kernel.min` and `Kernel.max` ## [0.6.3] - 20-07-2024 diff --git a/libs/exavmlib/lib/ArgumentError.ex b/libs/exavmlib/lib/ArgumentError.ex new file mode 100644 index 000000000..8bec61c8d --- /dev/null +++ b/libs/exavmlib/lib/ArgumentError.ex @@ -0,0 +1,27 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule ArgumentError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception message: "argument error" +end diff --git a/libs/exavmlib/lib/ArithmeticError.ex b/libs/exavmlib/lib/ArithmeticError.ex new file mode 100644 index 000000000..2e652c5f0 --- /dev/null +++ b/libs/exavmlib/lib/ArithmeticError.ex @@ -0,0 +1,27 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule ArithmeticError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception message: "bad argument in arithmetic expression" +end diff --git a/libs/exavmlib/lib/BadArityError.ex b/libs/exavmlib/lib/BadArityError.ex new file mode 100644 index 000000000..57551be18 --- /dev/null +++ b/libs/exavmlib/lib/BadArityError.ex @@ -0,0 +1,42 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule BadArityError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:function, :args] + + @impl true + def message(exception) do + fun = exception.function + args = exception.args + insp = Enum.map_join(args, ", ", &inspect/1) + # TODO: enable as soon as :erlang.fun_info and Function.info are implemented + # {:arity, arity} = Function.info(fun, :arity) + # "#{inspect(fun)} with arity #{arity} called with #{count(length(args), insp)}" + "#{inspect(fun)} called with #{count(length(args), insp)}" + end + + defp count(0, _insp), do: "no arguments" + defp count(1, insp), do: "1 argument (#{insp})" + defp count(x, insp), do: "#{x} arguments (#{insp})" +end diff --git a/libs/exavmlib/lib/BadBooleanError.ex b/libs/exavmlib/lib/BadBooleanError.ex new file mode 100644 index 000000000..1a343b965 --- /dev/null +++ b/libs/exavmlib/lib/BadBooleanError.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule BadBooleanError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:term, :operator] + + @impl true + def message(exception) do + "expected a boolean on left-side of \"#{exception.operator}\", got: #{inspect(exception.term)}" + end +end diff --git a/libs/exavmlib/lib/BadFunctionError.ex b/libs/exavmlib/lib/BadFunctionError.ex new file mode 100644 index 000000000..96d890ef5 --- /dev/null +++ b/libs/exavmlib/lib/BadFunctionError.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule BadFunctionError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:term] + + @impl true + def message(exception) do + "expected a function, got: #{inspect(exception.term)}" + end +end diff --git a/libs/exavmlib/lib/BadMapError.ex b/libs/exavmlib/lib/BadMapError.ex new file mode 100644 index 000000000..add5cbe08 --- /dev/null +++ b/libs/exavmlib/lib/BadMapError.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule BadMapError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:term] + + @impl true + def message(exception) do + "expected a map, got: #{inspect(exception.term)}" + end +end diff --git a/libs/exavmlib/lib/BadStructError.ex b/libs/exavmlib/lib/BadStructError.ex new file mode 100644 index 000000000..f8bdb6e9f --- /dev/null +++ b/libs/exavmlib/lib/BadStructError.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule BadStructError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:struct, :term] + + @impl true + def message(exception) do + "expected a struct named #{inspect(exception.struct)}, got: #{inspect(exception.term)}" + end +end diff --git a/libs/exavmlib/lib/CMakeLists.txt b/libs/exavmlib/lib/CMakeLists.txt index d4c63c1fa..92deabdb6 100644 --- a/libs/exavmlib/lib/CMakeLists.txt +++ b/libs/exavmlib/lib/CMakeLists.txt @@ -33,14 +33,47 @@ set(ELIXIR_MODULES LEDC Access Enum + Enumerable + Enumerable.List + Enumerable.Map + Enumerable.MapSet + Enumerable.Range + Exception IO List Map + MapSet Module Keyword Kernel Process + Protocol.UndefinedError + Range Tuple + + ArithmeticError + ArgumentError + BadFunctionError + BadStructError + RuntimeError + SystemLimitError + BadMapError + BadBooleanError + MatchError + CaseClauseError + WithClauseError + CondClauseError + TryClauseError + BadArityError + UndefinedFunctionError + FunctionClauseError + KeyError + ErlangError + + Collectable + Collectable.List + Collectable.Map + Collectable.MapSet ) pack_archive(exavmlib ${ELIXIR_MODULES}) diff --git a/libs/exavmlib/lib/CaseClauseError.ex b/libs/exavmlib/lib/CaseClauseError.ex new file mode 100644 index 000000000..c5c60551b --- /dev/null +++ b/libs/exavmlib/lib/CaseClauseError.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule CaseClauseError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:term] + + @impl true + def message(exception) do + "no case clause matching: #{inspect(exception.term)}" + end +end diff --git a/libs/exavmlib/lib/Collectable.List.ex b/libs/exavmlib/lib/Collectable.List.ex new file mode 100644 index 000000000..043bf0dde --- /dev/null +++ b/libs/exavmlib/lib/Collectable.List.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.7.4/lib/elixir/lib/collectable.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl Collectable, for: List do + def into(original) do + fun = fn + list, {:cont, x} -> [x | list] + list, :done -> original ++ :lists.reverse(list) + _, :halt -> :ok + end + + {[], fun} + end +end diff --git a/libs/exavmlib/lib/Collectable.Map.ex b/libs/exavmlib/lib/Collectable.Map.ex new file mode 100644 index 000000000..44bf17a4f --- /dev/null +++ b/libs/exavmlib/lib/Collectable.Map.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.7.4/lib/elixir/lib/collectable.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl Collectable, for: Map do + def into(original) do + fun = fn + map, {:cont, {k, v}} -> :maps.put(k, v, map) + map, :done -> map + _, :halt -> :ok + end + + {original, fun} + end +end diff --git a/libs/exavmlib/lib/Collectable.MapSet.ex b/libs/exavmlib/lib/Collectable.MapSet.ex new file mode 100644 index 000000000..6ab2b1e9b --- /dev/null +++ b/libs/exavmlib/lib/Collectable.MapSet.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2024 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.17.2/lib/elixir/lib/map_set.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl Collectable, for: MapSet do + def into(%@for{map: set} = map_set) do + fun = fn + list, {:cont, x} -> [x | list] + list, :done -> %{map_set | map: :sets.union(set, :sets.from_list(list, version: 2))} + _, :halt -> :ok + end + + {[], fun} + end +end diff --git a/libs/exavmlib/lib/Collectable.ex b/libs/exavmlib/lib/Collectable.ex new file mode 100644 index 000000000..a25b6d068 --- /dev/null +++ b/libs/exavmlib/lib/Collectable.ex @@ -0,0 +1,99 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.7.4/lib/elixir/lib/collectable.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defprotocol Collectable do + @moduledoc """ + A protocol to traverse data structures. + + The `Enum.into/2` function uses this protocol to insert an + enumerable into a collection: + + iex> Enum.into([a: 1, b: 2], %{}) + %{a: 1, b: 2} + + ## Why Collectable? + + The `Enumerable` protocol is useful to take values out of a collection. + In order to support a wide range of values, the functions provided by + the `Enumerable` protocol do not keep shape. For example, passing a + map to `Enum.map/2` always returns a list. + + This design is intentional. `Enumerable` was designed to support infinite + collections, resources and other structures with fixed shape. For example, + it doesn't make sense to insert values into a range, as it has a fixed + shape where just the range limits are stored. + + The `Collectable` module was designed to fill the gap left by the + `Enumerable` protocol. `into/1` can be seen as the opposite of + `Enumerable.reduce/3`. If `Enumerable` is about taking values out, + `Collectable.into/1` is about collecting those values into a structure. + + ## Examples + + To show how to manually use the `Collectable` protocol, let's play with its + implementation for `MapSet`. + + iex> {initial_acc, collector_fun} = Collectable.into(MapSet.new()) + iex> updated_acc = Enum.reduce([1, 2, 3], initial_acc, fn elem, acc -> + ...> collector_fun.(acc, {:cont, elem}) + ...> end) + iex> collector_fun.(updated_acc, :done) + #MapSet<[1, 2, 3]> + + To show how the protocol can be implemented, we can take again a look at the + implementation for `MapSet`. In this implementation "collecting" elements + simply means inserting them in the set through `MapSet.put/2`. + + defimpl Collectable do + def into(original) do + collector_fun = fn + set, {:cont, elem} -> MapSet.put(set, elem) + set, :done -> set + _set, :halt -> :ok + end + + {original, collector_fun} + end + end + + """ + + @type command :: {:cont, term} | :done | :halt + + @doc """ + Returns an initial accumulator and a "collector" function. + + The returned function receives a term and a command and injects the term into + the collectable on every `{:cont, term}` command. + + `:done` is passed as a command when no further values will be injected. This + is useful when there's a need to close resources or normalizing values. A + collectable must be returned when the command is `:done`. + + If injection is suddenly interrupted, `:halt` is passed and the function + can return any value as it won't be used. + + For examples on how to use the `Collectable` protocol and `into/1` see the + module documentation. + """ + @spec into(t) :: {term, (term, command -> t | term)} + def into(collectable) +end diff --git a/libs/exavmlib/lib/CondClauseError.ex b/libs/exavmlib/lib/CondClauseError.ex new file mode 100644 index 000000000..c25e4e3b7 --- /dev/null +++ b/libs/exavmlib/lib/CondClauseError.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule CondClauseError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [] + + @impl true + def message(_exception) do + "no cond clause evaluated to a true value" + end +end diff --git a/libs/exavmlib/lib/Enum.ex b/libs/exavmlib/lib/Enum.ex index ba9bd38a5..c702db55b 100644 --- a/libs/exavmlib/lib/Enum.ex +++ b/libs/exavmlib/lib/Enum.ex @@ -23,14 +23,36 @@ defmodule Enum do # This avoids crashing the compiler at build time @compile {:autoload, false} + @type t :: Enumerable.t() + @type index :: integer + @type element :: any + + require Stream.Reducers, as: R + + defmacrop next(_, entry, acc) do + quote(do: [unquote(entry) | unquote(acc)]) + end + def reduce(enumerable, acc, fun) when is_list(enumerable) do :lists.foldl(fun, acc, enumerable) end + def reduce(%_{} = enumerable, acc, fun) do + reduce_enumerable(enumerable, acc, fun) + end + def reduce(%{} = enumerable, acc, fun) do :maps.fold(fn k, v, acc -> fun.({k, v}, acc) end, acc, enumerable) end + def reduce(enumerable, acc, fun) do + reduce_enumerable(enumerable, acc, fun) + end + + defp reduce_enumerable(enumerable, acc, fun) do + Enumerable.reduce(enumerable, {:cont, acc}, fn x, acc -> {:cont, fun.(x, acc)} end) |> elem(1) + end + def all?(enumerable, fun) when is_list(enumerable) do all_list(enumerable, fun) end @@ -39,10 +61,30 @@ defmodule Enum do any_list(enumerable, fun) end + @doc """ + Returns the size of the enumerable. + + ## Examples + + iex> Enum.count([1, 2, 3]) + 3 + + """ + @spec count(t) :: non_neg_integer def count(enumerable) when is_list(enumerable) do length(enumerable) end + def count(enumerable) do + case Enumerable.count(enumerable) do + {:ok, value} when is_integer(value) -> + value + + {:error, module} -> + enumerable |> module.reduce({:cont, 0}, fn _, acc -> {:cont, acc + 1} end) |> elem(1) + end + end + def each(enumerable, fun) when is_list(enumerable) do :lists.foreach(fun, enumerable) :ok @@ -64,14 +106,108 @@ defmodule Enum do find_value_list(enumerable, default, fun) end + @doc """ + Returns a list where each element is the result of invoking + `fun` on each corresponding element of `enumerable`. + + For maps, the function expects a key-value tuple. + + ## Examples + + iex> Enum.map([1, 2, 3], fn x -> x * 2 end) + [2, 4, 6] + + iex> Enum.map([a: 1, b: 2], fn {k, v} -> {k, -v} end) + [a: -1, b: -2] + + """ + @spec map(t, (element -> any)) :: list + def map(enumerable, fun) + def map(enumerable, fun) when is_list(enumerable) do :lists.map(fun, enumerable) end + def map(enumerable, fun) do + reduce(enumerable, [], R.map(fun)) |> :lists.reverse() + end + + @doc """ + Maps and joins the given enumerable in one pass. + + `joiner` can be either a binary or a list and the result will be of + the same type as `joiner`. + If `joiner` is not passed at all, it defaults to an empty binary. + + All items returned from invoking the `mapper` must be convertible to + a binary, otherwise an error is raised. + + ## Examples + + iex> Enum.map_join([1, 2, 3], &(&1 * 2)) + "246" + + iex> Enum.map_join([1, 2, 3], " = ", &(&1 * 2)) + "2 = 4 = 6" + + """ + @spec map_join(t, String.t(), (element -> String.Chars.t())) :: String.t() + def map_join(enumerable, joiner \\ "", mapper) + + def map_join(enumerable, joiner, mapper) when is_binary(joiner) do + reduced = + reduce(enumerable, :first, fn + entry, :first -> entry_to_string(mapper.(entry)) + entry, acc -> [acc, joiner | entry_to_string(mapper.(entry))] + end) + + if reduced == :first do + "" + else + IO.iodata_to_binary(reduced) + end + end + + @doc """ + Checks if `element` exists within the enumerable. + + Membership is tested with the match (`===/2`) operator. + + ## Examples + + iex> Enum.member?(1..10, 5) + true + iex> Enum.member?(1..10, 5.0) + false + + iex> Enum.member?([1.0, 2.0, 3.0], 2) + false + iex> Enum.member?([1.0, 2.0, 3.0], 2.000) + true + + iex> Enum.member?([:a, :b, :c], :d) + false + + """ + @spec member?(t, element) :: boolean def member?(enumerable, element) when is_list(enumerable) do :lists.member(element, enumerable) end + def member?(enumerable, element) do + case Enumerable.member?(enumerable, element) do + {:ok, element} when is_boolean(element) -> + element + + {:error, module} -> + module.reduce(enumerable, {:cont, false}, fn + v, _ when v === element -> {:halt, true} + _, _ -> {:cont, false} + end) + |> elem(1) + end + end + def reject(enumerable, fun) when is_list(enumerable) do reject_list(enumerable, fun) end @@ -156,6 +292,137 @@ defmodule Enum do default end + @doc """ + Inserts the given `enumerable` into a `collectable`. + + ## Examples + + iex> Enum.into([1, 2], [0]) + [0, 1, 2] + + iex> Enum.into([a: 1, b: 2], %{}) + %{a: 1, b: 2} + + iex> Enum.into(%{a: 1}, %{b: 2}) + %{a: 1, b: 2} + + iex> Enum.into([a: 1, a: 2], %{}) + %{a: 2} + + """ + @spec into(Enumerable.t(), Collectable.t()) :: Collectable.t() + def into(enumerable, collectable) when is_list(collectable) do + collectable ++ to_list(enumerable) + end + + def into(%_{} = enumerable, collectable) do + into_protocol(enumerable, collectable) + end + + def into(enumerable, %_{} = collectable) do + into_protocol(enumerable, collectable) + end + + def into(%{} = enumerable, %{} = collectable) do + Map.merge(collectable, enumerable) + end + + def into(enumerable, %{} = collectable) when is_list(enumerable) do + Map.merge(collectable, :maps.from_list(enumerable)) + end + + def into(enumerable, %{} = collectable) do + reduce(enumerable, collectable, fn {key, val}, acc -> + Map.put(acc, key, val) + end) + end + + def into(enumerable, collectable) do + into_protocol(enumerable, collectable) + end + + defp into_protocol(enumerable, collectable) do + {initial, fun} = Collectable.into(collectable) + + into(enumerable, initial, fun, fn entry, acc -> + fun.(acc, {:cont, entry}) + end) + end + + @doc """ + Inserts the given `enumerable` into a `collectable` according to the + transformation function. + + ## Examples + + iex> Enum.into([2, 3], [3], fn x -> x * 3 end) + [3, 6, 9] + + iex> Enum.into(%{a: 1, b: 2}, %{c: 3}, fn {k, v} -> {k, v * 2} end) + %{a: 2, b: 4, c: 3} + + """ + @spec into(Enumerable.t(), Collectable.t(), (term -> term)) :: Collectable.t() + + def into(enumerable, collectable, transform) when is_list(collectable) do + collectable ++ map(enumerable, transform) + end + + def into(enumerable, collectable, transform) do + {initial, fun} = Collectable.into(collectable) + + into(enumerable, initial, fun, fn entry, acc -> + fun.(acc, {:cont, transform.(entry)}) + end) + end + + defp into(enumerable, initial, fun, callback) do + try do + reduce(enumerable, initial, callback) + catch + kind, reason -> + fun.(initial, :halt) + :erlang.raise(kind, reason, __STACKTRACE__) + else + acc -> fun.(acc, :done) + end + end + + @doc """ + Joins the given enumerable into a binary using `joiner` as a + separator. + + If `joiner` is not passed at all, it defaults to the empty binary. + + All items in the enumerable must be convertible to a binary, + otherwise an error is raised. + + ## Examples + + iex> Enum.join([1, 2, 3]) + "123" + + iex> Enum.join([1, 2, 3], " = ") + "1 = 2 = 3" + + """ + @spec join(t, String.t()) :: String.t() + def join(enumerable, joiner \\ "") + + def join(enumerable, joiner) when is_binary(joiner) do + reduced = + reduce(enumerable, :first, fn + entry, :first -> entry_to_string(entry) + entry, acc -> [acc, joiner | entry_to_string(entry)] + end) + + if reduced == :first do + "" + else + IO.iodata_to_binary(reduced) + end + end + ## reject defp reject_list([head | tail], fun) do @@ -169,4 +436,254 @@ defmodule Enum do defp reject_list([], _fun) do [] end + + @doc """ + Returns a list of elements in `enumerable` in reverse order. + + ## Examples + + iex> Enum.reverse([1, 2, 3]) + [3, 2, 1] + + """ + @spec reverse(t) :: list + def reverse(enumerable) + + def reverse([]), do: [] + def reverse([_] = list), do: list + def reverse([item1, item2]), do: [item2, item1] + def reverse([item1, item2 | rest]), do: :lists.reverse(rest, [item2, item1]) + def reverse(enumerable), do: reduce(enumerable, [], &[&1 | &2]) + + @doc """ + Returns a subset list of the given enumerable, from `range.first` to `range.last` positions. + + Given `enumerable`, it drops elements until element position `range.first`, + then takes elements until element position `range.last` (inclusive). + + Positions are normalized, meaning that negative positions will be counted from the end + (e.g. `-1` means the last element of the enumerable). + If `range.last` is out of bounds, then it is assigned as the position of the last element. + + If the normalized `range.first` position is out of bounds of the given enumerable, + or this one is greater than the normalized `range.last` position, then `[]` is returned. + + ## Examples + + iex> Enum.slice(1..100, 5..10) + [6, 7, 8, 9, 10, 11] + + iex> Enum.slice(1..10, 5..20) + [6, 7, 8, 9, 10] + + # last five elements (negative positions) + iex> Enum.slice(1..30, -5..-1) + [26, 27, 28, 29, 30] + + # last five elements (mixed positive and negative positions) + iex> Enum.slice(1..30, 25..-1) + [26, 27, 28, 29, 30] + + # out of bounds + iex> Enum.slice(1..10, 11..20) + [] + + # range.first is greater than range.last + iex> Enum.slice(1..10, 6..5) + [] + + """ + @doc since: "1.6.0" + @spec slice(t, Range.t()) :: list + def slice(enumerable, first..last) do + {count, fun} = slice_count_and_fun(enumerable) + corr_first = if first >= 0, do: first, else: first + count + corr_last = if last >= 0, do: last, else: last + count + amount = corr_last - corr_first + 1 + + if corr_first >= 0 and corr_first < count and amount > 0 do + fun.(corr_first, Kernel.min(amount, count - corr_first)) + else + [] + end + end + + @doc """ + Returns a subset list of the given enumerable, from `start` position with `amount` of elements if available. + + Given `enumerable`, it drops elements until element position `start`, + then takes `amount` of elements until the end of the enumerable. + + If `start` is out of bounds, it returns `[]`. + + If `amount` is greater than `enumerable` length, it returns as many elements as possible. + If `amount` is zero, then `[]` is returned. + + ## Examples + + iex> Enum.slice(1..100, 5, 10) + [6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + + # amount to take is greater than the number of elements + iex> Enum.slice(1..10, 5, 100) + [6, 7, 8, 9, 10] + + iex> Enum.slice(1..10, 5, 0) + [] + + # out of bound start position + iex> Enum.slice(1..10, 10, 5) + [] + + # out of bound start position (negative) + iex> Enum.slice(1..10, -11, 5) + [] + + """ + @spec slice(t, index, non_neg_integer) :: list + def slice(_enumerable, start, 0) when is_integer(start), do: [] + + def slice(enumerable, start, amount) + when is_integer(start) and is_integer(amount) and amount >= 0 do + slice_any(enumerable, start, amount) + end + + @doc """ + Splits the `enumerable` in two lists according to the given function `fun`. + + Splits the given `enumerable` in two lists by calling `fun` with each element + in the `enumerable` as its only argument. Returns a tuple with the first list + containing all the elements in `enumerable` for which applying `fun` returned + a truthy value, and a second list with all the elements for which applying + `fun` returned a falsy value (`false` or `nil`). + + The elements in both the returned lists are in the same relative order as they + were in the original enumerable (if such enumerable was ordered, e.g., a + list); see the examples below. + + ## Examples + + iex> Enum.split_with([5, 4, 3, 2, 1, 0], fn x -> rem(x, 2) == 0 end) + {[4, 2, 0], [5, 3, 1]} + + iex> Enum.split_with(%{a: 1, b: -2, c: 1, d: -3}, fn {_k, v} -> v < 0 end) + {[b: -2, d: -3], [a: 1, c: 1]} + + iex> Enum.split_with(%{a: 1, b: -2, c: 1, d: -3}, fn {_k, v} -> v > 50 end) + {[], [a: 1, b: -2, c: 1, d: -3]} + + iex> Enum.split_with(%{}, fn {_k, v} -> v > 50 end) + {[], []} + + """ + @doc since: "1.4.0" + def split_with(enumerable, fun) do + {acc1, acc2} = + reduce(enumerable, {[], []}, fn entry, {acc1, acc2} -> + if fun.(entry) do + {[entry | acc1], acc2} + else + {acc1, [entry | acc2]} + end + end) + + {:lists.reverse(acc1), :lists.reverse(acc2)} + end + + @doc """ + Converts `enumerable` to a list. + + ## Examples + + iex> Enum.to_list(1..3) + [1, 2, 3] + + """ + @spec to_list(t) :: [element] + def to_list(enumerable) when is_list(enumerable), do: enumerable + def to_list(%_{} = enumerable), do: reverse(enumerable) |> :lists.reverse() + def to_list(%{} = enumerable), do: Map.to_list(enumerable) + def to_list(enumerable), do: reverse(enumerable) |> :lists.reverse() + + # helpers + + @compile {:inline, entry_to_string: 1, reduce: 3} + + defp entry_to_string(entry) when is_binary(entry), do: entry + + ## drop + + defp drop_list(list, 0), do: list + defp drop_list([_ | tail], counter), do: drop_list(tail, counter - 1) + defp drop_list([], _), do: [] + + ## slice + + defp slice_any(enumerable, start, amount) when start < 0 do + {count, fun} = slice_count_and_fun(enumerable) + start = count + start + + if start >= 0 do + fun.(start, Kernel.min(amount, count - start)) + else + [] + end + end + + defp slice_any(list, start, amount) when is_list(list) do + list |> drop_list(start) |> take_list(amount) + end + + defp slice_any(enumerable, start, amount) do + case Enumerable.slice(enumerable) do + {:ok, count, _} when start >= count -> + [] + + {:ok, count, fun} when is_function(fun) -> + fun.(start, Kernel.min(amount, count - start)) + + {:error, module} -> + slice_enum(enumerable, module, start, amount) + end + end + + defp slice_enum(enumerable, module, start, amount) do + {_, {_, _, slice}} = + module.reduce(enumerable, {:cont, {start, amount, []}}, fn + _entry, {start, amount, _list} when start > 0 -> + {:cont, {start - 1, amount, []}} + + entry, {start, amount, list} when amount > 1 -> + {:cont, {start, amount - 1, [entry | list]}} + + entry, {start, amount, list} -> + {:halt, {start, amount, [entry | list]}} + end) + + :lists.reverse(slice) + end + + defp slice_count_and_fun(enumerable) when is_list(enumerable) do + length = length(enumerable) + {length, &Enumerable.List.slice(enumerable, &1, &2, length)} + end + + defp slice_count_and_fun(enumerable) do + case Enumerable.slice(enumerable) do + {:ok, count, fun} when is_function(fun) -> + {count, fun} + + {:error, module} -> + {_, {list, count}} = + module.reduce(enumerable, {:cont, {[], 0}}, fn elem, {acc, count} -> + {:cont, {[elem | acc], count + 1}} + end) + + {count, &Enumerable.List.slice(:lists.reverse(list), &1, &2, count)} + end + end + + defp take_list([head | _], 1), do: [head] + defp take_list([head | tail], counter), do: [head | take_list(tail, counter - 1)] + defp take_list([], _counter), do: [] end diff --git a/libs/exavmlib/lib/Enumerable.List.ex b/libs/exavmlib/lib/Enumerable.List.ex new file mode 100644 index 000000000..13a34d0b2 --- /dev/null +++ b/libs/exavmlib/lib/Enumerable.List.ex @@ -0,0 +1,42 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2017 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.10.1/lib/elixir/lib/enumerable.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl Enumerable, for: List do + def count(_list), do: {:error, __MODULE__} + def member?(_list, _value), do: {:error, __MODULE__} + def slice(_list), do: {:error, __MODULE__} + + def reduce(_list, {:halt, acc}, _fun), do: {:halted, acc} + def reduce(list, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(list, &1, fun)} + def reduce([], {:cont, acc}, _fun), do: {:done, acc} + def reduce([head | tail], {:cont, acc}, fun), do: reduce(tail, fun.(head, acc), fun) + + @doc false + def slice(_list, _start, 0, _size), do: [] + def slice(list, start, count, size) when start + count == size, do: list |> drop(start) + def slice(list, start, count, _size), do: list |> drop(start) |> take(count) + + defp drop(list, 0), do: list + defp drop([_ | tail], count), do: drop(tail, count - 1) + + defp take(_list, 0), do: [] + defp take([head | tail], count), do: [head | take(tail, count - 1)] +end diff --git a/libs/exavmlib/lib/Enumerable.Map.ex b/libs/exavmlib/lib/Enumerable.Map.ex new file mode 100644 index 000000000..cc3401fa0 --- /dev/null +++ b/libs/exavmlib/lib/Enumerable.Map.ex @@ -0,0 +1,48 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2017 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.10.1/lib/elixir/lib/enumerable.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl Enumerable, for: Map do + def count(map) do + {:ok, map_size(map)} + end + + def member?(map, {key, value}) do + {:ok, match?(%{^key => ^value}, map)} + end + + def member?(_map, _other) do + {:ok, false} + end + + def slice(map) do + size = map_size(map) + {:ok, size, &Enumerable.List.slice(:maps.to_list(map), &1, &2, size)} + end + + def reduce(map, acc, fun) do + reduce_list(:maps.to_list(map), acc, fun) + end + + defp reduce_list(_list, {:halt, acc}, _fun), do: {:halted, acc} + defp reduce_list(list, {:suspend, acc}, fun), do: {:suspended, acc, &reduce_list(list, &1, fun)} + defp reduce_list([], {:cont, acc}, _fun), do: {:done, acc} + defp reduce_list([head | tail], {:cont, acc}, fun), do: reduce_list(tail, fun.(head, acc), fun) +end diff --git a/libs/exavmlib/lib/Enumerable.MapSet.ex b/libs/exavmlib/lib/Enumerable.MapSet.ex new file mode 100644 index 000000000..4ff68716f --- /dev/null +++ b/libs/exavmlib/lib/Enumerable.MapSet.ex @@ -0,0 +1,39 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2024 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.17.2/lib/elixir/lib/map_set.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl Enumerable, for: MapSet do + def count(map_set) do + {:ok, MapSet.size(map_set)} + end + + def member?(map_set, val) do + {:ok, MapSet.member?(map_set, val)} + end + + def reduce(map_set, acc, fun) do + Enumerable.List.reduce(MapSet.to_list(map_set), acc, fun) + end + + def slice(map_set) do + size = MapSet.size(map_set) + {:ok, size, &MapSet.to_list/1} + end +end diff --git a/libs/exavmlib/lib/Enumerable.Range.ex b/libs/exavmlib/lib/Enumerable.Range.ex new file mode 100644 index 000000000..791be52cf --- /dev/null +++ b/libs/exavmlib/lib/Enumerable.Range.ex @@ -0,0 +1,80 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2020 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.10.4/lib/elixir/lib/enum.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defimpl Enumerable, for: Range do + def reduce(first..last, acc, fun) do + reduce(first, last, acc, fun, _up? = last >= first) + end + + defp reduce(_first, _last, {:halt, acc}, _fun, _up?) do + {:halted, acc} + end + + defp reduce(first, last, {:suspend, acc}, fun, up?) do + {:suspended, acc, &reduce(first, last, &1, fun, up?)} + end + + defp reduce(first, last, {:cont, acc}, fun, _up? = true) when first <= last do + reduce(first + 1, last, fun.(first, acc), fun, _up? = true) + end + + defp reduce(first, last, {:cont, acc}, fun, _up? = false) when first >= last do + reduce(first - 1, last, fun.(first, acc), fun, _up? = false) + end + + defp reduce(_, _, {:cont, acc}, _fun, _up) do + {:done, acc} + end + + def member?(first..last, value) when is_integer(value) do + if first <= last do + {:ok, first <= value and value <= last} + else + {:ok, last <= value and value <= first} + end + end + + def member?(_.._, _value) do + {:ok, false} + end + + def count(first..last) do + if first <= last do + {:ok, last - first + 1} + else + {:ok, first - last + 1} + end + end + + def slice(first..last) do + if first <= last do + {:ok, last - first + 1, &slice_asc(first + &1, &2)} + else + {:ok, first - last + 1, &slice_desc(first - &1, &2)} + end + end + + defp slice_asc(current, 1), do: [current] + defp slice_asc(current, remaining), do: [current | slice_asc(current + 1, remaining - 1)] + + defp slice_desc(current, 1), do: [current] + defp slice_desc(current, remaining), do: [current | slice_desc(current - 1, remaining - 1)] +end diff --git a/libs/exavmlib/lib/Enumerable.ex b/libs/exavmlib/lib/Enumerable.ex new file mode 100644 index 000000000..bbbd57f5c --- /dev/null +++ b/libs/exavmlib/lib/Enumerable.ex @@ -0,0 +1,218 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2017 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.10.1/lib/elixir/lib/enum.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defprotocol Enumerable do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + @moduledoc """ + Enumerable protocol used by `Enum` and `Stream` modules. + + When you invoke a function in the `Enum` module, the first argument + is usually a collection that must implement this protocol. + For example, the expression: + + Enum.map([1, 2, 3], &(&1 * 2)) + + invokes `Enumerable.reduce/3` to perform the reducing operation that + builds a mapped list by calling the mapping function `&(&1 * 2)` on + every element in the collection and consuming the element with an + accumulated list. + + Internally, `Enum.map/2` is implemented as follows: + + def map(enumerable, fun) do + reducer = fn x, acc -> {:cont, [fun.(x) | acc]} end + Enumerable.reduce(enumerable, {:cont, []}, reducer) |> elem(1) |> :lists.reverse() + end + + Notice the user-supplied function is wrapped into a `t:reducer/0` function. + The `t:reducer/0` function must return a tagged tuple after each step, + as described in the `t:acc/0` type. At the end, `Enumerable.reduce/3` + returns `t:result/0`. + + This protocol uses tagged tuples to exchange information between the + reducer function and the data type that implements the protocol. This + allows enumeration of resources, such as files, to be done efficiently + while also guaranteeing the resource will be closed at the end of the + enumeration. This protocol also allows suspension of the enumeration, + which is useful when interleaving between many enumerables is required + (as in zip). + + This protocol requires four functions to be implemented, `reduce/3`, + `count/1`, `member?/2`, and `slice/1`. The core of the protocol is the + `reduce/3` function. All other functions exist as optimizations paths + for data structures that can implement certain properties in better + than linear time. + """ + + @typedoc """ + The accumulator value for each step. + + It must be a tagged tuple with one of the following "tags": + + * `:cont` - the enumeration should continue + * `:halt` - the enumeration should halt immediately + * `:suspend` - the enumeration should be suspended immediately + + Depending on the accumulator value, the result returned by + `Enumerable.reduce/3` will change. Please check the `t:result/0` + type documentation for more information. + + In case a `t:reducer/0` function returns a `:suspend` accumulator, + it must be explicitly handled by the caller and never leak. + """ + @type acc :: {:cont, term} | {:halt, term} | {:suspend, term} + + @typedoc """ + The reducer function. + + Should be called with the enumerable element and the + accumulator contents. + + Returns the accumulator for the next enumeration step. + """ + @type reducer :: (term, term -> acc) + + @typedoc """ + The result of the reduce operation. + + It may be *done* when the enumeration is finished by reaching + its end, or *halted*/*suspended* when the enumeration was halted + or suspended by the `t:reducer/0` function. + + In case a `t:reducer/0` function returns the `:suspend` accumulator, the + `:suspended` tuple must be explicitly handled by the caller and + never leak. In practice, this means regular enumeration functions + just need to be concerned about `:done` and `:halted` results. + + Furthermore, a `:suspend` call must always be followed by another call, + eventually halting or continuing until the end. + """ + @type result :: + {:done, term} + | {:halted, term} + | {:suspended, term, continuation} + + @typedoc """ + A partially applied reduce function. + + The continuation is the closure returned as a result when + the enumeration is suspended. When invoked, it expects + a new accumulator and it returns the result. + + A continuation can be trivially implemented as long as the reduce + function is defined in a tail recursive fashion. If the function + is tail recursive, all the state is passed as arguments, so + the continuation is the reducing function partially applied. + """ + @type continuation :: (acc -> result) + + @typedoc """ + A slicing function that receives the initial position and the + number of elements in the slice. + + The `start` position is a number `>= 0` and guaranteed to + exist in the enumerable. The length is a number `>= 1` in a way + that `start + length <= count`, where `count` is the maximum + amount of elements in the enumerable. + + The function should return a non empty list where + the amount of elements is equal to `length`. + """ + @type slicing_fun :: (start :: non_neg_integer, length :: pos_integer -> [term()]) + + @doc """ + Reduces the enumerable into an element. + + Most of the operations in `Enum` are implemented in terms of reduce. + This function should apply the given `t:reducer/0` function to each + item in the enumerable and proceed as expected by the returned + accumulator. + + See the documentation of the types `t:result/0` and `t:acc/0` for + more information. + + ## Examples + + As an example, here is the implementation of `reduce` for lists: + + def reduce(_list, {:halt, acc}, _fun), do: {:halted, acc} + def reduce(list, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(list, &1, fun)} + def reduce([], {:cont, acc}, _fun), do: {:done, acc} + def reduce([head | tail], {:cont, acc}, fun), do: reduce(tail, fun.(head, acc), fun) + + """ + @spec reduce(t, acc, reducer) :: result + def reduce(enumerable, acc, fun) + + @doc """ + Retrieves the number of elements in the enumerable. + + It should return `{:ok, count}` if you can count the number of elements + in the enumerable. + + Otherwise it should return `{:error, __MODULE__}` and a default algorithm + built on top of `reduce/3` that runs in linear time will be used. + """ + @spec count(t) :: {:ok, non_neg_integer} | {:error, module} + def count(enumerable) + + @doc """ + Checks if an element exists within the enumerable. + + It should return `{:ok, boolean}` if you can check the membership of a + given element in the enumerable with `===/2` without traversing the whole + enumerable. + + Otherwise it should return `{:error, __MODULE__}` and a default algorithm + built on top of `reduce/3` that runs in linear time will be used. + """ + @spec member?(t, term) :: {:ok, boolean} | {:error, module} + def member?(enumerable, element) + + @doc """ + Returns a function that slices the data structure contiguously. + + It should return `{:ok, size, slicing_fun}` if the enumerable has + a known bound and can access a position in the enumerable without + traversing all previous elements. + + Otherwise it should return `{:error, __MODULE__}` and a default + algorithm built on top of `reduce/3` that runs in linear time will be + used. + + ## Differences to `count/1` + + The `size` value returned by this function is used for boundary checks, + therefore it is extremely important that this function only returns `:ok` + if retrieving the `size` of the enumerable is cheap, fast and takes constant + time. Otherwise the simplest of operations, such as `Enum.at(enumerable, 0)`, + will become too expensive. + + On the other hand, the `count/1` function in this protocol should be + implemented whenever you can count the number of elements in the collection. + """ + @spec slice(t) :: + {:ok, size :: non_neg_integer(), slicing_fun()} + | {:error, module()} + def slice(enumerable) +end diff --git a/libs/exavmlib/lib/ErlangError.ex b/libs/exavmlib/lib/ErlangError.ex new file mode 100644 index 000000000..fe6edba7e --- /dev/null +++ b/libs/exavmlib/lib/ErlangError.ex @@ -0,0 +1,133 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule ErlangError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:original] + + @impl true + def message(exception) do + "Erlang error: #{inspect(exception.original)}" + end + + @doc false + def normalize(:badarg, _stacktrace) do + %ArgumentError{} + end + + def normalize(:badarith, _stacktrace) do + %ArithmeticError{} + end + + def normalize(:system_limit, _stacktrace) do + %SystemLimitError{} + end + + def normalize(:cond_clause, _stacktrace) do + %CondClauseError{} + end + + def normalize({:badarity, {fun, args}}, _stacktrace) do + %BadArityError{function: fun, args: args} + end + + def normalize({:badfun, term}, _stacktrace) do + %BadFunctionError{term: term} + end + + def normalize({:badstruct, struct, term}, _stacktrace) do + %BadStructError{struct: struct, term: term} + end + + def normalize({:badmatch, term}, _stacktrace) do + %MatchError{term: term} + end + + def normalize({:badmap, term}, _stacktrace) do + %BadMapError{term: term} + end + + def normalize({:badbool, op, term}, _stacktrace) do + %BadBooleanError{operator: op, term: term} + end + + def normalize({:badkey, key}, stacktrace) do + term = + case stacktrace do + [{Map, :get_and_update!, [map, _, _], _} | _] -> map + [{Map, :update!, [map, _, _], _} | _] -> map + [{:maps, :update, [_, _, map], _} | _] -> map + [{:maps, :get, [_, map], _} | _] -> map + [{:erlang, :map_get, [_, map], _} | _] -> map + _ -> nil + end + + %KeyError{key: key, term: term} + end + + def normalize({:badkey, key, map}, _stacktrace) do + %KeyError{key: key, term: map} + end + + def normalize({:case_clause, term}, _stacktrace) do + %CaseClauseError{term: term} + end + + def normalize({:with_clause, term}, _stacktrace) do + %WithClauseError{term: term} + end + + def normalize({:try_clause, term}, _stacktrace) do + %TryClauseError{term: term} + end + + def normalize(:undef, stacktrace) do + {mod, fun, arity} = from_stacktrace(stacktrace) + %UndefinedFunctionError{module: mod, function: fun, arity: arity} + end + + def normalize(:function_clause, stacktrace) do + {mod, fun, arity} = from_stacktrace(stacktrace) + %FunctionClauseError{module: mod, function: fun, arity: arity} + end + + def normalize({:badarg, payload}, _stacktrace) do + %ArgumentError{message: "argument error: #{inspect(payload)}"} + end + + def normalize(other, _stacktrace) do + %ErlangError{original: other} + end + + defp from_stacktrace([{module, function, args, _} | _]) when is_list(args) do + {module, function, length(args)} + end + + defp from_stacktrace([{module, function, arity, _} | _]) do + {module, function, arity} + end + + defp from_stacktrace(_) do + {nil, nil, nil} + end +end diff --git a/libs/exavmlib/lib/Exception.ex b/libs/exavmlib/lib/Exception.ex new file mode 100644 index 000000000..d1deeec13 --- /dev/null +++ b/libs/exavmlib/lib/Exception.ex @@ -0,0 +1,557 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Exception do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + @moduledoc """ + Functions to format throw/catch/exit and exceptions. + + Note that stacktraces in Elixir are only available inside + catch and rescue by using the `__STACKTRACE__/0` variable. + + Do not rely on the particular format returned by the `format*` + functions in this module. They may be changed in future releases + in order to better suit Elixir's tool chain. In other words, + by using the functions in this module it is guaranteed you will + format exceptions as in the current Elixir version being used. + """ + + @typedoc "The exception type" + @type t :: %{ + required(:__struct__) => module, + required(:__exception__) => true, + optional(atom) => any + } + + @typedoc "The kind handled by formatting functions" + @type kind :: :error | non_error_kind + @typep non_error_kind :: :exit | :throw | {:EXIT, pid} + + @type stacktrace :: [stacktrace_entry] + @type stacktrace_entry :: + {module, atom, arity_or_args, location} + | {(... -> any), arity_or_args, location} + + @typep arity_or_args :: non_neg_integer | list + @typep location :: keyword + + @callback exception(term) :: t + @callback message(t) :: String.t() + + @doc """ + Called from `Exception.blame/3` to augment the exception struct. + + Can be used to collect additional information about the exception + or do some additional expensive computation. + """ + @callback blame(t, stacktrace) :: {t, stacktrace} + @optional_callbacks [blame: 2] + + @doc """ + Returns `true` if the given `term` is an exception. + """ + def exception?(term) + def exception?(%_{__exception__: true}), do: true + def exception?(_), do: false + + @doc """ + Gets the message for an `exception`. + """ + def message(%module{__exception__: true} = exception) do + try do + module.message(exception) + rescue + caught_exception -> + "got #{inspect(caught_exception.__struct__)} with message " <> + "#{inspect(message(caught_exception))} while retrieving Exception.message/1 " <> + "for #{inspect(exception)}" + else + result when is_binary(result) -> + result + + result -> + "got #{inspect(result)} " <> + "while retrieving Exception.message/1 for #{inspect(exception)} " <> + "(expected a string)" + end + end + + @doc """ + Normalizes an exception, converting Erlang exceptions + to Elixir exceptions. + + It takes the `kind` spilled by `catch` as an argument and + normalizes only `:error`, returning the untouched payload + for others. + + The third argument is the stacktrace which is used to enrich + a normalized error with more information. It is only used when + the kind is an error. + """ + @spec normalize(:error, any, stacktrace) :: t + @spec normalize(non_error_kind, payload, stacktrace) :: payload when payload: var + def normalize(kind, payload, stacktrace \\ []) + def normalize(:error, %_{__exception__: true} = payload, _stacktrace), do: payload + def normalize(:error, payload, stacktrace), do: ErlangError.normalize(payload, stacktrace) + def normalize(_kind, payload, _stacktrace), do: payload + + @doc """ + Normalizes and formats any throw/error/exit. + + The message is formatted and displayed in the same + format as used by Elixir's CLI. + + The third argument is the stacktrace which is used to enrich + a normalized error with more information. It is only used when + the kind is an error. + """ + @spec format_banner(kind, any, stacktrace) :: String.t() + def format_banner(kind, exception, stacktrace \\ []) + + def format_banner(:error, exception, stacktrace) do + exception = normalize(:error, exception, stacktrace) + "** (" <> inspect(exception.__struct__) <> ") " <> message(exception) + end + + def format_banner(:throw, reason, _stacktrace) do + "** (throw) " <> inspect(reason) + end + + def format_banner(:exit, reason, _stacktrace) do + "** (exit) " <> format_exit(reason, <<"\n ">>) + end + + def format_banner({:EXIT, pid}, reason, _stacktrace) do + "** (EXIT from #{inspect(pid)}) " <> format_exit(reason, <<"\n ">>) + end + + @doc """ + Normalizes and formats throw/errors/exits and stacktraces. + + It relies on `format_banner/3` and `format_stacktrace/1` + to generate the final format. + + If `kind` is `{:EXIT, pid}`, it does not generate a stacktrace, + as such exits are retrieved as messages without stacktraces. + """ + @spec format(kind, any, stacktrace) :: String.t() + def format(kind, payload, stacktrace \\ []) + + def format({:EXIT, _} = kind, any, _) do + format_banner(kind, any) + end + + def format(kind, payload, stacktrace) do + message = format_banner(kind, payload, stacktrace) + + case stacktrace do + [] -> message + _ -> message <> "\n" <> format_stacktrace(stacktrace) + end + end + + @doc """ + Attaches information to exceptions for extra debugging. + + This operation is potentially expensive, as it reads data + from the filesystem, parses beam files, evaluates code and + so on. + + If the exception module implements the optional `c:blame/2` + callback, it will be invoked to perform the computation. + """ + @doc since: "1.5.0" + @spec blame(:error, any, stacktrace) :: {t, stacktrace} + @spec blame(non_error_kind, payload, stacktrace) :: {payload, stacktrace} when payload: var + def blame(kind, error, stacktrace) + + def blame(:error, error, stacktrace) do + %module{} = struct = normalize(:error, error, stacktrace) + + # TODO: Code.ensure_loaded?(module) is not supported right now + # original code: Code.ensure_loaded?(module) and function_exported?(module, :blame, 2) + if function_exported?(module, :blame, 2) do + module.blame(struct, stacktrace) + else + {struct, stacktrace} + end + end + + def blame(_kind, reason, stacktrace) do + {reason, stacktrace} + end + + @doc """ + Formats an exit. It returns a string. + + Often there are errors/exceptions inside exits. Exits are often + wrapped by the caller and provide stacktraces too. This function + formats exits in a way to nicely show the exit reason, caller + and stacktrace. + """ + @spec format_exit(any) :: String.t() + def format_exit(reason) do + format_exit(reason, <<"\n ">>) + end + + # 2-Tuple could be caused by an error if the second element is a stacktrace. + defp format_exit({exception, maybe_stacktrace} = reason, joiner) + when is_list(maybe_stacktrace) and maybe_stacktrace !== [] do + try do + Enum.map(maybe_stacktrace, &format_stacktrace_entry/1) + catch + :error, _ -> + # Not a stacktrace, was an exit. + format_exit_reason(reason) + else + formatted_stacktrace -> + # Assume a non-empty list formattable as stacktrace is a + # stacktrace, so exit was caused by an error. + message = + "an exception was raised:" <> + joiner <> format_banner(:error, exception, maybe_stacktrace) + + Enum.join([message | formatted_stacktrace], joiner <> <<" ">>) + end + end + + # :supervisor.start_link returns this error reason when it fails to init + # because a child's start_link raises. + defp format_exit({:shutdown, {:failed_to_start_child, child, {:EXIT, reason}}}, joiner) do + format_start_child(child, reason, joiner) + end + + # :supervisor.start_link returns this error reason when it fails to init + # because a child's start_link returns {:error, reason}. + defp format_exit({:shutdown, {:failed_to_start_child, child, reason}}, joiner) do + format_start_child(child, reason, joiner) + end + + # 2-Tuple could be an exit caused by mfa if second element is mfa, args + # must be a list of arguments - max length 255 due to max arity. + defp format_exit({reason2, {mod, fun, args}} = reason, joiner) + when length(args) < 256 do + try do + format_mfa(mod, fun, args) + catch + :error, _ -> + # Not an mfa, was an exit. + format_exit_reason(reason) + else + mfa -> + # Assume tuple formattable as an mfa is an mfa, + # so exit was caused by failed mfa. + "exited in: " <> + mfa <> joiner <> "** (EXIT) " <> format_exit(reason2, joiner <> <<" ">>) + end + end + + defp format_exit(reason, _joiner) do + format_exit_reason(reason) + end + + defp format_exit_reason(:normal), do: "normal" + defp format_exit_reason(:shutdown), do: "shutdown" + + defp format_exit_reason({:shutdown, reason}) do + "shutdown: #{inspect(reason)}" + end + + defp format_exit_reason(:calling_self), do: "process attempted to call itself" + defp format_exit_reason(:timeout), do: "time out" + defp format_exit_reason(:killed), do: "killed" + defp format_exit_reason(:noconnection), do: "no connection" + + defp format_exit_reason(:noproc) do + "no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started" + end + + defp format_exit_reason({:nodedown, node_name}) when is_atom(node_name) do + "no connection to #{node_name}" + end + + # :gen_server exit reasons + + defp format_exit_reason({:already_started, pid}) do + "already started: " <> inspect(pid) + end + + defp format_exit_reason({:bad_return_value, value}) do + "bad return value: " <> inspect(value) + end + + defp format_exit_reason({:bad_call, request}) do + "bad call: " <> inspect(request) + end + + defp format_exit_reason({:bad_cast, request}) do + "bad cast: " <> inspect(request) + end + + # :supervisor.start_link error reasons + + # If value is a list will be formatted by mfa exit in format_exit/1 + defp format_exit_reason({:bad_return, {mod, :init, value}}) + when is_atom(mod) do + format_mfa(mod, :init, 1) <> " returned a bad value: " <> inspect(value) + end + + defp format_exit_reason({:bad_start_spec, start_spec}) do + "bad child specification, invalid children: " <> inspect(start_spec) + end + + defp format_exit_reason({:start_spec, start_spec}) do + "bad child specification, " <> format_sup_spec(start_spec) + end + + defp format_exit_reason({:supervisor_data, data}) do + "bad supervisor configuration, " <> format_sup_data(data) + end + + defp format_exit_reason(reason), do: inspect(reason) + + defp format_start_child(child, reason, joiner) do + "shutdown: failed to start child: " <> + inspect(child) <> joiner <> "** (EXIT) " <> format_exit(reason, joiner <> <<" ">>) + end + + defp format_sup_data({:invalid_type, type}) do + "invalid type: " <> inspect(type) + end + + defp format_sup_data({:invalid_strategy, strategy}) do + "invalid strategy: " <> inspect(strategy) + end + + defp format_sup_data({:invalid_intensity, intensity}) do + "invalid max_restarts (intensity): " <> inspect(intensity) + end + + defp format_sup_data({:invalid_period, period}) do + "invalid max_seconds (period): " <> inspect(period) + end + + defp format_sup_data({:invalid_max_children, max_children}) do + "invalid max_children: " <> inspect(max_children) + end + + defp format_sup_data({:invalid_extra_arguments, extra}) do + "invalid extra_arguments: " <> inspect(extra) + end + + defp format_sup_data(other), do: "got: #{inspect(other)}" + + defp format_sup_spec({:duplicate_child_name, id}) do + """ + more than one child specification has the id: #{inspect(id)}. + If using maps as child specifications, make sure the :id keys are unique. + If using a module or {module, arg} as child, use Supervisor.child_spec/2 to change the :id, for example: + + children = [ + Supervisor.child_spec({MyWorker, arg}, id: :my_worker_1), + Supervisor.child_spec({MyWorker, arg}, id: :my_worker_2) + ] + """ + end + + defp format_sup_spec({:invalid_child_spec, child_spec}) do + "invalid child specification: #{inspect(child_spec)}" + end + + defp format_sup_spec({:invalid_child_type, type}) do + "invalid child type: #{inspect(type)}. Must be :worker or :supervisor." + end + + defp format_sup_spec({:invalid_mfa, mfa}) do + "invalid mfa: #{inspect(mfa)}" + end + + defp format_sup_spec({:invalid_restart_type, restart}) do + "invalid restart type: #{inspect(restart)}. Must be :permanent, :transient or :temporary." + end + + defp format_sup_spec({:invalid_shutdown, shutdown}) do + "invalid shutdown: #{inspect(shutdown)}. Must be an integer >= 0, :infinity or :brutal_kill." + end + + defp format_sup_spec({:invalid_module, mod}) do + "invalid module: #{inspect(mod)}. Must be an atom." + end + + defp format_sup_spec({:invalid_modules, modules}) do + "invalid modules: #{inspect(modules)}. Must be a list of atoms or :dynamic." + end + + defp format_sup_spec(other), do: "got: #{inspect(other)}" + + @doc """ + Receives a stacktrace entry and formats it into a string. + """ + @spec format_stacktrace_entry(stacktrace_entry) :: String.t() + def format_stacktrace_entry(entry) + + # From Macro.Env.stacktrace + def format_stacktrace_entry({module, :__MODULE__, 0, location}) do + format_location(location) <> inspect(module) <> " (module)" + end + + # From :elixir_compiler_* + def format_stacktrace_entry({_module, :__MODULE__, 1, location}) do + format_location(location) <> "(module)" + end + + # From :elixir_compiler_* + def format_stacktrace_entry({_module, :__FILE__, 1, location}) do + format_location(location) <> "(file)" + end + + def format_stacktrace_entry({module, fun, arity, location}) do + format_application(module) <> format_location(location) <> format_mfa(module, fun, arity) + end + + def format_stacktrace_entry({fun, arity, location}) do + format_location(location) <> format_fa(fun, arity) + end + + defp format_application(module) do + # We cannot use Application due to bootstrap issues + case :application.get_application(module) do + {:ok, app} -> "(" <> Atom.to_string(app) <> ") " + :undefined -> "" + end + end + + @doc """ + Formats the stacktrace. + + A stacktrace must be given as an argument. If not, the stacktrace + is retrieved from `Process.info/2`. + """ + def format_stacktrace(trace \\ nil) do + trace = + if trace do + trace + else + case Process.info(self(), :current_stacktrace) do + {:current_stacktrace, t} -> Enum.drop(t, 3) + end + end + + case trace do + [] -> "\n" + _ -> " " <> Enum.map_join(trace, "\n ", &format_stacktrace_entry(&1)) <> "\n" + end + end + + @doc """ + Receives an anonymous function and arity and formats it as + shown in stacktraces. The arity may also be a list of arguments. + + ## Examples + + Exception.format_fa(fn -> nil end, 1) + #=> "#Function<...>/1" + + """ + def format_fa(fun, arity) when is_function(fun) do + "#{inspect(fun)}#{format_arity(arity)}" + end + + @doc """ + Receives a module, fun and arity and formats it + as shown in stacktraces. The arity may also be a list + of arguments. + + ## Examples + + iex> Exception.format_mfa(Foo, :bar, 1) + "Foo.bar/1" + + iex> Exception.format_mfa(Foo, :bar, []) + "Foo.bar()" + + iex> Exception.format_mfa(nil, :bar, []) + "nil.bar()" + + Anonymous functions are reported as -func/arity-anonfn-count-, + where func is the name of the enclosing function. Convert to + "anonymous fn in func/arity" + """ + def format_mfa(module, fun, arity) when is_atom(module) and is_atom(fun) do + # Original code: + # case Code.Identifier.extract_anonymous_fun_parent(fun) do + # {outer_name, outer_arity} -> + # "anonymous fn#{format_arity(arity)} in " <> + # "#{Code.Identifier.inspect_as_atom(module)}." <> + # "#{Code.Identifier.inspect_as_function(outer_name)}/#{outer_arity}" + # + # :error -> + # "#{Code.Identifier.inspect_as_atom(module)}." <> + # "#{Code.Identifier.inspect_as_function(fun)}#{format_arity(arity)}" + # end + # + # Here we use a super simplified version: + "#{inspect(module)}.#{inspect(fun)}#{format_arity(arity)}" + end + + defp format_arity(arity) when is_list(arity) do + inspected = for x <- arity, do: inspect(x) + "(#{Enum.join(inspected, ", ")})" + end + + defp format_arity(arity) when is_integer(arity) do + "/" <> Integer.to_string(arity) + end + + @doc """ + Formats the given `file` and `line` as shown in stacktraces. + If any of the values are `nil`, they are omitted. + + ## Examples + + iex> Exception.format_file_line("foo", 1) + "foo:1:" + + iex> Exception.format_file_line("foo", nil) + "foo:" + + iex> Exception.format_file_line(nil, nil) + "" + + """ + def format_file_line(file, line, suffix \\ "") do + if file do + if line && line != 0 do + "#{file}:#{line}:#{suffix}" + else + "#{file}:#{suffix}" + end + else + "" + end + end + + defp format_location(opts) when is_list(opts) do + format_file_line(Keyword.get(opts, :file), Keyword.get(opts, :line), " ") + end +end diff --git a/libs/exavmlib/lib/FunctionClauseError.ex b/libs/exavmlib/lib/FunctionClauseError.ex new file mode 100644 index 000000000..e37d4293c --- /dev/null +++ b/libs/exavmlib/lib/FunctionClauseError.ex @@ -0,0 +1,39 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule FunctionClauseError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:module, :function, :arity, :kind, :args, :clauses] + + @impl true + def message(exception) do + case exception do + %{function: nil} -> + "no function clause matches" + + %{module: module, function: function, arity: arity} -> + formatted = Exception.format_mfa(module, function, arity) + "no function clause matching in #{formatted}" + end + end +end diff --git a/libs/exavmlib/lib/IO.ex b/libs/exavmlib/lib/IO.ex index 6a54c1dfd..494a14a5a 100644 --- a/libs/exavmlib/lib/IO.ex +++ b/libs/exavmlib/lib/IO.ex @@ -2,6 +2,8 @@ # This file is part of AtomVM. # # Copyright 2024 Davide Bettio +# Copyright 2012-2017 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.7.4/lib/elixir/lib/io.ex # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,6 +24,39 @@ defmodule IO do # This avoids crashing the compiler at build time @compile {:autoload, false} + # Taken from Elixir io.ex + @doc """ + Converts iodata (a list of integers representing bytes, lists + and binaries) into a binary. + The operation is Unicode unsafe. + + Notice that this function treats lists of integers as raw bytes + and does not perform any kind of encoding conversion. If you want + to convert from a charlist to a string (UTF-8 encoded), please + use `chardata_to_string/1` instead. + + If this function receives a binary, the same binary is returned. + + Inlined by the compiler. + + ## Examples + + iex> bin1 = <<1, 2, 3>> + iex> bin2 = <<4, 5>> + iex> bin3 = <<6>> + iex> IO.iodata_to_binary([bin1, 1, [2, 3, bin2], 4 | bin3]) + <<1, 2, 3, 1, 2, 3, 4, 5, 4, 6>> + + iex> bin = <<1, 2, 3>> + iex> IO.iodata_to_binary(bin) + <<1, 2, 3>> + + """ + @spec iodata_to_binary(iodata) :: binary + def iodata_to_binary(item) do + :erlang.iolist_to_binary(item) + end + def puts(string) do :io.put_chars([to_chardata(string), ?\n]) end diff --git a/libs/exavmlib/lib/Kernel.ex b/libs/exavmlib/lib/Kernel.ex index 3950f1bf8..4d5088878 100644 --- a/libs/exavmlib/lib/Kernel.ex +++ b/libs/exavmlib/lib/Kernel.ex @@ -2,6 +2,8 @@ # This file is part of AtomVM. # # Copyright 2020 Davide Bettio +# Copyright 2012-2022 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/main/lib/elixir/lib/kernel.ex # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -121,4 +123,174 @@ defmodule Kernel do # handle spaces and special characters :erlang.atom_to_binary(atom, :latin1) end + + @doc """ + Returns the biggest of the two given terms according to + Erlang's term ordering. + + If the terms compare equal, the first one is returned. + + Inlined by the compiler. + + ## Examples + + iex> max(1, 2) + 2 + iex> max(:a, :b) + :b + + Using Erlang's term ordering means that comparisons are + structural and not semantic. For example, when comparing dates: + + iex> max(~D[2017-03-31], ~D[2017-04-01]) + ~D[2017-03-31] + + In the example above, `max/1` returned March 31st instead of April 1st + because the structural comparison compares the day before the year. In + such cases it is common for modules to provide functions such as + `Date.compare/2` that perform semantic comparison. + """ + @spec max(first, second) :: first | second when first: term, second: term + def max(first, second) do + :erlang.max(first, second) + end + + @doc """ + Returns the smallest of the two given terms according to + Erlang's term ordering. + + If the terms compare equal, the first one is returned. + + Inlined by the compiler. + + ## Examples + + iex> min(1, 2) + 1 + iex> min("foo", "bar") + "bar" + + Using Erlang's term ordering means that comparisons are + structural and not semantic. For example, when comparing dates: + + iex> min(~D[2017-03-31], ~D[2017-04-01]) + ~D[2017-04-01] + + In the example above, `min/1` returned April 1st instead of March 31st + because the structural comparison compares the day before the year. In + such cases it is common for modules to provide functions such as + `Date.compare/2` that perform semantic comparison. + """ + @spec min(first, second) :: first | second when first: term, second: term + def min(first, second) do + :erlang.min(first, second) + end + + # Taken from Elixir kernel.ex + @doc """ + Creates and updates structs. + + The `struct` argument may be an atom (which defines `defstruct`) + or a `struct` itself. The second argument is any `Enumerable` that + emits two-element tuples (key-value pairs) during enumeration. + + Keys in the `Enumerable` that don't exist in the struct are automatically + discarded. Note that keys must be atoms, as only atoms are allowed when + defining a struct. + + This function is useful for dynamically creating and updating structs, as + well as for converting maps to structs; in the latter case, just inserting + the appropriate `:__struct__` field into the map may not be enough and + `struct/2` should be used instead. + + ## Examples + + defmodule User do + defstruct name: "john" + end + + struct(User) + #=> %User{name: "john"} + + opts = [name: "meg"] + user = struct(User, opts) + #=> %User{name: "meg"} + + struct(user, unknown: "value") + #=> %User{name: "meg"} + + struct(User, %{name: "meg"}) + #=> %User{name: "meg"} + + # String keys are ignored + struct(User, %{"name" => "meg"}) + #=> %User{name: "john"} + + """ + @spec struct(module | struct, Enum.t()) :: struct + def struct(struct, fields \\ []) do + struct(struct, fields, fn + {:__struct__, _val}, acc -> + acc + + {key, val}, acc -> + case acc do + %{^key => _} -> %{acc | key => val} + _ -> acc + end + end) + end + + # Taken from Elixir kernel.ex + @doc """ + Similar to `struct/2` but checks for key validity. + + The function `struct!/2` emulates the compile time behaviour + of structs. This means that: + + * when building a struct, as in `struct!(SomeStruct, key: :value)`, + it is equivalent to `%SomeStruct{key: :value}` and therefore this + function will check if every given key-value belongs to the struct. + If the struct is enforcing any key via `@enforce_keys`, those will + be enforced as well; + + * when updating a struct, as in `struct!(%SomeStruct{}, key: :value)`, + it is equivalent to `%SomeStruct{struct | key: :value}` and therefore this + function will check if every given key-value belongs to the struct. + However, updating structs does not enforce keys, as keys are enforced + only when building; + + """ + @spec struct!(module | struct, Enum.t()) :: struct | no_return + def struct!(struct, fields \\ []) + + def struct!(struct, fields) when is_atom(struct) do + struct.__struct__(fields) + end + + def struct!(struct, fields) when is_map(struct) do + struct(struct, fields, fn + {:__struct__, _}, acc -> + acc + + {key, val}, acc -> + Map.replace!(acc, key, val) + end) + end + + defp struct(struct, [], _fun) when is_atom(struct) do + struct.__struct__() + end + + defp struct(struct, fields, fun) when is_atom(struct) do + struct(struct.__struct__(), fields, fun) + end + + defp struct(%_{} = struct, [], _fun) do + struct + end + + defp struct(%_{} = struct, fields, fun) do + Enum.reduce(fields, struct, fun) + end end diff --git a/libs/exavmlib/lib/KeyError.ex b/libs/exavmlib/lib/KeyError.ex new file mode 100644 index 000000000..d19f52319 --- /dev/null +++ b/libs/exavmlib/lib/KeyError.ex @@ -0,0 +1,53 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule KeyError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:key, :term, :message] + + @impl true + def message(exception = %{message: nil}), do: message(exception.key, exception.term) + def message(%{message: message}), do: message + + def message(key, term) do + message = "key #{inspect(key)} not found" + + if term != nil do + message <> " in: #{inspect(term)}" + else + message + end + end + + @impl true + def blame(exception = %{term: nil}, stacktrace) do + message = message(exception.key, exception.term) + {%{exception | message: message}, stacktrace} + end + + def blame(exception, stacktrace) do + %{term: term, key: key} = exception + message = message(key, term) + {%{exception | message: message}, stacktrace} + end +end diff --git a/libs/exavmlib/lib/Map.ex b/libs/exavmlib/lib/Map.ex index f5bfcf322..f8fbb8b5a 100644 --- a/libs/exavmlib/lib/Map.ex +++ b/libs/exavmlib/lib/Map.ex @@ -23,6 +23,9 @@ defmodule Map do # This avoids crashing the compiler at build time @compile {:autoload, false} + @type key :: any + @type value :: any + def new(list) when is_list(list), do: :maps.from_list(list) def new(%{} = map), do: map @@ -66,4 +69,53 @@ defmodule Map do def equal?(%{} = map1, %{} = map2), do: map1 === map2 def equal?(%{} = map1, map2), do: :erlang.error({:badmap, map2}, [map1, map2]) def equal?(term, other), do: :erlang.error({:badmap, term}, [term, other]) + + @doc """ + Puts a value under `key` only if the `key` already exists in `map`. + + ## Examples + + iex> Map.replace(%{a: 1, b: 2}, :a, 3) + %{a: 3, b: 2} + + iex> Map.replace(%{a: 1}, :b, 2) + %{a: 1} + + """ + @doc since: "1.11.0" + @spec replace(map, key, value) :: map + def replace(map, key, value) do + case map do + %{^key => _value} -> + %{map | key => value} + + %{} -> + map + + other -> + :erlang.error({:badmap, other}) + end + end + + @doc """ + Puts a value under `key` only if the `key` already exists in `map`. + + If `key` is not present in `map`, a `KeyError` exception is raised. + + Inlined by the compiler. + + ## Examples + + iex> Map.replace!(%{a: 1, b: 2}, :a, 3) + %{a: 3, b: 2} + + iex> Map.replace!(%{a: 1}, :b, 2) + ** (KeyError) key :b not found in: %{a: 1} + + """ + @doc since: "1.5.0" + @spec replace!(map, key, value) :: map + def replace!(map, key, value) do + :maps.update(key, value, map) + end end diff --git a/libs/exavmlib/lib/MapSet.ex b/libs/exavmlib/lib/MapSet.ex new file mode 100644 index 000000000..adb20ea62 --- /dev/null +++ b/libs/exavmlib/lib/MapSet.ex @@ -0,0 +1,434 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2024 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.17.2/lib/elixir/lib/map_set.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule MapSet do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + @moduledoc """ + Functions that work on sets. + + A set is a data structure that can contain unique elements of any kind, + without any particular order. `MapSet` is the "go to" set data structure in Elixir. + + A set can be constructed using `MapSet.new/0`: + + iex> MapSet.new() + MapSet.new([]) + + Elements in a set don't have to be of the same type and they can be + populated from an [enumerable](`t:Enumerable.t/0`) using `MapSet.new/1`: + + iex> MapSet.new([1, :two, {"three"}]) + MapSet.new([1, :two, {"three"}]) + + Elements can be inserted using `MapSet.put/2`: + + iex> MapSet.new([2]) |> MapSet.put(4) |> MapSet.put(0) + MapSet.new([0, 2, 4]) + + By definition, sets can't contain duplicate elements: when + inserting an element in a set where it's already present, the insertion is + simply a no-op. + + iex> map_set = MapSet.new() + iex> MapSet.put(map_set, "foo") + MapSet.new(["foo"]) + iex> map_set |> MapSet.put("foo") |> MapSet.put("foo") + MapSet.new(["foo"]) + + A `MapSet` is represented internally using the `%MapSet{}` struct. This struct + can be used whenever there's a need to pattern match on something being a `MapSet`: + + iex> match?(%MapSet{}, MapSet.new()) + true + + Note that, however, the struct fields are private and must not be accessed + directly; use the functions in this module to perform operations on sets. + + `MapSet`s can also be constructed starting from other collection-type data + structures: for example, see `MapSet.new/1` or `Enum.into/2`. + + `MapSet` is built on top of Erlang's + [`:sets`](https://www.erlang.org/doc/man/sets.html) (version 2). This means + that they share many properties, including logarithmic time complexity. Erlang + `:sets` (version 2) are implemented on top of maps, so see the documentation + for `Map` for more information on its execution time complexity. + """ + + @type value :: term + + @opaque internal(value) :: :sets.set(value) + @type t(value) :: %__MODULE__{map: internal(value)} + @type t :: t(term) + + # The key name is :map because the MapSet implementation used to be based on top of maps before + # Elixir 1.15 (and Erlang/OTP 24, which introduced :sets version 2). :sets v2's internal + # representation is, anyways, exactly the same as MapSet's previous implementation. We cannot + # change the :map key name here because we'd break backwards compatibility with code compiled + # with Elixir 1.14 and earlier and executed on Elixir 1.15+. + # AtomVM change here: do not use :sets.new(version: 2), otherwise it may fail at compile time + defstruct map: %{} + + @doc """ + Returns a new set. + + ## Examples + + iex> MapSet.new() + MapSet.new([]) + + """ + @spec new :: t + def new(), do: %MapSet{} + + @doc """ + Creates a set from an enumerable. + + ## Examples + + iex> MapSet.new([:b, :a, 3]) + MapSet.new([3, :a, :b]) + iex> MapSet.new([3, 3, 3, 2, 2, 1]) + MapSet.new([1, 2, 3]) + + """ + @spec new(Enumerable.t()) :: t + def new(enumerable) + + def new(%__MODULE__{} = map_set), do: map_set + + def new(enumerable) do + set = + enumerable + |> Enum.to_list() + |> :sets.from_list(version: 2) + + %MapSet{map: set} + end + + @doc """ + Creates a set from an enumerable via the transformation function. + + ## Examples + + iex> MapSet.new([1, 2, 1], fn x -> 2 * x end) + MapSet.new([2, 4]) + + """ + @spec new(Enumerable.t(), (term -> val)) :: t(val) when val: value + def new(enumerable, transform) when is_function(transform, 1) do + set = + enumerable + |> Enum.map(transform) + |> :sets.from_list(version: 2) + + %MapSet{map: set} + end + + @doc """ + Deletes `value` from `map_set`. + + Returns a new set which is a copy of `map_set` but without `value`. + + ## Examples + + iex> map_set = MapSet.new([1, 2, 3]) + iex> MapSet.delete(map_set, 4) + MapSet.new([1, 2, 3]) + iex> MapSet.delete(map_set, 2) + MapSet.new([1, 3]) + + """ + @spec delete(t(val1), val2) :: t(val1) when val1: value, val2: value + def delete(%MapSet{map: set} = map_set, value) do + %{map_set | map: :sets.del_element(value, set)} + end + + @doc """ + Returns a set that is `map_set1` without the members of `map_set2`. + + ## Examples + + iex> MapSet.difference(MapSet.new([1, 2]), MapSet.new([2, 3, 4])) + MapSet.new([1]) + + """ + @spec difference(t(val1), t(val2)) :: t(val1) when val1: value, val2: value + def difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do + %{map_set1 | map: :sets.subtract(set1, set2)} + end + + @doc """ + Returns a set with elements that are present in only one but not both sets. + + ## Examples + + iex> MapSet.symmetric_difference(MapSet.new([1, 2, 3]), MapSet.new([2, 3, 4])) + MapSet.new([1, 4]) + + """ + @doc since: "1.14.0" + @spec symmetric_difference(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value + def symmetric_difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do + {small, large} = if :sets.size(set1) <= :sets.size(set2), do: {set1, set2}, else: {set2, set1} + + disjointer_fun = fn elem, {small, acc} -> + if :sets.is_element(elem, small) do + {:sets.del_element(elem, small), acc} + else + {small, [elem | acc]} + end + end + + {new_small, list} = :sets.fold(disjointer_fun, {small, []}, large) + %{map_set1 | map: :sets.union(new_small, :sets.from_list(list, version: 2))} + end + + @doc """ + Checks if `map_set1` and `map_set2` have no members in common. + + ## Examples + + iex> MapSet.disjoint?(MapSet.new([1, 2]), MapSet.new([3, 4])) + true + iex> MapSet.disjoint?(MapSet.new([1, 2]), MapSet.new([2, 3])) + false + + """ + @spec disjoint?(t, t) :: boolean + def disjoint?(%MapSet{map: set1}, %MapSet{map: set2}) do + :sets.is_disjoint(set1, set2) + end + + @doc """ + Checks if two sets are equal. + + The comparison between elements is done using `===/2`, + which a set with `1` is not equivalent to a set with + `1.0`. + + ## Examples + + iex> MapSet.equal?(MapSet.new([1, 2]), MapSet.new([2, 1, 1])) + true + iex> MapSet.equal?(MapSet.new([1, 2]), MapSet.new([3, 4])) + false + iex> MapSet.equal?(MapSet.new([1]), MapSet.new([1.0])) + false + + """ + @spec equal?(t, t) :: boolean + def equal?(%MapSet{map: set1}, %MapSet{map: set2}) do + set1 === set2 + end + + @doc """ + Returns a set containing only members that `map_set1` and `map_set2` have in common. + + ## Examples + + iex> MapSet.intersection(MapSet.new([1, 2]), MapSet.new([2, 3, 4])) + MapSet.new([2]) + + iex> MapSet.intersection(MapSet.new([1, 2]), MapSet.new([3, 4])) + MapSet.new([]) + + """ + @spec intersection(t(val), t(val)) :: t(val) when val: value + def intersection(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do + %{map_set1 | map: :sets.intersection(set1, set2)} + end + + @doc """ + Checks if `map_set` contains `value`. + + ## Examples + + iex> MapSet.member?(MapSet.new([1, 2, 3]), 2) + true + iex> MapSet.member?(MapSet.new([1, 2, 3]), 4) + false + + """ + @spec member?(t, value) :: boolean + def member?(%MapSet{map: set}, value) do + :sets.is_element(value, set) + end + + @doc """ + Inserts `value` into `map_set` if `map_set` doesn't already contain it. + + ## Examples + + iex> MapSet.put(MapSet.new([1, 2, 3]), 3) + MapSet.new([1, 2, 3]) + iex> MapSet.put(MapSet.new([1, 2, 3]), 4) + MapSet.new([1, 2, 3, 4]) + + """ + @spec put(t(val), new_val) :: t(val | new_val) when val: value, new_val: value + def put(%MapSet{map: set} = map_set, value) do + %{map_set | map: :sets.add_element(value, set)} + end + + @doc """ + Returns the number of elements in `map_set`. + + ## Examples + + iex> MapSet.size(MapSet.new([1, 2, 3])) + 3 + + """ + @spec size(t) :: non_neg_integer + def size(%MapSet{map: set}) do + :sets.size(set) + end + + @doc """ + Checks if `map_set1`'s members are all contained in `map_set2`. + + This function checks if `map_set1` is a subset of `map_set2`. + + ## Examples + + iex> MapSet.subset?(MapSet.new([1, 2]), MapSet.new([1, 2, 3])) + true + iex> MapSet.subset?(MapSet.new([1, 2, 3]), MapSet.new([1, 2])) + false + + """ + @spec subset?(t, t) :: boolean + def subset?(%MapSet{map: set1}, %MapSet{map: set2}) do + :sets.is_subset(set1, set2) + end + + @doc """ + Converts `map_set` to a list. + + ## Examples + + iex> MapSet.to_list(MapSet.new([1, 2, 3])) + [1, 2, 3] + + """ + @spec to_list(t(val)) :: [val] when val: value + def to_list(%MapSet{map: set}) do + :sets.to_list(set) + end + + @doc """ + Returns a set containing all members of `map_set1` and `map_set2`. + + ## Examples + + iex> MapSet.union(MapSet.new([1, 2]), MapSet.new([2, 3, 4])) + MapSet.new([1, 2, 3, 4]) + + """ + @spec union(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value + def union(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do + %{map_set1 | map: :sets.union(set1, set2)} + end + + @doc """ + Filters the set by returning only the elements from `map_set` for which invoking + `fun` returns a truthy value. + + Also see `reject/2` which discards all elements where the function returns + a truthy value. + + > #### Performance considerations {: .tip} + > + > If you find yourself doing multiple calls to `MapSet.filter/2` + > and `MapSet.reject/2` in a pipeline, it is likely more efficient + > to use `Enum.map/2` and `Enum.filter/2` instead and convert to + > a map at the end using `MapSet.new/1`. + + ## Examples + + iex> MapSet.filter(MapSet.new(1..5), fn x -> x > 3 end) + MapSet.new([4, 5]) + + iex> MapSet.filter(MapSet.new(["a", :b, "c"]), &is_atom/1) + MapSet.new([:b]) + + """ + @doc since: "1.14.0" + @spec filter(t(a), (a -> as_boolean(term))) :: t(a) when a: value + def filter(%MapSet{map: set} = map_set, fun) when is_function(fun) do + pred = fn element -> !!fun.(element) end + %{map_set | map: :sets.filter(pred, set)} + end + + @doc """ + Returns a set by excluding the elements from `map_set` for which invoking `fun` + returns a truthy value. + + See also `filter/2`. + + ## Examples + + iex> MapSet.reject(MapSet.new(1..5), fn x -> rem(x, 2) != 0 end) + MapSet.new([2, 4]) + + iex> MapSet.reject(MapSet.new(["a", :b, "c"]), &is_atom/1) + MapSet.new(["a", "c"]) + + """ + @doc since: "1.14.0" + @spec reject(t(a), (a -> as_boolean(term))) :: t(a) when a: value + def reject(%MapSet{map: set} = map_set, fun) when is_function(fun) do + pred = fn element -> !fun.(element) end + %{map_set | map: :sets.filter(pred, set)} + end + + @doc """ + Splits the `map_set` into two `MapSet`s according to the given function `fun`. + + `fun` receives each element in the `map_set` as its only argument. Returns + a tuple with the first `MapSet` containing all the elements in `map_set` for which + applying `fun` returned a truthy value, and a second `MapSet` with all the elements + for which applying `fun` returned a falsy value (`false` or `nil`). + + ## Examples + + iex> {while_true, while_false} = MapSet.split_with(MapSet.new([1, 2, 3, 4]), fn v -> rem(v, 2) == 0 end) + iex> while_true + MapSet.new([2, 4]) + iex> while_false + MapSet.new([1, 3]) + + iex> {while_true, while_false} = MapSet.split_with(MapSet.new(), fn {_k, v} -> v > 50 end) + iex> while_true + MapSet.new([]) + iex> while_false + MapSet.new([]) + + """ + @doc since: "1.15.0" + @spec split_with(MapSet.t(), (any() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()} + def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do + {while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end) + {%MapSet{map: while_true}, %MapSet{map: while_false}} + end +end diff --git a/libs/exavmlib/lib/MatchError.ex b/libs/exavmlib/lib/MatchError.ex new file mode 100644 index 000000000..b99b564f0 --- /dev/null +++ b/libs/exavmlib/lib/MatchError.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule MatchError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:term] + + @impl true + def message(exception) do + "no match of right hand side value: #{inspect(exception.term)}" + end +end diff --git a/libs/exavmlib/lib/Protocol.UndefinedError.ex b/libs/exavmlib/lib/Protocol.UndefinedError.ex new file mode 100644 index 000000000..9f71d2e14 --- /dev/null +++ b/libs/exavmlib/lib/Protocol.UndefinedError.ex @@ -0,0 +1,46 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Protocol.UndefinedError do + defexception [:protocol, :value, description: ""] + + @impl true + def message(%{protocol: protocol, value: value, description: description}) do + "protocol #{inspect(protocol)} not implemented for #{inspect(value)}" <> + maybe_description(description) <> maybe_available(protocol) + end + + defp maybe_description(""), do: "" + defp maybe_description(description), do: ", " <> description + + defp maybe_available(protocol) do + case protocol.__protocol__(:impls) do + {:consolidated, []} -> + ". There are no implementations for this protocol." + + {:consolidated, types} -> + ". This protocol is implemented for: #{Enum.map_join(types, ", ", &inspect/1)}" + + :not_consolidated -> + "" + end + end +end diff --git a/libs/exavmlib/lib/Range.ex b/libs/exavmlib/lib/Range.ex new file mode 100644 index 000000000..e8f72c08a --- /dev/null +++ b/libs/exavmlib/lib/Range.ex @@ -0,0 +1,118 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2020 Elixir Contributors +# https://github.com/elixir-lang/elixir/commits/v1.10.4/lib/elixir/lib/enum.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Range do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + @moduledoc """ + Ranges represent a sequence of one or many, ascending + or descending, consecutive integers. + + Ranges can be either increasing (`first <= last`) or + decreasing (`first > last`). Ranges are also always + inclusive. + + A range is represented internally as a struct. However, + the most common form of creating and matching on ranges + is via the `../2` macro, auto-imported from `Kernel`: + + iex> range = 1..3 + 1..3 + iex> first..last = range + iex> first + 1 + iex> last + 3 + + A range implements the `Enumerable` protocol, which means + functions in the `Enum` module can be used to work with + ranges: + + iex> range = 1..10 + 1..10 + iex> Enum.reduce(range, 0, fn i, acc -> i * i + acc end) + 385 + iex> Enum.count(range) + 10 + iex> Enum.member?(range, 11) + false + iex> Enum.member?(range, 8) + true + + Such function calls are efficient memory-wise no matter the + size of the range. The implementation of the `Enumerable` + protocol uses logic based solely on the endpoints and does + not materialize the whole list of integers. + """ + + defstruct first: nil, last: nil + + @type t :: %__MODULE__{first: integer, last: integer} + @type t(first, last) :: %__MODULE__{first: first, last: last} + + @doc """ + Creates a new range. + + ## Examples + + iex> Range.new(-100, 100) + -100..100 + + """ + @spec new(integer, integer) :: t + def new(first, last) when is_integer(first) and is_integer(last) do + %Range{first: first, last: last} + end + + def new(first, last) do + raise ArgumentError, + "ranges (first..last) expect both sides to be integers, " <> + "got: #{inspect(first)}..#{inspect(last)}" + end + + @doc """ + Checks if two ranges are disjoint. + + ## Examples + + iex> Range.disjoint?(1..5, 6..9) + true + iex> Range.disjoint?(5..1, 6..9) + true + iex> Range.disjoint?(1..5, 5..9) + false + iex> Range.disjoint?(1..5, 2..7) + false + + """ + @doc since: "1.8.0" + @spec disjoint?(t, t) :: boolean + def disjoint?(first1..last1 = _range1, first2..last2 = _range2) do + {first1, last1} = normalize(first1, last1) + {first2, last2} = normalize(first2, last2) + last2 < first1 or last1 < first2 + end + + @compile inline: [normalize: 2] + defp normalize(first, last) when first > last, do: {last, first} + defp normalize(first, last), do: {first, last} +end diff --git a/libs/exavmlib/lib/RuntimeError.ex b/libs/exavmlib/lib/RuntimeError.ex new file mode 100644 index 000000000..423055eb4 --- /dev/null +++ b/libs/exavmlib/lib/RuntimeError.ex @@ -0,0 +1,27 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule RuntimeError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception message: "runtime error" +end diff --git a/libs/exavmlib/lib/SystemLimitError.ex b/libs/exavmlib/lib/SystemLimitError.ex new file mode 100644 index 000000000..b4a36d4b3 --- /dev/null +++ b/libs/exavmlib/lib/SystemLimitError.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule SystemLimitError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [] + + @impl true + def message(_) do + "a system limit has been reached" + end +end diff --git a/libs/exavmlib/lib/TryClauseError.ex b/libs/exavmlib/lib/TryClauseError.ex new file mode 100644 index 000000000..cbd9937be --- /dev/null +++ b/libs/exavmlib/lib/TryClauseError.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule TryClauseError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:term] + + @impl true + def message(exception) do + "no try clause matching: #{inspect(exception.term)}" + end +end diff --git a/libs/exavmlib/lib/UndefinedFunctionError.ex b/libs/exavmlib/lib/UndefinedFunctionError.ex new file mode 100644 index 000000000..8653e3cda --- /dev/null +++ b/libs/exavmlib/lib/UndefinedFunctionError.ex @@ -0,0 +1,77 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule UndefinedFunctionError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:module, :function, :arity, :reason, :message] + + @impl true + def message(%{message: nil} = exception) do + %{reason: reason, module: module, function: function, arity: arity} = exception + {message, _loaded?} = message(reason, module, function, arity) + message + end + + def message(%{message: message}) do + message + end + + defp message(nil, module, function, arity) do + cond do + is_nil(function) or is_nil(arity) -> + {"undefined function", false} + + is_nil(module) -> + formatted_fun = Exception.format_mfa(module, function, arity) + {"function #{formatted_fun} is undefined", false} + + function_exported?(module, :module_info, 0) -> + message(:"function not exported", module, function, arity) + + true -> + message(:"module could not be loaded", module, function, arity) + end + end + + defp message(:"module could not be loaded", module, function, arity) do + formatted_fun = Exception.format_mfa(module, function, arity) + {"function #{formatted_fun} is undefined (module #{inspect(module)} is not available)", false} + end + + defp message(:"function not exported", module, function, arity) do + formatted_fun = Exception.format_mfa(module, function, arity) + {"function #{formatted_fun} is undefined or private", true} + end + + defp message(reason, module, function, arity) do + formatted_fun = Exception.format_mfa(module, function, arity) + {"function #{formatted_fun} is undefined (#{reason})", false} + end + + @impl true + def blame(exception, stacktrace) do + %{reason: reason, module: module, function: function, arity: arity} = exception + {message, _loaded?} = message(reason, module, function, arity) + {%{exception | message: message}, stacktrace} + end +end diff --git a/libs/exavmlib/lib/WithClauseError.ex b/libs/exavmlib/lib/WithClauseError.ex new file mode 100644 index 000000000..fe8f7790e --- /dev/null +++ b/libs/exavmlib/lib/WithClauseError.ex @@ -0,0 +1,32 @@ +# +# This file is part of elixir-lang. +# +# Copyright 2012-2018 Elixir Contributors +# https://github.com/elixir-lang/elixir/tree/v1.7.4/lib/elixir/lib/exception.ex +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule WithClauseError do + # This avoids crashing the compiler at build time + @compile {:autoload, false} + + defexception [:term] + + @impl true + def message(exception) do + "no with clause matching: #{inspect(exception.term)}" + end +end diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0bcf04980..c3c1c3878 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -95,6 +95,11 @@ if (NOT "${CMAKE_GENERATOR}" MATCHES "Xcode") add_subdirectory(libs/estdlib) add_subdirectory(libs/eavmlib) add_subdirectory(libs/alisp) + if (Elixir_FOUND) + add_subdirectory(libs/exavmlib) + else() + message("Unable to find elixirc -- skipping Elixir tests") + endif() endif() if (COVERAGE) diff --git a/tests/libs/exavmlib/CMakeLists.txt b/tests/libs/exavmlib/CMakeLists.txt new file mode 100644 index 000000000..4e9c419c8 --- /dev/null +++ b/tests/libs/exavmlib/CMakeLists.txt @@ -0,0 +1,25 @@ +# +# This file is part of AtomVM. +# +# Copyright 2024 Davide Bettio +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +project(test_exavmlib) + +include(BuildElixir) + +pack_runnable(Tests Tests estdlib eavmlib exavmlib etest) diff --git a/tests/libs/exavmlib/Tests.ex b/tests/libs/exavmlib/Tests.ex new file mode 100644 index 000000000..57c299934 --- /dev/null +++ b/tests/libs/exavmlib/Tests.ex @@ -0,0 +1,153 @@ +# +# This file is part of AtomVM. +# +# Copyright 2024 Davide Bettio +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +defmodule Tests do + def start() do + :ok = IO.puts("Running Elixir tests") + :ok = test_enum() + :ok = test_exception() + :ok = IO.puts("Finished Elixir tests") + end + + defp test_enum() do + # list + 3 = Enum.count([1, 2, 3]) + true = Enum.member?([1, 2, 3], 1) + false = Enum.member?([1, 2, 3], 4) + [0, 2, 4] = Enum.map([0, 1, 2], fn x -> x * 2 end) + 6 = Enum.reduce([1, 2, 3], 0, fn x, acc -> acc + x end) + [2, 3] = Enum.slice([1, 2, 3], 1, 2) + + # map + 2 = Enum.count(%{a: 1, b: 2}) + true = Enum.member?(%{a: 1}, {:a, 1}) + false = Enum.member?(%{a: 1}, {:b, 1}) + kw = Enum.map(%{a: 1, b: 2}, fn x -> x end) + 1 = kw[:a] + 2 = kw[:b] + kw2 = Enum.reduce(%{a: :A, b: :B}, [], fn {k, v}, acc -> [{v, k} | acc] end) + :a = kw2[:A] + :b = kw2[:B] + kw3 = Enum.slice(%{a: 1, b: 2}, 0, 1) + true = (length(kw3) == 1) and ((kw3[:a] == 1) or (kw3[:b] == 2)) + + # map set + 3 = Enum.count(MapSet.new([0, 1, 2])) + true = Enum.member?(MapSet.new([1, 2, 3]), 1) + false = Enum.member?(MapSet.new([1, 2, 3]), 4) + [0, 2, 4] = Enum.map(MapSet.new([0, 1, 2]), fn x -> x * 2 end) + 6 = Enum.reduce(MapSet.new([1, 2, 3]), 0, fn x, acc -> acc + x end) + [] = Enum.slice(MapSet.new([1, 2]), 1, 0) + + # range + 4 = Enum.count(1..4) + true = Enum.member?(1..4, 2) + false = Enum.member?(1..4, 5) + [1, 2, 3, 4] = Enum.map(1..4, fn x -> x end) + 55 = Enum.reduce(1..10, 0, fn x, acc -> x + acc end) + [6, 7, 8, 9, 10] = Enum.slice(1..10, 5, 100) + + # into + %{a: 1, b: 2} = Enum.into([a: 1, b: 2], %{}) + %{a: 2} = Enum.into([a: 1, a: 2], %{}) + expected_mapset = MapSet.new([1, 2, 3]) + ^expected_mapset = Enum.into([1, 2, 3], MapSet.new()) + + # Enum.join + "1, 2, 3" = Enum.join(["1", "2", "3"], ", ") + + # Enum.reverse + [4, 3, 2] = Enum.reverse([2, 3, 4]) + + undef = + try do + Enum.map({1, 2}, fn x -> x end) + rescue + e -> e + end + + case undef do + %Protocol.UndefinedError{description: "", protocol: Enumerable, value: {1, 2}} -> + :ok + + %UndefinedFunctionError{arity: 3, function: :reduce, module: Enumerable} -> + # code compiled with OTP != 25 doesn't raise Protocol.UndefinedError + :ok + end + + :ok + end + + defp test_exception() do + ex1 = + try do + raise "This is a test" + rescue + e -> e + end + + %RuntimeError{message: "This is a test"} = ex1 + + ex2 = + try do + :undef.ined(1, 2) + rescue + e -> e + end + + # TODO: match for arity: 2, function: :ined, module: :undef + %UndefinedFunctionError{} = ex2 + + ex3 = + try do + fact(5) + fact(-2) + rescue + e -> e + end + + %ArithmeticError{} = ex3 + + ex4 = + try do + :erlang.integer_to_list(fact(-2)) + rescue + e -> e + end + + %ArgumentError{} = ex4 + + ex5 = + try do + a = fact(-1) + b = fact(3) + ^a = b + rescue + e -> e + end + + %MatchError{} = ex5 + + :ok + end + + defp fact(n) when n < 0, do: :test + defp fact(0), do: 1 + defp fact(n), do: fact(n - 1) * n +end