Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for configuring pins and width for sdmmc on ESP32
- Added support for map comprehensions
- Added USB CDC port drivers for ESP32, RP2, and STM32 platforms
- Added `erlang:term_to_binary/2`, `erlang:is_builtin/3` and `erlang:bitstring_to_list/1`

### Changed
- Updated network type db() to dbm() to reflect the actual representation of the type
Expand Down
83 changes: 77 additions & 6 deletions libs/estdlib/src/erlang.erl
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
erase/0,
erase/1,
function_exported/3,
is_builtin/3,
loaded/0,
module_loaded/1,
display/1,
Expand All @@ -71,6 +72,7 @@
binary_to_integer/1,
binary_to_integer/2,
binary_to_list/1,
bitstring_to_list/1,
atom_to_binary/1,
atom_to_binary/2,
atom_to_list/1,
Expand Down Expand Up @@ -122,6 +124,7 @@
garbage_collect/1,
binary_to_term/1,
term_to_binary/1,
term_to_binary/2,
split_binary/2,
crc32/1,
crc32/2,
Expand Down Expand Up @@ -183,7 +186,8 @@
atomvm_heap_growth_strategy/0,
stacktrace/0,
stacktrace_extrainfo/0,
raise_stacktrace/0
raise_stacktrace/0,
term_to_binary_option/0
]).

-type atom_encoding() :: latin1 | utf8 | unicode.
Expand All @@ -202,6 +206,12 @@

-type demonitor_option() :: flush | {flush, boolean()} | info | {info, boolean()}.

-type term_to_binary_option() ::
compressed
| {compressed, Level :: 0..9}
| deterministic
| {minor_version, Version :: 0..2}.

-type atomvm_heap_growth_strategy() ::
bounded_free
| minimum
Expand Down Expand Up @@ -678,6 +688,27 @@ erase(_Key) ->
function_exported(_Module, _Function, _Arity) ->
erlang:nif_error(undefined).

%%-----------------------------------------------------------------------------
%% @param Module module name
%% @param Function function name
%% @param Arity function arity
%% @returns `true' if `Module:Function/Arity' is a builtin, `false' otherwise.
%% @doc Determine whether a function is implemented natively (as a BIF or a
%% NIF) by AtomVM. As on BEAM, this reflects functions "implemented in
%% C" rather than in Erlang, but it is answered against AtomVM's own
%% registry. It therefore differs from BEAM whenever the two virtual
%% machines disagree on whether a given function is native: for
%% example `erlang:atom_to_binary/1' is native on AtomVM but
%% Erlang-implemented on BEAM, while `erlang:md5/1' is a BEAM BIF that
%% AtomVM implements in Erlang. An arity that no builtin has (including a
%% negative integer) returns `false'. Raises `badarg' if `Module' or
%% `Function' is not an atom, or `Arity' is not an integer.
%% @end
%%-----------------------------------------------------------------------------
-spec is_builtin(Module :: module(), Function :: atom(), Arity :: arity()) -> boolean().
is_builtin(_Module, _Function, _Arity) ->
erlang:nif_error(undefined).

%%-----------------------------------------------------------------------------
%% @returns list of loaded modules
%% @doc Returns all loaded modules.
Expand Down Expand Up @@ -892,6 +923,20 @@ binary_to_integer(_Binary, _Base) ->
binary_to_list(_Binary) ->
erlang:nif_error(undefined).

