From 8b5e0054c6e80508f43c49e49e36f88d54f86b14 Mon Sep 17 00:00:00 2001 From: Tomasz Sobkiewicz Date: Tue, 7 Jan 2025 13:58:14 +0100 Subject: [PATCH] Add ets:update_counter Signed-off-by: Tomasz Sobkiewicz --- CHANGELOG.md | 1 + libs/estdlib/src/ets.erl | 42 ++++++++++- src/libAtomVM/ets.c | 120 ++++++++++++++++++++++++++++++++ src/libAtomVM/ets.h | 5 +- src/libAtomVM/nifs.c | 38 ++++++++++ src/libAtomVM/nifs.gperf | 2 + tests/erlang_tests/test_ets.erl | 19 +++++ 7 files changed, 224 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6db6dd9ca..9e51da301 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added the ability to run beams from the CLI for Generic Unix platform (it was already possible with nodejs and emscripten). - Added support for 'erlang:--/2'. - Added support for list insertion in 'ets:insert/2'. +- Added support for list insertion in 'ets:update_counter/3' and 'ets:update_counter/4'. ### Fixed diff --git a/libs/estdlib/src/ets.erl b/libs/estdlib/src/ets.erl index 043c54cdc..f9e177aa7 100644 --- a/libs/estdlib/src/ets.erl +++ b/libs/estdlib/src/ets.erl @@ -29,7 +29,9 @@ insert/2, lookup/2, lookup_element/3, - delete/2 + delete/2, + update_counter/3, + update_counter/4 ]). -export_type([ @@ -101,3 +103,41 @@ lookup_element(_Table, _Key, _Pos) -> -spec delete(Table :: table(), Key :: term()) -> true. delete(_Table, _Key) -> erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Table a reference to the ets table +%% @param Key the key used to look up the entry expecting to contain a tuple of integers or a single integer +%% @param Params the increment value or a tuple {Pos, Increment} or {Pos, Increment, Treshold, SetValue}, +%% where Pos is an integer (1-based index) specifying the position in the tuple to increment. Value is clamped to SetValue if it exceeds Threshold after update. +%% @returns the updated element's value after performing the increment, or the default value if applicable +%% @doc Updates a counter value at Key in the table. If Params is a single integer, it increments the direct integer value at Key or the first integer in a tuple. If Params is a tuple {Pos, Increment}, it increments the integer at the specified position Pos in the tuple stored at Key. +%% @end +%%----------------------------------------------------------------------------- +-spec update_counter( + Table :: table(), + Key :: term(), + Params :: + integer() | {pos_integer(), integer()} | {pos_integer(), integer(), integer(), integer()} +) -> integer(). +update_counter(_Table, _Key, _Params) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Table a reference to the ets table +%% @param Key the key used to look up the entry expecting to contain a tuple of integers or a single integer +%% @param Params the increment value or a tuple {Pos, Increment} or {Pos, Increment, Treshold, SetValue}, +%% where Pos is an integer (1-based index) specifying the position in the tuple to increment. If after incrementation value exceeds the Treshold, it is set to SetValue. +%% @param Default the default value used if the entry at Key doesn't exist or doesn't contain a valid tuple with a sufficient size or integer at Pos +%% @returns the updated element's value after performing the increment, or the default value if applicable +%% @doc Updates a counter value at Key in the table. If Params is a single integer, it increments the direct integer value at Key or the first integer in a tuple. If Params is a tuple {Pos, Increment}, it increments the integer at the specified position Pos in the tuple stored at Key. If the needed element does not exist, uses Default value as a fallback. +%% @end +%%----------------------------------------------------------------------------- +-spec update_counter( + Table :: table(), + Key :: term(), + Params :: + integer() | {pos_integer(), integer()} | {pos_integer(), integer(), integer(), integer()}, + Default :: integer() +) -> integer(). +update_counter(_Table, _Key, _Params, _Default) -> + erlang:nif_error(undefined). diff --git a/src/libAtomVM/ets.c b/src/libAtomVM/ets.c index c381a8a13..29985991c 100644 --- a/src/libAtomVM/ets.c +++ b/src/libAtomVM/ets.c @@ -25,6 +25,7 @@ #include "ets_hashtable.h" #include "list.h" #include "memory.h" +#include "overflow_helpers.h" #include "term.h" #ifndef AVM_NO_SMP @@ -428,3 +429,122 @@ EtsErrorCode ets_delete(term name_or_ref, term key, term *ret, Context *ctx) return EtsOk; } + +static bool operation_to_tuple4(term operation, term *position, term *increment, term *threshold, term *set_value) +{ + if (term_is_integer(operation)) { + *increment = operation; + *position = term_from_int(2); + *threshold = term_invalid_term(); + *set_value = term_invalid_term(); + return true; + } + + if (!term_is_tuple(operation)) { + return false; + } + int n = term_get_tuple_arity(operation); + if (n != 2 && n != 4) { + return false; + } + + term pos = term_get_tuple_element(operation, 0); + term incr = term_get_tuple_element(operation, 1); + if (!term_is_integer(pos) || !term_is_integer(incr)) { + return false; + } + + if (n == 2) { + *position = pos; + *increment = incr; + *threshold = term_invalid_term(); + *set_value = term_invalid_term(); + return true; + } + + term tresh = term_get_tuple_element(operation, 2); + term set_val = term_get_tuple_element(operation, 3); + if (!term_is_integer(tresh) || !term_is_integer(set_val)) { + return false; + } + + *position = pos; + *increment = incr; + *threshold = tresh; + *set_value = set_val; + return true; +} + +EtsErrorCode ets_update_counter(term ref, term key, term operation, term default_value, term *ret, Context *ctx) +{ + struct EtsTable *ets_table = term_is_atom(ref) ? ets_get_table_by_name(&ctx->global->ets, ref, TableAccessWrite) : ets_get_table_by_ref(&ctx->global->ets, term_to_ref_ticks(ref), TableAccessWrite); + if (IS_NULL_PTR(ets_table)) { + return EtsTableNotFound; + } + + term list = term_invalid_term(); + EtsErrorCode result = ets_table_lookup(ets_table, key, &list, ctx); + if (result != EtsOk) { + SMP_UNLOCK(ets_table); + return result; + } + + term to_insert; + if (term_is_nil(list)) { + if (term_is_invalid_term(default_value)) { + SMP_UNLOCK(ets_table); + return EtsBadEntry; + } + to_insert = default_value; + } else { + to_insert = term_get_list_head(list); + } + + if (!(term_is_tuple(to_insert))) { + SMP_UNLOCK(ets_table); + return EtsBadEntry; + } + term position_term, increment_term, threshold_term, set_value_term; + + if (!operation_to_tuple4(operation, &position_term, &increment_term, &threshold_term, &set_value_term)) { + SMP_UNLOCK(ets_table); + return EtsBadEntry; + } + int arity = term_get_tuple_arity(to_insert); + int position = term_to_int(position_term) - 1; + if (arity <= position || position < 1) { + SMP_UNLOCK(ets_table); + return EtsBadEntry; + } + + term elem = term_get_tuple_element(to_insert, position); + if (!term_is_integer(elem)) { + SMP_UNLOCK(ets_table); + return EtsBadEntry; + } + int increment = term_to_int(increment_term); + int elem_value; + if (BUILTIN_ADD_OVERFLOW_INT(increment, term_to_int(elem), &elem_value)) { + SMP_UNLOCK(ets_table); + return EtsOverlfow; + } + if (!term_is_invalid_term(threshold_term) && !term_is_invalid_term(set_value_term)) { + int threshold = term_to_int(threshold_term); + int set_value = term_to_int(set_value_term); + + if (increment >= 0 && elem_value > threshold) { + elem_value = set_value; + } else if (increment < 0 && elem_value < threshold) { + elem_value = set_value; + } + } + + elem = term_from_int(elem_value); + term_put_tuple_element(to_insert, position, elem); + EtsErrorCode insert_result = ets_table_insert(ets_table, to_insert, ctx); + if (insert_result == EtsOk) { + *ret = elem; + } + SMP_UNLOCK(ets_table); + return insert_result; +} diff --git a/src/libAtomVM/ets.h b/src/libAtomVM/ets.h index 1d09125fa..0ea887ab4 100644 --- a/src/libAtomVM/ets.h +++ b/src/libAtomVM/ets.h @@ -57,7 +57,8 @@ typedef enum EtsErrorCode EtsBadEntry, EtsAllocationFailure, EtsEntryNotFound, - EtsBadPosition + EtsBadPosition, + EtsOverlfow } EtsErrorCode; struct Ets { @@ -77,7 +78,7 @@ EtsErrorCode ets_insert(term ref, term entry, Context *ctx); EtsErrorCode ets_lookup(term ref, term key, term *ret, Context *ctx); EtsErrorCode ets_lookup_element(term ref, term key, size_t pos, term *ret, Context *ctx); EtsErrorCode ets_delete(term ref, term key, term *ret, Context *ctx); - +EtsErrorCode ets_update_counter(term ref, term key, term value, term pos, term *ret, Context *ctx); #ifdef __cplusplus } #endif diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index dd4e35ac6..854a4429d 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -158,6 +158,7 @@ static term nif_ets_insert(Context *ctx, int argc, term argv[]); static term nif_ets_lookup(Context *ctx, int argc, term argv[]); static term nif_ets_lookup_element(Context *ctx, int argc, term argv[]); static term nif_ets_delete(Context *ctx, int argc, term argv[]); +static term nif_ets_update_counter(Context *ctx, int argc, term argv[]); static term nif_erlang_pid_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[]); @@ -697,6 +698,12 @@ static const struct Nif ets_delete_nif = .nif_ptr = nif_ets_delete }; +static const struct Nif ets_update_counter_nif = +{ + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_update_counter +}; + static const struct Nif atomvm_add_avm_pack_binary_nif = { .base.type = NIFFunctionType, @@ -3415,6 +3422,37 @@ static term nif_ets_delete(Context *ctx, int argc, term argv[]) } } +static term nif_ets_update_counter(Context *ctx, int argc, term argv[]) +{ + term ref = argv[0]; + VALIDATE_VALUE(ref, is_ets_table_id); + + term key = argv[1]; + term operation = argv[2]; + term default_value = term_invalid_term(); + if (argc == 4) { + default_value = argv[3]; + VALIDATE_VALUE(default_value, term_is_tuple); + term_put_tuple_element(default_value, 0, key); + } + term ret = term_invalid_term(); + EtsErrorCode result = ets_update_counter(ref, key, operation, default_value, &ret, ctx); + switch (result) { + case EtsOk: + return ret; + case EtsTableNotFound: + case EtsPermissionDenied: + case EtsBadEntry: + RAISE_ERROR(BADARG_ATOM); + case EtsAllocationFailure: + RAISE_ERROR(MEMORY_ATOM); + case EtsOverlfow: + RAISE_ERROR(OVERFLOW_ATOM); + default: + AVM_ABORT(); + } +} + static term nif_erts_debug_flat_size(Context *ctx, int argc, term argv[]) { UNUSED(ctx); diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index 0cc99e02c..ffd644da6 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -134,6 +134,8 @@ ets:insert/2, &ets_insert_nif ets:lookup/2, &ets_lookup_nif ets:lookup_element/3, &ets_lookup_element_nif ets:delete/2, &ets_delete_nif +ets:update_counter/3, &ets_update_counter_nif +ets:update_counter/4, &ets_update_counter_nif atomvm:add_avm_pack_binary/2, &atomvm_add_avm_pack_binary_nif atomvm:add_avm_pack_file/2, &atomvm_add_avm_pack_file_nif atomvm:close_avm_pack/2, &atomvm_close_avm_pack_nif diff --git a/tests/erlang_tests/test_ets.erl b/tests/erlang_tests/test_ets.erl index 6368c35ed..f267fc2c2 100644 --- a/tests/erlang_tests/test_ets.erl +++ b/tests/erlang_tests/test_ets.erl @@ -32,6 +32,7 @@ start() -> ok = test_public_access(), ok = test_lookup_element(), ok = test_insert_list(), + ok = test_update_counter(), 0. test_basic() -> @@ -362,3 +363,21 @@ test_insert_list() -> expect_failure(fun() -> ets:insert(Tid, [{foo, tapas} | {patat, patat}]) end), expect_failure(fun() -> ets:insert(Tid, [{}]) end), ok. + +test_update_counter() -> + Tid = ets:new(test_lookup_element, []), + true = ets:insert(Tid, {foo, 1, 2, 3}), + 3 = ets:update_counter(Tid, foo, 2), + expect_failure(fun() -> ets:update_counter(Tid, tapas, 2) end), + 5 = ets:update_counter(Tid, tapas, 2, {batat, 3}), + [] = ets:lookup(Tid, batat), + [{tapas, 5}] = ets:lookup(Tid, tapas), + 0 = ets:update_counter(Tid, foo, {3, -2}), + expect_failure(fun() -> ets:update_counter(Tid, foo, {-3, -2}) end), + expect_failure(fun() -> ets:update_counter(Tid, foo, {30, -2}) end), + expect_failure(fun() -> ets:update_counter(Tid, patatas, {3, -2}, {cow, 1}) end), + 0 = ets:update_counter(Tid, patatas, {3, -2}, {cow, 1, 2, 3}), + 0 = ets:update_counter(Tid, patatas, {3, -2, 0, 0}), + 10 = ets:update_counter(Tid, patatas, {3, 10, 10, 0}), + 0 = ets:update_counter(Tid, patatas, {3, 10, 10, 0}), + ok.