Skip to content

Commit

Permalink
Merge pull request #1259 from bettio/increase-elixir-support-2
Browse files Browse the repository at this point in the history
Increase Elixir support (part 2)

This PR introduces a number of important Elixir features, such as `String.Chars`
protocol and wider support to `Enumerable` in `Enum` module.

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

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
  • Loading branch information
bettio committed Sep 15, 2024
2 parents 2aaf7a3 + e8d214b commit 8bcca51
Show file tree
Hide file tree
Showing 13 changed files with 476 additions and 11 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- ESP32: add a new Elixir release "flavor" with a bigger boot.avm partition that has room for
Elixir standard library modules
- ESP32: `--boot` option to mkimage.sh tool
- Add `erlang:atom_to_binary/1` that is equivalent to `erlang:atom_to_binary(Atom, utf8)`
- Support for Elixir `String.Chars` protocol, now functions such as `Enum.join` are able to take
also non string parameters (e.g. `Enum.join([1, 2], ",")`
- Support for Elixir `Enum.at/3`
- Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each` and
`Enum.filter`

### Changed

Expand Down
7 changes: 7 additions & 0 deletions libs/exavmlib/lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ set(ELIXIR_MODULES
Collectable.List
Collectable.Map
Collectable.MapSet

String.Chars
String.Chars.Atom
String.Chars.BitString
String.Chars.Float
String.Chars.Integer
String.Chars.List
)

pack_archive(exavmlib ${ELIXIR_MODULES})
218 changes: 216 additions & 2 deletions libs/exavmlib/lib/Enum.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,14 @@ defmodule Enum do
@type index :: integer
@type element :: any

@type default :: any

require Stream.Reducers, as: R

defmacrop skip(acc) do
acc
end

defmacrop next(_, entry, acc) do
quote(do: [unquote(entry) | unquote(acc)])
end
Expand All @@ -53,14 +59,132 @@ defmodule Enum do
Enumerable.reduce(enumerable, {:cont, acc}, fn x, acc -> {:cont, fun.(x, acc)} end) |> elem(1)
end

@doc """
Returns `true` if `fun.(element)` is truthy for all elements in `enumerable`.
Iterates over the `enumerable` and invokes `fun` on each element. When an invocation
of `fun` returns a falsy value (`false` or `nil`) iteration stops immediately and
`false` is returned. In all other cases `true` is returned.
## Examples
iex> Enum.all?([2, 4, 6], fn x -> rem(x, 2) == 0 end)
true
iex> Enum.all?([2, 3, 4], fn x -> rem(x, 2) == 0 end)
false
iex> Enum.all?([], fn x -> x > 0 end)
true
If no function is given, the truthiness of each element is checked during iteration.
When an element has a falsy value (`false` or `nil`) iteration stops immediately and
`false` is returned. In all other cases `true` is returned.
iex> Enum.all?([1, 2, 3])
true
iex> Enum.all?([1, nil, 3])
false
iex> Enum.all?([])
true
"""
@spec all?(t, (element -> as_boolean(term))) :: boolean

def all?(enumerable, fun \\ fn x -> x end)

def all?(enumerable, fun) when is_list(enumerable) do
all_list(enumerable, fun)
end

def all?(enumerable, fun) do
Enumerable.reduce(enumerable, {:cont, true}, fn entry, _ ->
if fun.(entry), do: {:cont, true}, else: {:halt, false}
end)
|> elem(1)
end

@doc """
Returns `true` if `fun.(element)` is truthy for at least one element in `enumerable`.
Iterates over the `enumerable` and invokes `fun` on each element. When an invocation
of `fun` returns a truthy value (neither `false` nor `nil`) iteration stops
immediately and `true` is returned. In all other cases `false` is returned.
## Examples
iex> Enum.any?([2, 4, 6], fn x -> rem(x, 2) == 1 end)
false
iex> Enum.any?([2, 3, 4], fn x -> rem(x, 2) == 1 end)
true
iex> Enum.any?([], fn x -> x > 0 end)
false
If no function is given, the truthiness of each element is checked during iteration.
When an element has a truthy value (neither `false` nor `nil`) iteration stops
immediately and `true` is returned. In all other cases `false` is returned.
iex> Enum.any?([false, false, false])
false
iex> Enum.any?([false, true, false])
true
iex> Enum.any?([])
false
"""
@spec any?(t, (element -> as_boolean(term))) :: boolean

def any?(enumerable, fun \\ fn x -> x end)

def any?(enumerable, fun) when is_list(enumerable) do
any_list(enumerable, fun)
end

def any?(enumerable, fun) do
Enumerable.reduce(enumerable, {:cont, false}, fn entry, _ ->
if fun.(entry), do: {:halt, true}, else: {:cont, false}
end)
|> elem(1)
end

@doc """
Finds the element at the given `index` (zero-based).
Returns `default` if `index` is out of bounds.
A negative `index` can be passed, which means the `enumerable` is
enumerated once and the `index` is counted from the end (for example,
`-1` finds the last element).
## Examples
iex> Enum.at([2, 4, 6], 0)
2
iex> Enum.at([2, 4, 6], 2)
6
iex> Enum.at([2, 4, 6], 4)
nil
iex> Enum.at([2, 4, 6], 4, :none)
:none
"""
@spec at(t, index, default) :: element | default
def at(enumerable, index, default \\ nil) when is_integer(index) do
case slice_any(enumerable, index, 1) do
[value] -> value
[] -> default
end
end

@doc """
Returns the size of the enumerable.
Expand All @@ -85,19 +209,102 @@ defmodule Enum do
end
end

@doc """
Invokes the given `fun` for each element in the `enumerable`.
Returns `:ok`.
## Examples
Enum.each(["some", "example"], fn x -> IO.puts(x) end)
"some"
"example"
#=> :ok
"""
@spec each(t, (element -> any)) :: :ok
def each(enumerable, fun) when is_list(enumerable) do
:lists.foreach(fun, enumerable)
:ok
end

def each(enumerable, fun) do
reduce(enumerable, nil, fn entry, _ ->
fun.(entry)
nil
end)

:ok
end

@doc """
Filters the `enumerable`, i.e. returns only those elements
for which `fun` returns a truthy value.
See also `reject/2` which discards all elements where the
function returns a truthy value.
## Examples
iex> Enum.filter([1, 2, 3], fn x -> rem(x, 2) == 0 end)
[2]
Keep in mind that `filter` is not capable of filtering and
transforming an element at the same time. If you would like
to do so, consider using `flat_map/2`. For example, if you
want to convert all strings that represent an integer and
discard the invalid one in one pass:
strings = ["1234", "abc", "12ab"]
Enum.flat_map(strings, fn string ->
case Integer.parse(string) do
# transform to integer
{int, _rest} -> [int]
# skip the value
:error -> []
end
end)
"""
@spec filter(t, (element -> as_boolean(term))) :: list
def filter(enumerable, fun) when is_list(enumerable) do
filter_list(enumerable, fun)
end

def filter(enumerable, fun) do
reduce(enumerable, [], R.filter(fun)) |> :lists.reverse()
end

@doc """
Returns the first element for which `fun` returns a truthy value.
If no such element is found, returns `default`.
## Examples
iex> Enum.find([2, 3, 4], fn x -> rem(x, 2) == 1 end)
3
iex> Enum.find([2, 4, 6], fn x -> rem(x, 2) == 1 end)
nil
iex> Enum.find([2, 4, 6], 0, fn x -> rem(x, 2) == 1 end)
0
"""
@spec find(t, default, (element -> any)) :: element | default
def find(enumerable, default \\ nil, fun)

def find(enumerable, default, fun) when is_list(enumerable) do
find_list(enumerable, default, fun)
end

def find(enumerable, default, fun) do
Enumerable.reduce(enumerable, {:cont, default}, fn entry, default ->
if fun.(entry), do: {:halt, entry}, else: {:cont, default}
end)
|> elem(1)
end

def find_index(enumerable, fun) when is_list(enumerable) do
find_index_list(enumerable, 0, fun)
end
Expand Down Expand Up @@ -389,12 +596,12 @@ defmodule Enum do
end

@doc """
Joins the given enumerable into a binary using `joiner` as a
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,
All elements in the `enumerable` must be convertible to a binary,
otherwise an error is raised.
## Examples
Expand All @@ -409,6 +616,12 @@ defmodule Enum do
@spec join(t, String.t()) :: String.t()
def join(enumerable, joiner \\ "")

def join(enumerable, "") do
enumerable
|> map(&entry_to_string(&1))
|> IO.iodata_to_binary()
end

def join(enumerable, joiner) when is_binary(joiner) do
reduced =
reduce(enumerable, :first, fn
Expand Down Expand Up @@ -610,6 +823,7 @@ defmodule Enum do
@compile {:inline, entry_to_string: 1, reduce: 3}

defp entry_to_string(entry) when is_binary(entry), do: entry
defp entry_to_string(entry), do: String.Chars.to_string(entry)

## drop

Expand Down
2 changes: 1 addition & 1 deletion libs/exavmlib/lib/Enumerable.MapSet.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ defimpl Enumerable, for: MapSet do

def slice(map_set) do
size = MapSet.size(map_set)
{:ok, size, &MapSet.to_list/1}
{:ok, size, &Enumerable.List.slice(MapSet.to_list(map_set), &1, &2, size)}
end
end
32 changes: 32 additions & 0 deletions libs/exavmlib/lib/String.Chars.Atom.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#
# This file is part of elixir-lang.
#
# Copyright 2013-2023 Elixir Contributors
# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/string/chars.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
#

import Kernel, except: [to_string: 1]

defimpl String.Chars, for: Atom do
def to_string(nil) do
""
end

def to_string(atom) do
Atom.to_string(atom)
end
end
35 changes: 35 additions & 0 deletions libs/exavmlib/lib/String.Chars.BitString.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#
# This file is part of elixir-lang.
#
# Copyright 2013-2023 Elixir Contributors
# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/string/chars.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
#

import Kernel, except: [to_string: 1]

defimpl String.Chars, for: BitString do
def to_string(term) when is_binary(term) do
term
end

def to_string(term) do
raise Protocol.UndefinedError,
protocol: @protocol,
value: term,
description: "cannot convert a bitstring to a string"
end
end
Loading

0 comments on commit 8bcca51

Please sign in to comment.