%%-----------------------------------------------------------------------------
%% @param Bitstring Bitstring to convert to list
%% @returns a list of bytes from the bitstring
%% @doc Convert a bitstring to a list of bytes.
%%
%% Unlike Erlang/OTP, AtomVM only supports byte-aligned bitstrings (binaries),
%% so the returned list never has a trailing bitstring and this function
%% behaves like `binary_to_list/1'.
%% @end
%%-----------------------------------------------------------------------------
-spec bitstring_to_list(Bitstring :: bitstring()) -> [byte()].
bitstring_to_list(_Bitstring) ->
erlang:nif_error(undefined).

%%-----------------------------------------------------------------------------
%% @param Atom Atom to convert
%% @returns a binary with the atom's name
Expand Down Expand Up @@ -1516,10 +1561,8 @@ garbage_collect(_Pid) ->
%%-----------------------------------------------------------------------------
%% @returns A term decoded from passed binary
%% @param Binary binary to decode
%% @doc Decode a term that was previously encodes with `term_to_binary/1'
%% @doc Decode a term that was previously encoded with `term_to_binary/1'.
%% This function should be mostly compatible with its Erlang/OTP counterpart.
%% Unlike modern Erlang/OTP, resources are currently serialized as empty
%% binaries and cannot be unserialized.
%% @end
%%-----------------------------------------------------------------------------
-spec binary_to_term(Binary :: binary()) -> any().
Expand All @@ -1531,14 +1574,42 @@ binary_to_term(_Binary) ->
%% @param Term term to encode
%% @doc Encode a term to a binary that can later be decoded with `binary_to_term/1'.
%% This function should be mostly compatible with its Erlang/OTP counterpart.
%% Unlike modern Erlang/OTP, resources are currently serialized as empty
%% binaries.
%% @end
%%-----------------------------------------------------------------------------
-spec term_to_binary(Term :: any()) -> binary().
term_to_binary(_Term) ->
erlang:nif_error(undefined).

%%-----------------------------------------------------------------------------
%% @returns A binary encoding passed term.
%% @param Term term to encode
%% @param Options encoding options
%% @doc Encode a term to a binary that can later be decoded with `binary_to_term/1'.
%%
%% The following options are accepted:
%% <ul>
%% <li>`compressed' and `{compressed, Level}' (`Level' from 0 to 9): accepted
%% for compatibility but ignored. AtomVM's encoding is always
%% uncompressed; the resulting binary is still a valid external term that
%% `binary_to_term/1' can decode.</li>
%% <li>`deterministic': accepted for compatibility. AtomVM's encoding is
%% already deterministic, so this option has no effect.</li>
%% <li>`{minor_version, Version}' (`Version' from 0 to 2): accepted for
%% compatibility. AtomVM always encodes floats using the `NEW_FLOAT_EXT'
%% representation (minor version 1 and later).</li>
%% </ul>
%%
%% Unlike Erlang/OTP, the `local' option is not supported: AtomVM never emits
%% the node-local `LOCAL_EXT' encoding. Passing `local', or any other unknown
%% option, raises a `badarg' error.
%%
%% This function should be mostly compatible with its Erlang/OTP counterpart.
%% @end
%%-----------------------------------------------------------------------------
-spec term_to_binary(Term :: any(), Options :: [term_to_binary_option()]) -> binary().
term_to_binary(_Term, _Options) ->
erlang:nif_error(undefined).

%%-----------------------------------------------------------------------------
%% @returns A tuple with two subbinaries
%% @param Bin binary to split
Expand Down
75 changes: 74 additions & 1 deletion src/libAtomVM/nifs.c
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ static term nif_erlang_port_to_list(Context *ctx, int argc, term argv[]);
static term nif_erlang_ref_to_list(Context *ctx, int argc, term argv[]);
static term nif_erlang_fun_to_list(Context *ctx, int argc, term argv[]);
static term nif_erlang_function_exported(Context *ctx, int argc, term argv[]);
static term nif_erlang_is_builtin(Context *ctx, int argc, term argv[]);
static term nif_erlang_garbage_collect(Context *ctx, int argc, term argv[]);
static term nif_erlang_group_leader(Context *ctx, int argc, term argv[]);
static term nif_erlang_get_module_info(Context *ctx, int argc, term argv[]);
Expand Down Expand Up @@ -688,6 +689,10 @@ static const struct Nif function_exported_nif = {
.base.type = NIFFunctionType,
.nif_ptr = nif_erlang_function_exported
};
static const struct Nif is_builtin_nif = {
.base.type = NIFFunctionType,
.nif_ptr = nif_erlang_is_builtin
};

static const struct Nif garbage_collect_nif = {
.base.type = NIFFunctionType,
Expand Down Expand Up @@ -3439,10 +3444,48 @@ static term nif_erlang_binary_to_term(Context *ctx, int argc, term argv[])
}
}

// AtomVM's external term encoding is already deterministic and always uses
// NEW_FLOAT_EXT (minor_version >= 1). `compressed' is accepted but ignored:
// the uncompressed encoding is a valid external term that any
// binary_to_term can read.
static bool is_valid_term_to_binary_option(Context *ctx, term opt)
{
if (term_is_atom(opt)) {
return globalcontext_is_term_equal_to_atom_string(ctx->global, opt, ATOM_STR("\xD", "deterministic"))
|| globalcontext_is_term_equal_to_atom_string(ctx->global, opt, ATOM_STR("\xA", "compressed"));
}
if (term_is_tuple(opt) && term_get_tuple_arity(opt) == 2) {
term name = term_get_tuple_element(opt, 0);
term value = term_get_tuple_element(opt, 1);
if (!term_is_integer(value)) {
return false;
}
avm_int_t int_value = term_to_int(value);
if (globalcontext_is_term_equal_to_atom_string(ctx->global, name, ATOM_STR("\xD", "minor_version"))) {
return int_value >= 0 && int_value <= 2;
}
if (globalcontext_is_term_equal_to_atom_string(ctx->global, name, ATOM_STR("\xA", "compressed"))) {
return int_value >= 0 && int_value <= 9;
}
}
return false;
}

