From ce7740cb9a7d8651faf35fa7d8400b3a6ea7a8a1 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 26 Aug 2023 23:14:32 +0200 Subject: [PATCH] Implement erlang:exit/2 Also add documentation for `erlang:exit/1` Fix a bug where exit reason was incorrectly a tuple for exception of class exit No longer dump crash logs when exit reason is normal Signed-off-by: Paul Guyot --- CHANGELOG.md | 4 + libs/estdlib/src/erlang.erl | 49 ++++++ src/libAtomVM/defaultatoms.c | 4 + src/libAtomVM/defaultatoms.h | 8 +- src/libAtomVM/nifs.c | 46 ++++- src/libAtomVM/nifs.gperf | 1 + src/libAtomVM/opcodesswitch.h | 10 +- tests/erlang_tests/CMakeLists.txt | 4 + tests/erlang_tests/test_exit1.erl | 167 ++++++++++++++++++ tests/erlang_tests/test_exit2.erl | 270 ++++++++++++++++++++++++++++++ tests/test.c | 2 + 11 files changed, 557 insertions(+), 8 deletions(-) create mode 100644 tests/erlang_tests/test_exit1.erl create mode 100644 tests/erlang_tests/test_exit2.erl diff --git a/CHANGELOG.md b/CHANGELOG.md index d37510fce..fccefab2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,8 +70,10 @@ functions that default to `?ATOMVM_NVS_NS` are deprecated now). - Added `unicode` module with `characters_to_list/1,2` and `characters_to_binary/1,2,3` functions - Added support for `crypto:hash/2` (ESP32 and generic_unix with openssl) - Added erlang:spawn_link/1,3 +- Added erlang:exit/2 - Added links to process_info/2 - Added lists:usort/1,2 +- Added missing documentation and specifications for available nifs ### Fixed - Fixed issue with formatting integers with io:format() on STM32 platform @@ -81,6 +83,8 @@ functions that default to `?ATOMVM_NVS_NS` are deprecated now). - Fixed numerous bugs in memory allocations that could crash the VM - Fixed SNTP support that had been broken in IDF 4.x builds - Fixed `erlang:send/2` not sending to registered name +- Fixed incorrect exit reason for exceptions of class exit +- Fixed several incorrect type specifications ### Breaking Changes diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 6f26e90ea..00d66ff01 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -85,6 +85,8 @@ monitor/2, demonitor/1, demonitor/2, + exit/1, + exit/2, open_port/2, system_time/1, group_leader/0, @@ -929,6 +931,53 @@ demonitor(_Monitor) -> demonitor(_Monitor, _Options) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Reason reason for exit +%% @doc Raises an exception of class `exit' with reason `Reason'. +%% The exception can be caught. If it is not, the process exits. +%% If the exception is not caught the signal is sent to linked processes. +%% In this case, if `Reason' is `kill', it is not transformed into `killed' and +%% linked processes can trap it (unlike `exit/2'). +%% @end +%%----------------------------------------------------------------------------- +-spec exit(Reason :: any()) -> no_return(). +exit(_Reason) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Process target process +%% @param Reason reason for exit +%% @returns `true' +%% @doc Send an exit signal to target process. +%% The consequences of the exit signal depends on `Reason', on whether +%% `Process' is self() or another process and whether target process is +%% trapping exit. +%% If `Reason' is not `kill' nor `normal': +%% +%% If `Reason' is `kill', the target process exits with `Reason' changed to +%% `killed'. +%% If `Reason' is `normal' and `Process' is not `self()': +%% +%% If `Reason' is `normal' and `Process' is `self()': +%% +%% @end +%%----------------------------------------------------------------------------- +-spec exit(Process :: pid(), Reason :: any()) -> true. +exit(_Process, _Reason) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param PortName Tuple {spawn, Name} identifying the port %% @param Options Options, meaningful for the port diff --git a/src/libAtomVM/defaultatoms.c b/src/libAtomVM/defaultatoms.c index 1eafab97b..dc0035e2c 100644 --- a/src/libAtomVM/defaultatoms.c +++ b/src/libAtomVM/defaultatoms.c @@ -145,6 +145,8 @@ static const char *const exports_atom = "\x7" "exports"; static const char *const incomplete_atom = "\xA" "incomplete"; +static const char *const kill_atom = "\x4" "kill"; +static const char *const killed_atom = "\x6" "killed"; static const char *const links_atom = "\x5" "links"; void defaultatoms_init(GlobalContext *glb) @@ -276,6 +278,8 @@ void defaultatoms_init(GlobalContext *glb) ok &= globalcontext_insert_atom(glb, incomplete_atom) == INCOMPLETE_ATOM_INDEX; + ok &= globalcontext_insert_atom(glb, kill_atom) == KILL_ATOM_INDEX; + ok &= globalcontext_insert_atom(glb, killed_atom) == KILLED_ATOM_INDEX; ok &= globalcontext_insert_atom(glb, links_atom) == LINKS_ATOM_INDEX; if (!ok) { diff --git a/src/libAtomVM/defaultatoms.h b/src/libAtomVM/defaultatoms.h index 019fc6a4b..220d72c15 100644 --- a/src/libAtomVM/defaultatoms.h +++ b/src/libAtomVM/defaultatoms.h @@ -154,9 +154,11 @@ extern "C" { #define INCOMPLETE_ATOM_INDEX 99 -#define LINKS_ATOM_INDEX 100 +#define KILL_ATOM_INDEX 100 +#define KILLED_ATOM_INDEX 101 +#define LINKS_ATOM_INDEX 102 -#define PLATFORM_ATOMS_BASE_INDEX 101 +#define PLATFORM_ATOMS_BASE_INDEX 103 #define FALSE_ATOM TERM_FROM_ATOM_INDEX(FALSE_ATOM_INDEX) #define TRUE_ATOM TERM_FROM_ATOM_INDEX(TRUE_ATOM_INDEX) @@ -285,6 +287,8 @@ extern "C" { #define INCOMPLETE_ATOM TERM_FROM_ATOM_INDEX(INCOMPLETE_ATOM_INDEX) +#define KILL_ATOM TERM_FROM_ATOM_INDEX(KILL_ATOM_INDEX) +#define KILLED_ATOM TERM_FROM_ATOM_INDEX(KILLED_ATOM_INDEX) #define LINKS_ATOM TERM_FROM_ATOM_INDEX(LINKS_ATOM_INDEX) void defaultatoms_init(GlobalContext *glb); diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 17c8ff436..3b83c8b05 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -37,6 +37,7 @@ #include "defaultatoms.h" #include "dictionary.h" #include "externalterm.h" +#include "globalcontext.h" #include "interop.h" #include "mailbox.h" #include "module.h" @@ -3132,11 +3133,48 @@ static term nif_erlang_error(Context *ctx, int argc, term argv[]) static term nif_erlang_exit(Context *ctx, int argc, term argv[]) { - UNUSED(argc); + if (argc == 1) { + term reason = argv[0]; + RAISE(LOWERCASE_EXIT_ATOM, reason); + } else { + term target_process = argv[0]; + VALIDATE_VALUE(target_process, term_is_pid); + term reason = argv[1]; + GlobalContext *glb = ctx->global; + Context *target = globalcontext_get_process_lock(glb, term_to_local_process_id(target_process)); + bool self_is_signaled = false; + if (LIKELY(target)) { + if (reason == KILL_ATOM) { + mailbox_send_term_signal(target, KillSignal, KILLED_ATOM); + self_is_signaled = target == ctx; + } else { + if (target->trap_exit) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(3)) != MEMORY_GC_OK)) { + globalcontext_get_process_unlock(glb, target); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } - term reason = argv[0]; - ctx->exit_reason = reason; - RAISE(LOWERCASE_EXIT_ATOM, reason); + term info_tuple = term_alloc_tuple(3, &ctx->heap); + term_put_tuple_element(info_tuple, 0, EXIT_ATOM); + term_put_tuple_element(info_tuple, 1, term_from_local_process_id(ctx->process_id)); + term_put_tuple_element(info_tuple, 2, reason); + mailbox_send(target, info_tuple); + } else if (ctx == target) { + mailbox_send_term_signal(target, KillSignal, reason); + self_is_signaled = target == ctx; + } else if (reason != NORMAL_ATOM){ + mailbox_send_term_signal(target, KillSignal, reason); + self_is_signaled = target == ctx; + } // else there is no effect + } + globalcontext_get_process_unlock(glb, target); + } + if (self_is_signaled) { + context_update_flags(ctx, ~NoFlags, Trap); + return term_invalid_term(); + } + return TRUE_ATOM; + } } static term nif_erlang_make_fun_3(Context *ctx, int argc, term argv[]) diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index abf357650..27b4d92bd 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -49,6 +49,7 @@ erlang:erase/1, &erase_nif erlang:error/1, &error_nif erlang:error/2, &error_nif erlang:exit/1, &exit_nif +erlang:exit/2, &exit_nif erlang:display/1, &display_nif erlang:float_to_binary/1, &float_to_binary_nif erlang:float_to_binary/2, &float_to_binary_nif diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 588b859f8..c119c079b 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -7169,9 +7169,14 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) } } - dump(ctx); + // Do not print crash dump if reason is normal. + if (ctx->x[0] != LOWERCASE_EXIT_ATOM || ctx->x[1] != NORMAL_ATOM) { + dump(ctx); + } - { + if (ctx->x[0] == LOWERCASE_EXIT_ATOM) { + ctx->exit_reason = ctx->x[1]; + } else { bool throw = ctx->x[0] == THROW_ATOM; int exit_reason_tuple_size = (throw ? TUPLE_SIZE(2) : 0) + TUPLE_SIZE(2); @@ -7184,6 +7189,7 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) term_put_tuple_element(error_term, 0, NOCATCH_ATOM); term_put_tuple_element(error_term, 1, ctx->x[1]); } else { + // error error_term = ctx->x[1]; } diff --git a/tests/erlang_tests/CMakeLists.txt b/tests/erlang_tests/CMakeLists.txt index 77c79b6cb..03ea7ef00 100644 --- a/tests/erlang_tests/CMakeLists.txt +++ b/tests/erlang_tests/CMakeLists.txt @@ -465,6 +465,8 @@ compile_erlang(link_kill_parent) compile_erlang(link_throw) compile_erlang(unlink_error) compile_erlang(trap_exit_flag) +compile_erlang(test_exit1) +compile_erlang(test_exit2) compile_erlang(test_stacktrace) compile_erlang(small_big_ext) @@ -907,6 +909,8 @@ add_custom_target(erlang_test_modules DEPENDS link_throw.beam unlink_error.beam trap_exit_flag.beam + test_exit1.beam + test_exit2.beam test_stacktrace.beam diff --git a/tests/erlang_tests/test_exit1.erl b/tests/erlang_tests/test_exit1.erl new file mode 100644 index 000000000..c0f53fc95 --- /dev/null +++ b/tests/erlang_tests/test_exit1.erl @@ -0,0 +1,167 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Paul Guyot +% +% 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_exit1). + +-export([start/0]). + +start() -> + ok = test_catch(), + ok = test_nocatch(), + ok = test_trap_kill(), + 0. + +test_catch() -> + ok = + try + exit(normal), + fail + catch + exit:normal -> ok + end, + ok = + try + exit(foo), + fail + catch + exit:foo -> ok + end, + ok = + try + exit(kill), + fail + catch + exit:kill -> ok + end, + ok. + +test_nocatch() -> + {Pid1, Ref1} = spawn_opt( + fun() -> + exit(normal) + end, + [monitor] + ), + ok = + receive + {'DOWN', Ref1, process, Pid1, normal} -> ok; + Other -> Other + after 500 -> timeout + end, + {Pid2, Ref2} = spawn_opt( + fun() -> + exit(foo) + end, + [monitor] + ), + ok = + receive + {'DOWN', Ref2, process, Pid2, foo} -> ok + after 500 -> timeout + end, + {Pid3, Ref3} = spawn_opt( + fun() -> + exit(kill) + end, + [monitor] + ), + ok = + receive + {'DOWN', Ref3, process, Pid3, kill} -> ok + after 500 -> timeout + end, + {Pid4, Ref4} = spawn_opt( + fun() -> + process_flag(trap_exit, true), + exit(normal) + end, + [monitor] + ), + ok = + receive + {'DOWN', Ref4, process, Pid4, normal} -> ok + after 500 -> timeout + end, + {Pid5, Ref5} = spawn_opt( + fun() -> + process_flag(trap_exit, true), + exit(foo) + end, + [monitor] + ), + ok = + receive + {'DOWN', Ref5, process, Pid5, foo} -> ok + after 500 -> timeout + end, + {Pid6, Ref6} = spawn_opt( + fun() -> + process_flag(trap_exit, true), + exit(kill) + end, + [monitor] + ), + ok = + receive + {'DOWN', Ref6, process, Pid6, kill} -> ok + after 500 -> timeout + end, + ok. + +test_trap_kill() -> + process_flag(trap_exit, true), + {Pid1, Ref1} = spawn_opt( + fun() -> + exit(kill) + end, + [monitor, link] + ), + ok = + receive + {'DOWN', Ref1, process, Pid1, kill} -> ok + after 500 -> timeout + end, + ok = + receive + {'EXIT', Pid1, kill} -> ok + after 500 -> timeout + end, + {Pid2, Ref2} = spawn_opt( + fun() -> + spawn_opt( + fun() -> + exit(kill) + end, + [link] + ), + % wait to be killed by link + ok = + receive + after 500 -> timeout + end + end, + [monitor] + ), + ok = + receive + {'DOWN', Ref2, process, Pid2, kill} -> ok + after 500 -> timeout + end, + ok. diff --git a/tests/erlang_tests/test_exit2.erl b/tests/erlang_tests/test_exit2.erl new file mode 100644 index 000000000..5a45c319b --- /dev/null +++ b/tests/erlang_tests/test_exit2.erl @@ -0,0 +1,270 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Paul Guyot +% +% 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_exit2). + +-export([start/0]). + +start() -> + ok = test_reason_other(), + ok = test_reason_kill(), + ok = test_reason_normal_not_self(), + ok = test_reason_normal_self(), + ok = test_exit_dead(), + 0. + +test_reason_other() -> + {Pid1, Ref1} = spawn_opt( + fun() -> + receive + after 500 -> ok + end + end, + [monitor] + ), + true = exit(Pid1, other), + ok = + receive + {'DOWN', Ref1, process, Pid1, other} -> ok + after 500 -> timeout + end, + Parent = self(), + Pid2 = spawn_opt( + fun() -> + process_flag(trap_exit, true), + Parent ! {self(), ready}, + ok = + receive + {'EXIT', _From, _Reason} = ExitMessage -> + Parent ! {self(), ExitMessage}, + ok; + Other -> + Parent ! {self(), {unexpected, Other}} + end + end, + [] + ), + ok = + receive + {Pid2, ready} -> ok + after 500 -> timeout + end, + true = exit(Pid2, other), + ok = + receive + {Pid2, {'EXIT', Parent, other}} -> ok; + Other -> Other + after 500 -> timeout + end, + ok. + +test_reason_kill() -> + {Pid1, Ref1} = spawn_opt( + fun() -> + receive + after 500 -> ok + end + end, + [monitor] + ), + true = exit(Pid1, kill), + ok = + receive + {'DOWN', Ref1, process, Pid1, killed} -> ok + after 500 -> timeout + end, + Parent = self(), + {Pid2, Ref2} = spawn_opt( + fun() -> + process_flag(trap_exit, true), + Parent ! {self(), ready}, + receive + after 500 -> ok + end + end, + [monitor] + ), + {Pid3, Ref3} = spawn_opt( + fun() -> + link(Pid2), + Parent ! {self(), ready}, + receive + after 500 -> ok + end + end, + [monitor] + ), + {Pid4, Ref4} = spawn_opt( + fun() -> + link(Pid2), + process_flag(trap_exit, true), + Parent ! {self(), ready}, + receive + {'EXIT', _From, _Reason} = ExitMessage -> Parent ! {self(), ExitMessage} + after 500 -> ok + end + end, + [monitor] + ), + ok = + receive + {Pid2, ready} -> ok + after 500 -> timeout + end, + ok = + receive + {Pid3, ready} -> ok + after 500 -> timeout + end, + ok = + receive + {Pid4, ready} -> ok + after 500 -> timeout + end, + true = exit(Pid2, kill), + ok = + receive + {'DOWN', Ref2, process, Pid2, killed} -> ok + after 500 -> timeout + end, + ok = + receive + {'DOWN', Ref3, process, Pid3, killed} -> ok + after 500 -> timeout + end, + ok = + receive + {'DOWN', Ref4, process, Pid4, normal} -> ok + after 500 -> timeout + end, + Pid2 = + receive + {Pid4, {'EXIT', Pid, killed}} -> Pid + after 500 -> timeout + end, + ok. + +test_reason_normal_not_self() -> + {Pid1, Ref1} = spawn_opt( + fun() -> + receive + {Caller, ping} -> Caller ! {self(), pong} + after 500 -> ok + end + end, + [monitor] + ), + true = exit(Pid1, normal), + Pid1 ! {self(), ping}, + ok = + receive + {Pid1, pong} -> ok + after 500 -> timeout + end, + ok = + receive + {'DOWN', Ref1, process, Pid1, normal} -> ok + after 500 -> timeout + end, + Parent = self(), + Pid2 = spawn_opt( + fun() -> + process_flag(trap_exit, true), + Parent ! {self(), ready}, + ok = + receive + {'EXIT', _From, _Reason} = ExitMessage -> + Parent ! {self(), ExitMessage}, + ok; + Other -> + Parent ! {self(), {unexpected, Other}} + end + end, + [] + ), + ok = + receive + {Pid2, ready} -> ok + after 500 -> timeout + end, + true = exit(Pid2, normal), + ok = + receive + {Pid2, {'EXIT', Parent, normal}} -> ok; + Other -> Other + after 500 -> timeout + end, + ok. + +test_reason_normal_self() -> + Parent = self(), + {Pid1, Ref1} = spawn_opt( + fun() -> + exit(self(), normal), + Parent ! {self(), unexpected}, + exit(unexpected) + end, + [monitor] + ), + ok = + receive + {'DOWN', Ref1, process, Pid1, normal} -> ok + after 500 -> timeout + end, + {Pid2, Ref2} = spawn_opt( + fun() -> + process_flag(trap_exit, true), + exit(self(), normal), + ok = + receive + {'EXIT', _From, _Reason} = ExitMessage -> + Parent ! {self(), ExitMessage}, + ok; + Other -> + Parent ! {self(), {unexpected, Other}} + end, + exit(expected) + end, + [monitor] + ), + ok = + receive + {Pid2, {'EXIT', Pid2, normal}} -> ok; + Other -> Other + after 500 -> timeout + end, + ok = + receive + {'DOWN', Ref2, process, Pid2, expected} -> ok + after 500 -> timeout + end, + ok. + +test_exit_dead() -> + {Pid1, Ref1} = spawn_opt(fun() -> ok end, [monitor]), + ok = + receive + {'DOWN', Ref1, process, Pid1, normal} -> ok + after 500 -> timeout + end, + true = exit(Pid1, normal), + true = exit(Pid1, kill), + true = exit(Pid1, other), + ok. diff --git a/tests/test.c b/tests/test.c index fa4b52935..ba4f93a1e 100644 --- a/tests/test.c +++ b/tests/test.c @@ -503,6 +503,8 @@ struct Test tests[] = { TEST_CASE_EXPECTED(link_throw, 1), TEST_CASE_EXPECTED(unlink_error, 1), TEST_CASE(trap_exit_flag), + TEST_CASE(test_exit1), + TEST_CASE(test_exit2), TEST_CASE_COND(test_stacktrace, 0, SKIP_STACKTRACES), TEST_CASE(small_big_ext), TEST_CASE(test_crypto),