Skip to content

Commit 8bcca51

Browse files
committed
Merge pull request #1259 from bettio/increase-elixir-support-2
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
2 parents 2aaf7a3 + e8d214b commit 8bcca51

File tree

13 files changed

+476
-11
lines changed

13 files changed

+476
-11
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- ESP32: add a new Elixir release "flavor" with a bigger boot.avm partition that has room for
1212
Elixir standard library modules
1313
- ESP32: `--boot` option to mkimage.sh tool
14+
- Add `erlang:atom_to_binary/1` that is equivalent to `erlang:atom_to_binary(Atom, utf8)`
15+
- Support for Elixir `String.Chars` protocol, now functions such as `Enum.join` are able to take
16+
also non string parameters (e.g. `Enum.join([1, 2], ",")`
17+
- Support for Elixir `Enum.at/3`
18+
- Add support to Elixir `Enumerable` protocol also for `Enum.all?`, `Enum.any?`, `Enum.each` and
19+
`Enum.filter`
1420

1521
### Changed
1622

libs/exavmlib/lib/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ set(ELIXIR_MODULES
7474
Collectable.List
7575
Collectable.Map
7676
Collectable.MapSet
77+
78+
String.Chars
79+
String.Chars.Atom
80+
String.Chars.BitString
81+
String.Chars.Float
82+
String.Chars.Integer
83+
String.Chars.List
7784
)
7885

7986
pack_archive(exavmlib ${ELIXIR_MODULES})

libs/exavmlib/lib/Enum.ex

Lines changed: 216 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,14 @@ defmodule Enum do
2727
@type index :: integer
2828
@type element :: any
2929

30+
@type default :: any
31+
3032
require Stream.Reducers, as: R
3133

34+
defmacrop skip(acc) do
35+
acc
36+
end
37+
3238
defmacrop next(_, entry, acc) do
3339
quote(do: [unquote(entry) | unquote(acc)])
3440
end
@@ -53,14 +59,132 @@ defmodule Enum do
5359
Enumerable.reduce(enumerable, {:cont, acc}, fn x, acc -> {:cont, fun.(x, acc)} end) |> elem(1)
5460
end
5561

62+
@doc """
63+
Returns `true` if `fun.(element)` is truthy for all elements in `enumerable`.
64+
65+
Iterates over the `enumerable` and invokes `fun` on each element. When an invocation
66+
of `fun` returns a falsy value (`false` or `nil`) iteration stops immediately and
67+
`false` is returned. In all other cases `true` is returned.
68+
69+
## Examples
70+
71+
iex> Enum.all?([2, 4, 6], fn x -> rem(x, 2) == 0 end)
72+
true
73+
74+
iex> Enum.all?([2, 3, 4], fn x -> rem(x, 2) == 0 end)
75+
false
76+
77+
iex> Enum.all?([], fn x -> x > 0 end)
78+
true
79+
80+
If no function is given, the truthiness of each element is checked during iteration.
81+
When an element has a falsy value (`false` or `nil`) iteration stops immediately and
82+
`false` is returned. In all other cases `true` is returned.
83+
84+
iex> Enum.all?([1, 2, 3])
85+
true
86+
87+
iex> Enum.all?([1, nil, 3])
88+
false
89+
90+
iex> Enum.all?([])
91+
true
92+
93+
"""
94+
@spec all?(t, (element -> as_boolean(term))) :: boolean
95+
96+
def all?(enumerable, fun \\ fn x -> x end)
97+
5698
def all?(enumerable, fun) when is_list(enumerable) do
5799
all_list(enumerable, fun)
58100
end
59101

102+
def all?(enumerable, fun) do
103+
Enumerable.reduce(enumerable, {:cont, true}, fn entry, _ ->
104+
if fun.(entry), do: {:cont, true}, else: {:halt, false}
105+
end)
106+
|> elem(1)
107+
end
108+
109+
@doc """
110+
Returns `true` if `fun.(element)` is truthy for at least one element in `enumerable`.
111+
112+
Iterates over the `enumerable` and invokes `fun` on each element. When an invocation
113+
of `fun` returns a truthy value (neither `false` nor `nil`) iteration stops
114+
immediately and `true` is returned. In all other cases `false` is returned.
115+
116+
## Examples
117+
118+
iex> Enum.any?([2, 4, 6], fn x -> rem(x, 2) == 1 end)
119+
false
120+
121+
iex> Enum.any?([2, 3, 4], fn x -> rem(x, 2) == 1 end)
122+
true
123+
124+
iex> Enum.any?([], fn x -> x > 0 end)
125+
false
126+
127+
If no function is given, the truthiness of each element is checked during iteration.
128+
When an element has a truthy value (neither `false` nor `nil`) iteration stops
129+
immediately and `true` is returned. In all other cases `false` is returned.
130+
131+
iex> Enum.any?([false, false, false])
132+
false
133+
134+
iex> Enum.any?([false, true, false])
135+
true
136+
137+
iex> Enum.any?([])
138+
false
139+
140+
"""
141+
@spec any?(t, (element -> as_boolean(term))) :: boolean
142+
143+
def any?(enumerable, fun \\ fn x -> x end)
144+
60145
def any?(enumerable, fun) when is_list(enumerable) do
61146
any_list(enumerable, fun)
62147
end
63148

149+
def any?(enumerable, fun) do
150+
Enumerable.reduce(enumerable, {:cont, false}, fn entry, _ ->
151+
if fun.(entry), do: {:halt, true}, else: {:cont, false}
152+
end)
153+
|> elem(1)
154+
end
155+
156+
@doc """
157+
Finds the element at the given `index` (zero-based).
158+
159+
Returns `default` if `index` is out of bounds.
160+
161+
A negative `index` can be passed, which means the `enumerable` is
162+
enumerated once and the `index` is counted from the end (for example,
163+
`-1` finds the last element).
164+
165+
## Examples
166+
167+
iex> Enum.at([2, 4, 6], 0)
168+
2
169+
170+
iex> Enum.at([2, 4, 6], 2)
171+
6
172+
173+
iex> Enum.at([2, 4, 6], 4)
174+
nil
175+
176+
iex> Enum.at([2, 4, 6], 4, :none)
177+
:none
178+
179+
"""
180+
@spec at(t, index, default) :: element | default
181+
def at(enumerable, index, default \\ nil) when is_integer(index) do
182+
case slice_any(enumerable, index, 1) do
183+
[value] -> value
184+
[] -> default
185+
end
186+
end
187+
64188
@doc """
65189
Returns the size of the enumerable.
66190
@@ -85,19 +209,102 @@ defmodule Enum do
85209
end
86210
end
87211

212+
@doc """
213+
Invokes the given `fun` for each element in the `enumerable`.
214+
215+
Returns `:ok`.
216+
217+
## Examples
218+
219+
Enum.each(["some", "example"], fn x -> IO.puts(x) end)
220+
"some"
221+
"example"
222+
#=> :ok
223+
224+
"""
225+
@spec each(t, (element -> any)) :: :ok
88226
def each(enumerable, fun) when is_list(enumerable) do
89227
:lists.foreach(fun, enumerable)
90228
:ok
91229
end
92230

231+
def each(enumerable, fun) do
232+
reduce(enumerable, nil, fn entry, _ ->
233+
fun.(entry)
234+
nil
235+
end)
236+
237+
:ok
238+
end
239+
240+
@doc """
241+
Filters the `enumerable`, i.e. returns only those elements
242+
for which `fun` returns a truthy value.
243+
244+
See also `reject/2` which discards all elements where the
245+
function returns a truthy value.
246+
247+
## Examples
248+
249+
iex> Enum.filter([1, 2, 3], fn x -> rem(x, 2) == 0 end)
250+
[2]
251+
252+
Keep in mind that `filter` is not capable of filtering and
253+
transforming an element at the same time. If you would like
254+
to do so, consider using `flat_map/2`. For example, if you
255+
want to convert all strings that represent an integer and
256+
discard the invalid one in one pass:
257+
258+
strings = ["1234", "abc", "12ab"]
259+
260+
Enum.flat_map(strings, fn string ->
261+
case Integer.parse(string) do
262+
# transform to integer
263+
{int, _rest} -> [int]
264+
# skip the value
265+
:error -> []
266+
end
267+
end)
268+
269+
"""
270+
@spec filter(t, (element -> as_boolean(term))) :: list
93271
def filter(enumerable, fun) when is_list(enumerable) do
94272
filter_list(enumerable, fun)
95273
end
96274

275+
def filter(enumerable, fun) do
276+
reduce(enumerable, [], R.filter(fun)) |> :lists.reverse()
277+
end
278+
279+
@doc """
280+
Returns the first element for which `fun` returns a truthy value.
281+
If no such element is found, returns `default`.
282+
283+
## Examples
284+
285+
iex> Enum.find([2, 3, 4], fn x -> rem(x, 2) == 1 end)
286+
3
287+
288+
iex> Enum.find([2, 4, 6], fn x -> rem(x, 2) == 1 end)
289+
nil
290+
iex> Enum.find([2, 4, 6], 0, fn x -> rem(x, 2) == 1 end)
291+
0
292+
293+
"""
294+
@spec find(t, default, (element -> any)) :: element | default
295+
def find(enumerable, default \\ nil, fun)
296+
97297
def find(enumerable, default, fun) when is_list(enumerable) do
98298
find_list(enumerable, default, fun)
99299
end
100300

301+
def find(enumerable, default, fun) do
302+
Enumerable.reduce(enumerable, {:cont, default}, fn entry, default ->
303+
if fun.(entry), do: {:halt, entry}, else: {:cont, default}
304+
end)
305+
|> elem(1)
306+
end
307+
101308
def find_index(enumerable, fun) when is_list(enumerable) do
102309
find_index_list(enumerable, 0, fun)
103310
end
@@ -389,12 +596,12 @@ defmodule Enum do
389596
end
390597

391598
@doc """
392-
Joins the given enumerable into a binary using `joiner` as a
599+
Joins the given `enumerable` into a binary using `joiner` as a
393600
separator.
394601
395602
If `joiner` is not passed at all, it defaults to the empty binary.
396603
397-
All items in the enumerable must be convertible to a binary,
604+
All elements in the `enumerable` must be convertible to a binary,
398605
otherwise an error is raised.
399606
400607
## Examples
@@ -409,6 +616,12 @@ defmodule Enum do
409616
@spec join(t, String.t()) :: String.t()
410617
def join(enumerable, joiner \\ "")
411618

619+
def join(enumerable, "") do
620+
enumerable
621+
|> map(&entry_to_string(&1))
622+
|> IO.iodata_to_binary()
623+
end
624+
412625
def join(enumerable, joiner) when is_binary(joiner) do
413626
reduced =
414627
reduce(enumerable, :first, fn
@@ -610,6 +823,7 @@ defmodule Enum do
610823
@compile {:inline, entry_to_string: 1, reduce: 3}
611824

612825
defp entry_to_string(entry) when is_binary(entry), do: entry
826+
defp entry_to_string(entry), do: String.Chars.to_string(entry)
613827

614828
## drop
615829

libs/exavmlib/lib/Enumerable.MapSet.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ defimpl Enumerable, for: MapSet do
3434

3535
def slice(map_set) do
3636
size = MapSet.size(map_set)
37-
{:ok, size, &MapSet.to_list/1}
37+
{:ok, size, &Enumerable.List.slice(MapSet.to_list(map_set), &1, &2, size)}
3838
end
3939
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#
2+
# This file is part of elixir-lang.
3+
#
4+
# Copyright 2013-2023 Elixir Contributors
5+
# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/string/chars.ex
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
#
19+
# SPDX-License-Identifier: Apache-2.0
20+
#
21+
22+
import Kernel, except: [to_string: 1]
23+
24+
defimpl String.Chars, for: Atom do
25+
def to_string(nil) do
26+
""
27+
end
28+
29+
def to_string(atom) do
30+
Atom.to_string(atom)
31+
end
32+
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#
2+
# This file is part of elixir-lang.
3+
#
4+
# Copyright 2013-2023 Elixir Contributors
5+
# https://github.com/elixir-lang/elixir/commits/v1.17.2/lib/elixir/lib/string/chars.ex
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
#
19+
# SPDX-License-Identifier: Apache-2.0
20+
#
21+
22+
import Kernel, except: [to_string: 1]
23+
24+
defimpl String.Chars, for: BitString do
25+
def to_string(term) when is_binary(term) do
26+
term
27+
end
28+
29+
def to_string(term) do
30+
raise Protocol.UndefinedError,
31+
protocol: @protocol,
32+
value: term,
33+
description: "cannot convert a bitstring to a string"
34+
end
35+
end

0 commit comments

Comments
 (0)