static term nif_erlang_term_to_binary(Context *ctx, int argc, term argv[])
{
UNUSED(argc);
term t = argv[0];
if (argc == 2) {
term options = argv[1];
while (term_is_nonempty_list(options)) {
if (UNLIKELY(!is_valid_term_to_binary_option(ctx, term_get_list_head(options)))) {
RAISE_ERROR(BADARG_ATOM);
}
options = term_get_list_tail(options);
}
if (UNLIKELY(!term_is_nil(options))) {
RAISE_ERROR(BADARG_ATOM);
}
}
term ret = external_term_to_binary(ctx, t);
if (term_is_invalid_term(ret)) {
RAISE_ERROR(BADARG_ATOM);
Expand Down Expand Up @@ -4661,6 +4704,36 @@ static term nif_erlang_function_exported(Context *ctx, int argc, term argv[])
return TRUE_ATOM;
}

static term nif_erlang_is_builtin(Context *ctx, int argc, term argv[])
{
UNUSED(argc);

term module = argv[0];
term function = argv[1];
term arity_term = argv[2];

VALIDATE_VALUE(module, term_is_atom);
VALIDATE_VALUE(function, term_is_atom);
VALIDATE_VALUE(arity_term, term_is_integer);

atom_index_t module_name_ix = term_to_atom_index(module);
atom_index_t function_name_ix = term_to_atom_index(function);

avm_int_t arity = term_to_int(arity_term);

char mfa[MAX_MFA_NAME_LEN];
atom_table_write_mfa(ctx->global->atom_table, mfa, sizeof(mfa), module_name_ix, function_name_ix, arity);

// A function is a builtin iff it is implemented natively in AtomVM, i.e.
// registered as a BIF or a NIF. Unlike function_exported/3, a function
// exported by a loaded (Erlang) module is not a builtin.
if (bif_registry_get_handler(mfa) != NULL || nifs_get(mfa) != NULL) {
return TRUE_ATOM;
}

return FALSE_ATOM;
}

static term nif_erlang_garbage_collect(Context *ctx, int argc, term argv[])
{
if (argc == 0) {
Expand Down
3 changes: 3 additions & 0 deletions src/libAtomVM/nifs.gperf
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ erlang:binary_to_float/1, &binary_to_float_nif
erlang:binary_to_integer/1, &binary_to_integer_nif
erlang:binary_to_integer/2, &binary_to_integer_nif
erlang:binary_to_list/1, &binary_to_list_nif
erlang:bitstring_to_list/1, &binary_to_list_nif
erlang:binary_to_existing_atom/1, &binary_to_existing_atom_1_nif
erlang:delete_element/2, &delete_element_nif
erlang:erase/0, &erase_0_nif
Expand Down Expand Up @@ -133,6 +134,7 @@ erlang:put/2, &put_nif
erlang:binary_to_term/1, &binary_to_term_nif
erlang:binary_to_term/2, &binary_to_term_nif
erlang:term_to_binary/1, &term_to_binary_nif
erlang:term_to_binary/2, &term_to_binary_nif
erlang:split_binary/2, &split_binary_nif
erlang:throw/1, &throw_nif
erlang:raise/3, &raise_nif
Expand All @@ -142,6 +144,7 @@ erlang:port_to_list/1, &port_to_list_nif
erlang:ref_to_list/1, &ref_to_list_nif
erlang:fun_to_list/1, &fun_to_list_nif
erlang:function_exported/3, &function_exported_nif
erlang:is_builtin/3, &is_builtin_nif
erlang:!/2, &send_nif
erlang:garbage_collect/0, &garbage_collect_nif
erlang:garbage_collect/1, &garbage_collect_nif
Expand Down
4 changes: 4 additions & 0 deletions tests/erlang_tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ compile_erlang(test_abs)
compile_erlang(test_is_process_alive)
compile_erlang(test_is_not_type)
compile_erlang(test_is_bitstring_is_binary)
compile_erlang(test_is_builtin)
compile_erlang(test_badarith)
compile_erlang(test_badarith2)
compile_erlang(test_badarith3)
Expand Down Expand Up @@ -271,6 +272,7 @@ compile_erlang(test_heap_growth)
compile_erlang(test_system_flag)
compile_erlang(test_system_info)
compile_erlang(test_binary_to_term)
compile_erlang(test_bitstring_to_list)
compile_erlang(test_selective_receive)
compile_erlang(test_timeout_not_integer)
compile_erlang(test_undef)
Expand Down Expand Up @@ -793,6 +795,7 @@ set(erlang_test_beams
test_is_process_alive.beam
test_is_not_type.beam
test_is_bitstring_is_binary.beam
test_is_builtin.beam
test_badarith.beam
test_badarith2.beam
test_badarith3.beam
Expand Down Expand Up @@ -830,6 +833,7 @@ set(erlang_test_beams
test_system_flag.beam
test_system_info.beam
test_binary_to_term.beam
test_bitstring_to_list.beam
test_selective_receive.beam
test_timeout_not_integer.beam
test_undef.beam
Expand Down
22 changes: 22 additions & 0 deletions tests/erlang_tests/test_binary_to_term.erl
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,30 @@ start() ->
ok = test_safe_option(),
ok = test_invalid_export_fun_encoding(),
ok = test_atom_utf8_ext_node(),
ok = test_term_to_binary_options(),
0.

test_term_to_binary_options() ->
T = ?MODULE:id({foo, [1, 2, 3], #{a => <<"b">>}, 3.14}),
Plain = erlang:term_to_binary(T),
Plain = erlang:term_to_binary(T, []),
Plain = erlang:term_to_binary(T, [deterministic]),
Plain = erlang:term_to_binary(T, [{minor_version, 2}, deterministic]),
T = erlang:binary_to_term(erlang:term_to_binary(T, [{minor_version, 1}])),
% compressed output may use a different encoding but must round-trip
T = erlang:binary_to_term(erlang:term_to_binary(T, [compressed])),
Big = duplicate(200, T, []),
Big = erlang:binary_to_term(erlang:term_to_binary(Big, [compressed, deterministic])),
Big = erlang:binary_to_term(erlang:term_to_binary(Big, [{compressed, 6}])),
ok = expect_badarg(fun() -> erlang:term_to_binary(T, [bad_option]) end),
ok = expect_badarg(fun() -> erlang:term_to_binary(T, not_a_list) end),
ok = expect_badarg(fun() -> erlang:term_to_binary(T, [{minor_version, 17}]) end),
ok = expect_badarg(fun() -> erlang:term_to_binary(T, [{compressed, 42}]) end),
ok.

duplicate(0, _T, Acc) -> Acc;
duplicate(N, T, Acc) -> duplicate(N - 1, T, [T | Acc]).

test_reverse(T, Interop) ->
test_reverse(T, Interop, []).

Expand Down
Loading
Loading