From bb901b8406d414746496c60ddf4d9f8420b5d4b0 Mon Sep 17 00:00:00 2001 From: Mateusz Front Date: Thu, 25 Sep 2025 17:47:34 +0200 Subject: [PATCH] Rewrite lists:keyfind, lists:keymember, lists:member as NIFs; implement erlang:list_to_bitstring Signed-off-by: Mateusz Front --- CHANGELOG.md | 2 + libs/estdlib/src/lists.erl | 40 +------ src/libAtomVM/defaultatoms.def | 3 + src/libAtomVM/nifs.c | 111 ++++++++++++++++++ src/libAtomVM/nifs.gperf | 4 + tests/erlang_tests/CMakeLists.txt | 11 ++ tests/erlang_tests/test_list_to_bitstring.erl | 75 ++++++++++++ tests/erlang_tests/test_lists_keyfind.erl | 95 +++++++++++++++ tests/erlang_tests/test_lists_keymember.erl | 95 +++++++++++++++ tests/erlang_tests/test_lists_member.erl | 71 +++++++++++ tests/test.c | 5 + 11 files changed, 476 insertions(+), 36 deletions(-) create mode 100644 tests/erlang_tests/test_list_to_bitstring.erl create mode 100644 tests/erlang_tests/test_lists_keyfind.erl create mode 100644 tests/erlang_tests/test_lists_keymember.erl create mode 100644 tests/erlang_tests/test_lists_member.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index eab29a8c64..e5c49554c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `supervisor:which_children/1` and `supervisor:count_children/1` - Added `monitored_by` in `process_info/2` - Added mock implementation for `current_stacktrace` in `process_info` +- Added `erlang:list_to_bitstring` +- Reimplemented `lists:keyfind`, `lists:keymember` and `lists:member` as NIFs ### Changed diff --git a/libs/estdlib/src/lists.erl b/libs/estdlib/src/lists.erl index 3be5778eb9..c7bdf340df 100644 --- a/libs/estdlib/src/lists.erl +++ b/libs/estdlib/src/lists.erl @@ -135,11 +135,7 @@ last([_H | T]) -> last(T). %%----------------------------------------------------------------------------- -spec member(E :: term(), L :: list()) -> boolean(). member(_, []) -> - false; -member(E, [E | _]) -> - true; -member(E, [_ | T]) -> - member(E, T). + erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param E the member to delete @@ -265,22 +261,8 @@ keydelete(K, I, [H | T], L2) -> %% @end %%----------------------------------------------------------------------------- -spec keyfind(K :: term(), I :: pos_integer(), L :: list(tuple())) -> tuple() | false. -keyfind(_K, _I, []) -> - false; -keyfind(K, I, [H | T]) when is_tuple(H) -> - case I =< tuple_size(H) of - true -> - case element(I, H) of - K -> - H; - _ -> - keyfind(K, I, T) - end; - false -> - keyfind(K, I, T) - end; -keyfind(K, I, [_H | T]) -> - keyfind(K, I, T). +keyfind(_K, _I, _L) -> + erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param K the key to match @@ -292,21 +274,7 @@ keyfind(K, I, [_H | T]) -> %%----------------------------------------------------------------------------- -spec keymember(K :: term(), I :: pos_integer(), L :: list(tuple())) -> boolean(). keymember(_K, _I, []) -> - false; -keymember(K, I, [H | T]) when is_tuple(H) -> - case I =< tuple_size(H) of - true -> - case element(I, H) of - K -> - true; - _ -> - keymember(K, I, T) - end; - false -> - keymember(K, I, T) - end; -keymember(K, I, [_H | T]) -> - keymember(K, I, T). + erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param K the key to match diff --git a/src/libAtomVM/defaultatoms.def b/src/libAtomVM/defaultatoms.def index bff6e87fac..f192cb3bc5 100644 --- a/src/libAtomVM/defaultatoms.def +++ b/src/libAtomVM/defaultatoms.def @@ -194,3 +194,6 @@ X(INIT_ATOM, "\x4", "init") X(MONITORED_BY_ATOM, "\xC", "monitored_by") X(CURRENT_STACKTRACE_ATOM, "\x12", "current_stacktrace") + +X(KEY_ATOM, "\x3", "key") +X(VALUE_ATOM, "\x5", "value") diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 9961dac5c5..84b9785e94 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -177,6 +177,7 @@ static term nif_erlang_setnode_2(Context *ctx, int argc, term argv[]); static term nif_erlang_memory(Context *ctx, int argc, term argv[]); static term nif_erlang_monitor(Context *ctx, int argc, term argv[]); static term nif_erlang_demonitor(Context *ctx, int argc, term argv[]); +static term nif_erlang_list_to_bitstring_1(Context *ctx, int argc, term argv[]); static term nif_erlang_unlink(Context *ctx, int argc, term argv[]); static term nif_atomvm_add_avm_pack_binary(Context *ctx, int argc, term argv[]); static term nif_atomvm_add_avm_pack_file(Context *ctx, int argc, term argv[]); @@ -197,6 +198,9 @@ static term nif_code_ensure_loaded(Context *ctx, int argc, term argv[]); static term nif_erlang_module_loaded(Context *ctx, int argc, term argv[]); static term nif_erlang_nif_error(Context *ctx, int argc, term argv[]); static term nif_lists_reverse(Context *ctx, int argc, term argv[]); +static term nif_lists_keyfind(Context *ctx, int argc, term argv[]); +static term nif_lists_keymember(Context *ctx, int argc, term argv[]); +static term nif_lists_member(Context *ctx, int argc, term argv[]); static term nif_maps_from_keys(Context *ctx, int argc, term argv[]); static term nif_maps_next(Context *ctx, int argc, term argv[]); static term nif_unicode_characters_to_list(Context *ctx, int argc, term argv[]); @@ -764,6 +768,22 @@ static const struct Nif erlang_lists_subtract_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_erlang_lists_subtract }; +static const struct Nif lists_member_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_lists_member +}; +static const struct Nif lists_keymember_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_lists_keymember +}; +static const struct Nif lists_keyfind_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_lists_keyfind +}; +static const struct Nif list_to_bitstring_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_erlang_list_to_bitstring_1 +}; static const struct Nif zlib_compress_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_zlib_compress_1 @@ -5824,6 +5844,97 @@ static term nif_erlang_lists_subtract(Context *ctx, int argc, term argv[]) return result; } +static term nif_lists_member(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc) + term elem = argv[0]; + term list = argv[1]; + + while (term_is_nonempty_list(list)) { + term head = term_get_list_head(list); + + TermCompareResult cmp_result = term_compare(head, elem, TermCompareExact, ctx->global); + + if (cmp_result == TermEquals) { + return TRUE_ATOM; + } + + if (UNLIKELY(cmp_result == TermCompareMemoryAllocFail)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + list = term_get_list_tail(list); + } + + VALIDATE_VALUE(list, term_is_nil); + + return FALSE_ATOM; +} + +static term nif_lists_keymember(Context *ctx, int argc, term argv[]) +{ + term result = nif_lists_keyfind(ctx, argc, argv); + if (UNLIKELY(term_is_invalid_term(result))) { + return result; + } + return result == FALSE_ATOM ? FALSE_ATOM : TRUE_ATOM; +} + +static term nif_lists_keyfind(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc) + term key = argv[0]; + term n = argv[1]; + term tuple_list = argv[2]; + + VALIDATE_VALUE(n, term_is_integer); + + avm_int_t n_pos = term_to_int(n); + + if (n_pos <= 0) { + RAISE_ERROR(BADARG_ATOM); + } + + while (term_is_nonempty_list(tuple_list)) { + term tuple = term_get_list_head(tuple_list); + + if (!term_is_tuple(tuple)) { + tuple_list = term_get_list_tail(tuple_list); + continue; + } + + int tuple_size = term_get_tuple_arity(tuple); + + if (n_pos > tuple_size) { + tuple_list = term_get_list_tail(tuple_list); + continue; + } + + term nth_element = term_get_tuple_element(tuple, n_pos - 1); + + TermCompareResult cmp_result = term_compare(nth_element, key, TermCompareExact, ctx->global); + + if (cmp_result == TermEquals) { + return tuple; + } + if (UNLIKELY(cmp_result == TermCompareMemoryAllocFail)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + tuple_list = term_get_list_tail(tuple_list); + } + + VALIDATE_VALUE(tuple_list, term_is_nil); + + return FALSE_ATOM; +} + +static term nif_erlang_list_to_bitstring_1(Context *ctx, int argc, term argv[]) +{ + // TODO: implement proper list_to_bitstring function when the bitstrings are supported + return nif_erlang_list_to_binary_1(ctx, argc, argv); +} + #ifdef WITH_ZLIB static term nif_zlib_compress_1(Context *ctx, int argc, term argv[]) { diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index 0e7c0e7e06..78cf367ff1 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -140,6 +140,7 @@ erlang:dist_ctrl_get_data/1, &dist_ctrl_get_data_nif erlang:dist_ctrl_put_data/2, &dist_ctrl_put_data_nif erlang:module_loaded/1,&module_loaded_nif erlang:nif_error/1,&nif_error_nif +erlang:list_to_bitstring/1, &list_to_bitstring_nif erts_debug:flat_size/1, &flat_size_nif ets:new/2, &ets_new_nif ets:insert/2, &ets_insert_nif @@ -180,6 +181,9 @@ base64:encode/1, &base64_encode_nif base64:decode/1, &base64_decode_nif base64:encode_to_string/1, &base64_encode_to_string_nif base64:decode_to_string/1, &base64_decode_to_string_nif +lists:keyfind/3, &lists_keyfind_nif +lists:keymember/3, &lists_keymember_nif +lists:member/2, &lists_member_nif lists:reverse/1, &lists_reverse_nif lists:reverse/2, &lists_reverse_nif maps:from_keys/2, &maps_from_keys_nif diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index 8aeb8b9fd3..5a4e0a62ea 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -551,6 +551,11 @@ compile_assembler(test_op_bs_start_match_asm) compile_erlang(test_op_bs_create_bin) compile_assembler(test_op_bs_create_bin_asm) +compile_erlang(test_list_to_bitstring) +compile_erlang(test_lists_member) +compile_erlang(test_lists_keymember) +compile_erlang(test_lists_keyfind) + if(Erlang_VERSION VERSION_GREATER_EQUAL "23") set(OTP23_OR_GREATER_TESTS test_op_bs_start_match_asm.beam @@ -1070,6 +1075,12 @@ add_custom_target(erlang_test_modules DEPENDS test_ets.beam test_node.beam + + test_list_to_bitstring.beam + test_lists_member.beam + test_lists_keymember.beam + test_lists_keyfind.beam + test_op_bs_start_match.beam test_op_bs_create_bin.beam diff --git a/tests/erlang_tests/test_list_to_bitstring.erl b/tests/erlang_tests/test_list_to_bitstring.erl new file mode 100644 index 0000000000..c2bca8bae1 --- /dev/null +++ b/tests/erlang_tests/test_list_to_bitstring.erl @@ -0,0 +1,75 @@ +% +% This file is part of AtomVM. +% +% Copyright 2025 Franciszek Kubis +% +% 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 +% + +-module(test_list_to_bitstring). + +-export([start/0, concat/2, concat2/2, compare_bin/3, id/1]). + +start() -> + ok = test_concat(), + ok = test_iolist(), + ok = test_empty_list_to_binary(), + 0. + +test_concat() -> + Bin = concat("Hello", "world"), + Bin2 = concat2("", ""), + CompRes1 = compare_bin(Bin, <<"Hello world">>) - compare_bin(Bin, <<"HelloXworld">>), + 1 = CompRes1 + byte_size(Bin2) + invalid(42), + ok. + +test_iolist() -> + <<"Hello world">> = list_to_bitstring(?MODULE:id([<<"Hello ">>, [<<"wor">>, [$l, $d]]])), + ok. + +test_empty_list_to_binary() -> + <<"">> = erlang:list_to_bitstring(?MODULE:id([])), + ok. + +concat(A, B) -> + list_to_bitstring(?MODULE:id(A ++ " " ++ B)). + +concat2(A, B) -> + list_to_bitstring(?MODULE:id(A ++ B)). + +invalid(A) -> + try list_to_bitstring(?MODULE:id(A)) of + Any -> byte_size(Any) + catch + error:badarg -> 0; + _:_ -> 1000 + end. + +compare_bin(Bin1, Bin2) -> + compare_bin(Bin1, Bin2, byte_size(Bin1) - 1). + +compare_bin(_Bin1, _Bin2, -1) -> + 1; +compare_bin(Bin1, Bin2, Index) -> + B1 = binary:at(Bin1, Index), + case binary:at(Bin2, Index) of + B1 -> + compare_bin(Bin1, Bin2, Index - 1); + _Any -> + 0 + end. + +id(X) -> + X. diff --git a/tests/erlang_tests/test_lists_keyfind.erl b/tests/erlang_tests/test_lists_keyfind.erl new file mode 100644 index 0000000000..852219dd0b --- /dev/null +++ b/tests/erlang_tests/test_lists_keyfind.erl @@ -0,0 +1,95 @@ +% +% This file is part of AtomVM. +% +% Copyright 2025 Software Mansion +% +% 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 +% + +-module(test_lists_keyfind). + +-export([start/0, id/1]). + +-define(ID(X), ?MODULE:id(X)). + +id(X) -> X. + +start() -> + ok = test_keyfind_with_existing_key(), + ok = test_keyfind_with_non_existing_key(), + ok = test_keyfind_with_empty_list(), + ok = test_keyfind_with_existing_key_on_different_position(), + ok = test_keyfind_with_existing_key_and_different_length_of_tuples(), + ok = test_keyfind_with_invalid_input(), + 0. + +test_keyfind_with_existing_key() -> + Key = ?ID(bob), + Tuple1 = ?ID({1, alice}), + Tuple2 = ?ID({2, bob}), + Tuple3 = ?ID({3, carol}), + List = ?ID([Tuple1, Tuple2, Tuple3]), + {2, bob} = lists:keyfind(Key, 2, List), + ok. + +test_keyfind_with_existing_key_and_different_length_of_tuples() -> + Key = ?ID(is), + Tuple1 = ?ID({1}), + Tuple2 = ?ID({2, bob}), + Tuple3 = ?ID({3, carol, singing, tree}), + Tuple4 = ?ID({here, it, is}), + List = ?ID([Tuple1, Tuple2, Tuple3, Tuple4]), + {here, it, is} = lists:keyfind(Key, 3, List), + ok. + +test_keyfind_with_non_existing_key() -> + Key = ?ID(4), + Tuple1 = ?ID({1, alice}), + Tuple2 = ?ID({2, bob}), + Tuple3 = ?ID({3, carol}), + List = ?ID([Tuple1, Tuple2, Tuple3]), + false = lists:keyfind(Key, 1, List), + ok. + +test_keyfind_with_empty_list() -> + Key = ?ID(3), + List = ?ID([]), + false = lists:keyfind(Key, 1, List), + ok. + +test_keyfind_with_existing_key_on_different_position() -> + Key = ?ID(1), + Tuple1 = ?ID({1, alice}), + Tuple2 = ?ID({4, bob}), + Tuple3 = ?ID({3, carol}), + List = ?ID([Tuple1, Tuple2, Tuple3]), + false = lists:keyfind(Key, 2, List), + ok. + +test_keyfind_with_invalid_input() -> + ok = + try + lists:keyfind(key, 2, not_a_list), + error + catch + error:badarg -> ok + end, + ok = + try + lists:keyfind(key, 2, [{a, 1}, {b, 2} | foo]), + error + catch + error:badarg -> ok + end. diff --git a/tests/erlang_tests/test_lists_keymember.erl b/tests/erlang_tests/test_lists_keymember.erl new file mode 100644 index 0000000000..ccc0b6079e --- /dev/null +++ b/tests/erlang_tests/test_lists_keymember.erl @@ -0,0 +1,95 @@ +% +% This file is part of AtomVM. +% +% Copyright 2025 Software Mansion +% +% 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 +% + +-module(test_lists_keymember). + +-export([start/0, id/1]). + +-define(ID(X), ?MODULE:id(X)). + +id(X) -> X. + +start() -> + ok = test_keymember_with_existing_key(), + ok = test_keymember_with_non_existing_key(), + ok = test_keymember_with_empty_list(), + ok = test_keymember_with_existing_key_on_different_position(), + ok = test_keymember_with_existing_key_and_different_length_of_tuples(), + ok = test_keymember_with_invalid_input(), + 0. + +test_keymember_with_existing_key() -> + Key = ?ID(bob), + Tuple1 = ?ID({1, alice}), + Tuple2 = ?ID({2, bob}), + Tuple3 = ?ID({3, carol}), + List = ?ID([Tuple1, Tuple2, Tuple3]), + true = lists:keymember(Key, 2, List), + ok. + +test_keymember_with_existing_key_and_different_length_of_tuples() -> + Key = ?ID(is), + Tuple1 = ?ID({1}), + Tuple2 = ?ID({2, bob}), + Tuple3 = ?ID({3, carol, singing, tree}), + Tuple4 = ?ID({here, it, is}), + List = ?ID([Tuple1, Tuple2, Tuple3, Tuple4]), + true = lists:keymember(Key, 3, List), + ok. + +test_keymember_with_non_existing_key() -> + Key = ?ID(4), + Tuple1 = ?ID({1, alice}), + Tuple2 = ?ID({2, bob}), + Tuple3 = ?ID({3, carol}), + List = ?ID([Tuple1, Tuple2, Tuple3]), + false = lists:keymember(Key, 1, List), + ok. + +test_keymember_with_empty_list() -> + Key = ?ID(3), + List = ?ID([]), + false = lists:keymember(Key, 1, List), + ok. + +test_keymember_with_existing_key_on_different_position() -> + Key = ?ID(1), + Tuple1 = ?ID({1, alice}), + Tuple2 = ?ID({4, bob}), + Tuple3 = ?ID({3, carol}), + List = ?ID([Tuple1, Tuple2, Tuple3]), + false = lists:keymember(Key, 2, List), + ok. + +test_keymember_with_invalid_input() -> + ok = + try + lists:keymember(key, 2, not_a_list), + error + catch + error:badarg -> ok + end, + ok = + try + lists:keymember(key, 2, [{a, 1}, {b, 2} | foo]), + error + catch + error:badarg -> ok + end. diff --git a/tests/erlang_tests/test_lists_member.erl b/tests/erlang_tests/test_lists_member.erl new file mode 100644 index 0000000000..8f937c5cc9 --- /dev/null +++ b/tests/erlang_tests/test_lists_member.erl @@ -0,0 +1,71 @@ +% +% This file is part of AtomVM. +% +% Copyright 2025 Software Mansion +% +% 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 +% + +-module(test_lists_member). + +-export([start/0, id/1]). + +-define(ID(X), ?MODULE:id(X)). + +id(X) -> X. + +start() -> + true = test_member_with_existing_element(), + true = test_member_with_existing_element_on_heterogenous_list(), + false = test_member_with_non_existing_element(), + false = test_member_in_empty_list(), + ok = test_member_with_invalid_input(), + 0. + +test_member_with_existing_element() -> + Element = ?ID(5), + List = ?ID([1, 2, 3, 4, 5, 6, 7, 8, 9]), + lists:member(Element, List). + +test_member_with_existing_element_on_heterogenous_list() -> + Element = ?ID(key), + List = ?ID([1, "a", 3, "b", 5, 6, "cd", 8, key]), + lists:member(Element, List). + +test_member_with_non_existing_element() -> + Element = ?ID(10), + List = ?ID([1, 2, 3, 4, 5, 6, 7, 8, 9]), + lists:member(Element, List). + +test_member_in_empty_list() -> + Element = ?ID(element), + List = ?ID([]), + lists:member(Element, List). + +test_member_with_invalid_input() -> + ok = + try + lists:member(?ID(element), ?ID(not_a_list)), + error + catch + error:badarg -> ok + end, + ok = + try + lists:member(?ID(element), ?ID([a, b | foo])), + error + catch + error:badarg -> ok + end. diff --git a/tests/test.c b/tests/test.c index f322316c36..d57d3c24b4 100644 --- a/tests/test.c +++ b/tests/test.c @@ -598,6 +598,11 @@ struct Test tests[] = { TEST_CASE(test_ets), TEST_CASE(test_node), + TEST_CASE(test_list_to_bitstring), + TEST_CASE(test_lists_member), + TEST_CASE(test_lists_keymember), + TEST_CASE(test_lists_keyfind), + // TEST CRASHES HERE: TEST_CASE(memlimit), { NULL, 0, false, false }