diff --git a/libs/estdlib/src/ets.erl b/libs/estdlib/src/ets.erl index 5c3144db3..9b608ff4f 100644 --- a/libs/estdlib/src/ets.erl +++ b/libs/estdlib/src/ets.erl @@ -43,7 +43,7 @@ ]). -opaque table() :: atom | reference(). --type table_type() :: set. +-type table_type() :: set | duplicate_bag. -type access_type() :: private | protected | public. -type option() :: table_type() | {keypos, non_neg_integer()} | access_type(). -type options() :: [option()]. diff --git a/src/libAtomVM/defaultatoms.def b/src/libAtomVM/defaultatoms.def index fa5150a07..e9276685f 100644 --- a/src/libAtomVM/defaultatoms.def +++ b/src/libAtomVM/defaultatoms.def @@ -188,3 +188,9 @@ X(BREAK_IGNORED_ATOM, "\xD", "break_ignored") X(SCOPE_ATOM, "\x5", "scope") X(NOMATCH_ATOM, "\x7", "nomatch") +X(NAMED_TABLE_ATOM, "\xB", "named_table") +X(KEYPOS_ATOM, "\x6", "keypos") +X(PRIVATE_ATOM, "\x7", "private") +X(PUBLIC_ATOM, "\x6", "public") +X(DUPLICATE_BAG_ATOM, "\xd", "duplicate_bag") +X(SET_ATOM, "\x3", "set") diff --git a/src/libAtomVM/ets.c b/src/libAtomVM/ets.c index 36f51e83b..0f99058f0 100644 --- a/src/libAtomVM/ets.c +++ b/src/libAtomVM/ets.c @@ -26,7 +26,10 @@ #include "list.h" #include "memory.h" #include "overflow_helpers.h" +#include "smp.h" #include "term.h" +#include "utils.h" +#include #define ETS_NO_INDEX SIZE_MAX #define ETS_ANY_PROCESS -1 @@ -55,7 +58,7 @@ struct EtsTable term name; bool is_named; int32_t owner_process_id; - size_t keypos; + size_t key_index; EtsTableType table_type; // In the future, we might support rb-trees for sorted sets // For this MVP, we only support unsorted sets @@ -73,7 +76,7 @@ typedef enum TableAccessType TableAccessWrite } TableAccessType; -static void ets_delete_all_tables(struct Ets *ets, GlobalContext *global); +static void delete_all_tables(struct Ets *ets, GlobalContext *global); static void ets_add_table(struct Ets *ets, struct EtsTable *ets_table) { @@ -84,7 +87,7 @@ static void ets_add_table(struct Ets *ets, struct EtsTable *ets_table) synclist_unlock(&ets->ets_tables); } -static struct EtsTable *ets_acquire_table(struct Ets *ets, int32_t process_id, term name_or_ref, TableAccessType requested_access_type) +static struct EtsTable *acquire_table(struct Ets *ets, int32_t process_id, term name_or_ref, TableAccessType requested_access_type) { uint64_t ref = 0; term name = term_invalid_term(); @@ -132,14 +135,14 @@ void ets_init(struct Ets *ets) void ets_destroy(struct Ets *ets, GlobalContext *global) { - ets_delete_all_tables(ets, global); + delete_all_tables(ets, global); synclist_destroy(&ets->ets_tables); } EtsErrorCode ets_create_table_maybe_gc(term name, bool is_named, EtsTableType table_type, EtsAccessType access_type, size_t keypos, term *ret, Context *ctx) { if (is_named) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ETS_ANY_PROCESS, name, TableAccessNone); + struct EtsTable *ets_table = acquire_table(&ctx->global->ets, ETS_ANY_PROCESS, name, TableAccessNone); if (ets_table != NULL) { return EtsTableNameInUse; } @@ -169,7 +172,7 @@ EtsErrorCode ets_create_table_maybe_gc(term name, bool is_named, EtsTableType ta uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); ets_table->ref_ticks = ref_ticks; - ets_table->keypos = keypos; + ets_table->key_index = keypos; #ifndef AVM_NO_SMP ets_table->lock = smp_rwlock_create(); @@ -206,7 +209,7 @@ static void ets_table_destroy(struct EtsTable *table, GlobalContext *global) typedef bool (*ets_table_filter_pred)(struct EtsTable *table, void *data); -static void ets_delete_tables_internal(struct Ets *ets, ets_table_filter_pred pred, void *data, GlobalContext *global) +static void delete_tables_pred(struct Ets *ets, ets_table_filter_pred pred, void *data, GlobalContext *global) { struct ListHead *ets_tables_list = synclist_wrlock(&ets->ets_tables); struct ListHead *item; @@ -229,7 +232,7 @@ static bool equal_process_id_pred(struct EtsTable *table, void *data) void ets_delete_owned_tables(struct Ets *ets, int32_t process_id, GlobalContext *global) { - ets_delete_tables_internal(ets, equal_process_id_pred, &process_id, global); + delete_tables_pred(ets, equal_process_id_pred, &process_id, global); } static bool true_pred(struct EtsTable *table, void *data) @@ -240,33 +243,80 @@ static bool true_pred(struct EtsTable *table, void *data) return true; } -static void ets_delete_all_tables(struct Ets *ets, GlobalContext *global) +static void delete_all_tables(struct Ets *ets, GlobalContext *global) { - ets_delete_tables_internal(ets, true_pred, NULL, global); + delete_tables_pred(ets, true_pred, NULL, global); } -static EtsErrorCode ets_table_insert(struct EtsTable *ets_table, term entry, Context *ctx) +static inline bool has_key_at(term tuple, size_t key_index) { - size_t keypos = ets_table->keypos; + return key_index < (size_t) term_get_tuple_arity(tuple); +} + +static struct HNode *tuple_to_insertion_node(struct EtsTable *ets_table, term tuple, GlobalContext *global) +{ + bool is_duplicate_bag = ets_table->table_type == EtsTableDuplicateBag; + size_t key_index = ets_table->key_index; + + if (is_duplicate_bag) { + term key = term_get_tuple_element(tuple, key_index); + term old_tuples = ets_hashtable_lookup(ets_table->hashtable, key, global); + assert(term_is_list(old_tuples)); + bool is_new = term_is_nil(old_tuples); + return ets_hashtable_new_node_from_list(is_new ? term_nil() : old_tuples, tuple, key_index); + } + return ets_hashtable_new_node(tuple, key_index); +} + +static struct HNode **list_to_insertion_nodes(struct EtsTable *ets_table, term list, size_t size, GlobalContext *global) +{ + size_t last_i = 0; + struct HNode **nodes = malloc(size * sizeof(struct HNode *)); + if (IS_NULL_PTR(nodes)) { + goto oom; + } - if ((size_t) term_get_tuple_arity(entry) < keypos + 1) { + while (term_is_nonempty_list(list)) { + term tuple = term_get_list_head(list); + nodes[last_i] = tuple_to_insertion_node(ets_table, tuple, global); + if (IS_NULL_PTR(nodes[last_i])) { + goto oom; + } + ++last_i; + list = term_get_list_tail(list); + } + return nodes; +oom: + // skip last node, it's NULL + for (size_t i = 0; i < last_i; ++i) { + ets_hashtable_free_node(nodes[i], global); + } + free(nodes); + return NULL; +} + +static EtsErrorCode insert_tuple(struct EtsTable *ets_table, term tuple, GlobalContext *global) +{ + size_t key_index = ets_table->key_index; + if (UNLIKELY(!has_key_at(tuple, key_index))) { return EtsBadEntry; } - struct HNode *new_node = ets_hashtable_new_node(entry, keypos); - if (IS_NULL_PTR(new_node)) { + struct HNode *node = tuple_to_insertion_node(ets_table, tuple, global); + if (IS_NULL_PTR(node)) { return EtsAllocationFailure; } - EtsHashtableStatus res = ets_hashtable_insert(ets_table->hashtable, new_node, EtsHashtableAllowOverwrite, ctx->global); + EtsHashtableStatus res = ets_hashtable_insert(ets_table->hashtable, node, EtsHashtableAllowOverwrite, global); if (UNLIKELY(res != EtsHashtableOk)) { + ets_hashtable_free_node(node, global); return EtsAllocationFailure; } return EtsOk; } -static EtsErrorCode ets_table_insert_list(struct EtsTable *ets_table, term list, Context *ctx) +static EtsErrorCode insert_tuple_list(struct EtsTable *ets_table, term list, GlobalContext *global) { term iter = list; size_t size = 0; @@ -274,37 +324,24 @@ static EtsErrorCode ets_table_insert_list(struct EtsTable *ets_table, term list, while (term_is_nonempty_list(iter)) { term tuple = term_get_list_head(iter); iter = term_get_list_tail(iter); - if (!term_is_tuple(tuple) || (size_t) term_get_tuple_arity(tuple) < (ets_table->keypos + 1)) { + if (UNLIKELY(!term_is_tuple(tuple) || !has_key_at(tuple, ets_table->key_index))) { return EtsBadEntry; } ++size; } - if (!term_is_nil(iter)) { + bool improper = !term_is_nil(iter); + if (UNLIKELY(improper)) { return EtsBadEntry; } - struct HNode **nodes = malloc(size * sizeof(struct HNode *)); + struct HNode **nodes = list_to_insertion_nodes(ets_table, list, size, global); if (IS_NULL_PTR(nodes)) { return EtsAllocationFailure; } - size_t i = 0; - while (term_is_nonempty_list(list)) { - term tuple = term_get_list_head(list); - nodes[i] = ets_hashtable_new_node(tuple, ets_table->keypos); - if (IS_NULL_PTR(nodes[i])) { - for (size_t it = 0; it < i; ++it) { - ets_hashtable_free_node(nodes[it], ctx->global); - } - free(nodes); - return EtsAllocationFailure; - } - ++i; - list = term_get_list_tail(list); - } - for (size_t i = 0; i < size; ++i) { - EtsHashtableStatus res = ets_hashtable_insert(ets_table->hashtable, nodes[i], EtsHashtableAllowOverwrite, ctx->global); + EtsHashtableStatus res = ets_hashtable_insert(ets_table->hashtable, nodes[i], EtsHashtableAllowOverwrite, global); + // insert can fail when comparing keys assert(res == EtsHashtableOk); } @@ -314,16 +351,16 @@ static EtsErrorCode ets_table_insert_list(struct EtsTable *ets_table, term list, EtsErrorCode ets_insert(term name_or_ref, term entry, Context *ctx) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessWrite); + struct EtsTable *ets_table = acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessWrite); if (IS_NULL_PTR(ets_table)) { return EtsBadAccess; } EtsErrorCode result; if (term_is_tuple(entry)) { - result = ets_table_insert(ets_table, entry, ctx); + result = insert_tuple(ets_table, entry, ctx->global); } else if (term_is_list(entry)) { - result = ets_table_insert_list(ets_table, entry, ctx); + result = insert_tuple_list(ets_table, entry, ctx->global); } else { result = EtsBadEntry; } @@ -333,20 +370,53 @@ EtsErrorCode ets_insert(term name_or_ref, term entry, Context *ctx) return result; } -static EtsErrorCode ets_table_lookup_maybe_gc(struct EtsTable *ets_table, term key, term *ret, Context *ctx, int num_roots, term *roots) +static EtsErrorCode lookup_maybe_gc(struct EtsTable *ets_table, term key, size_t index, term *ret, Context *ctx, int num_roots, term *roots) { - term res = ets_hashtable_lookup(ets_table->hashtable, key, ets_table->keypos, ctx->global); + bool is_duplicate_bag = ets_table->table_type == EtsTableDuplicateBag; + bool lookup_element = index != ETS_NO_INDEX; + term ets_entry = ets_hashtable_lookup(ets_table->hashtable, key, ctx->global); - if (term_is_nil(res)) { + if (term_is_nil(ets_entry)) { *ret = term_nil(); - } else { + } else if (is_duplicate_bag) { + assert(term_is_list(ets_entry)); + // for tuple list and it reversed version - we don't want to copy terms in the loop + int _proper; + size_t n = term_list_length(ets_entry, &_proper); + size_t size = LIST_SIZE(n, 1) + memory_estimate_usage(ets_entry); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, size, num_roots, roots, MEMORY_CAN_SHRINK))) { + return EtsAllocationFailure; + } + term tuples = memory_copy_term_tree(&ctx->heap, ets_entry); + // lookup returns in insertion order + // TODO: store it in correct order? + term reversed = term_nil(); + while (term_is_nonempty_list(tuples)) { + term elem = term_get_list_head(tuples); + if (lookup_element) { + term tuple = elem; + if (UNLIKELY(!has_key_at(tuple, index))) { + return EtsBadPosition; + } + elem = term_get_tuple_element(tuple, index); + } + reversed = term_list_prepend(elem, reversed, &ctx->heap); + tuples = term_get_list_tail(tuples); + } - size_t size = (size_t) memory_estimate_usage(res); - // allocate [object] + *ret = reversed; + } else { + if (lookup_element) { + if (UNLIKELY(!has_key_at(ets_entry, index))) { + return EtsBadPosition; + } + ets_entry = term_get_tuple_element(ets_entry, index); + } + size_t size = (size_t) memory_estimate_usage(ets_entry); if (UNLIKELY(memory_ensure_free_with_roots(ctx, size + CONS_SIZE, num_roots, roots, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { return EtsAllocationFailure; } - term new_res = memory_copy_term_tree(&ctx->heap, res); + term new_res = memory_copy_term_tree(&ctx->heap, ets_entry); *ret = term_list_prepend(new_res, term_nil(), &ctx->heap); } @@ -355,48 +425,36 @@ static EtsErrorCode ets_table_lookup_maybe_gc(struct EtsTable *ets_table, term k EtsErrorCode ets_lookup_maybe_gc(term name_or_ref, term key, term *ret, Context *ctx) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessRead); + struct EtsTable *ets_table = acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessRead); if (IS_NULL_PTR(ets_table)) { return EtsBadAccess; } - EtsErrorCode result = ets_table_lookup_maybe_gc(ets_table, key, ret, ctx, 0, NULL); + EtsErrorCode result = lookup_maybe_gc(ets_table, key, ETS_NO_INDEX, ret, ctx, 0, NULL); SMP_UNLOCK(ets_table); return result; } -EtsErrorCode ets_lookup_element_maybe_gc(term name_or_ref, term key, size_t pos, term *ret, Context *ctx) +EtsErrorCode ets_lookup_element_maybe_gc(term name_or_ref, term key, size_t key_index, term *ret, Context *ctx) { - if (UNLIKELY(pos == 0)) { - return EtsBadPosition; - } - - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessRead); + struct EtsTable *ets_table = acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessRead); if (IS_NULL_PTR(ets_table)) { return EtsBadAccess; } + bool is_duplicate_bag = ets_table->table_type == EtsTableDuplicateBag; - term entry = ets_hashtable_lookup(ets_table->hashtable, key, ets_table->keypos, ctx->global); - - if (term_is_nil(entry)) { + term entry; + EtsErrorCode result = lookup_maybe_gc(ets_table, key, key_index, &entry, ctx, 0, NULL); + if (result != EtsOk) { SMP_UNLOCK(ets_table); - return EtsEntryNotFound; - } - - if ((size_t) term_get_tuple_arity(entry) < pos) { - SMP_UNLOCK(ets_table); - return EtsBadPosition; + return result; } - - term res = term_get_tuple_element(entry, pos - 1); - size_t size = (size_t) memory_estimate_usage(res); - // allocate [object] - if (UNLIKELY(memory_ensure_free_opt(ctx, size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (term_is_nil(entry)) { SMP_UNLOCK(ets_table); - return EtsAllocationFailure; + return EtsEntryNotFound; } - *ret = memory_copy_term_tree(&ctx->heap, res); + *ret = is_duplicate_bag ? entry : term_get_list_head(entry); SMP_UNLOCK(ets_table); return EtsOk; @@ -404,7 +462,7 @@ EtsErrorCode ets_lookup_element_maybe_gc(term name_or_ref, term key, size_t pos, EtsErrorCode ets_drop_table(term name_or_ref, term *ret, Context *ctx) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessWrite); + struct EtsTable *ets_table = acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessWrite); if (IS_NULL_PTR(ets_table)) { return EtsBadAccess; } @@ -422,12 +480,12 @@ EtsErrorCode ets_drop_table(term name_or_ref, term *ret, Context *ctx) EtsErrorCode ets_delete(term name_or_ref, term key, term *ret, Context *ctx) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessWrite); + struct EtsTable *ets_table = acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessWrite); if (IS_NULL_PTR(ets_table)) { return EtsBadAccess; } - bool _found = ets_hashtable_remove(ets_table->hashtable, key, ets_table->keypos, ctx->global); + bool _found = ets_hashtable_remove(ets_table->hashtable, key, ctx->global); UNUSED(_found); SMP_UNLOCK(ets_table); @@ -482,17 +540,21 @@ static bool operation_to_tuple4(term operation, size_t default_pos, term *positi EtsErrorCode ets_update_counter_maybe_gc(term ref, term key, term operation, term default_value, term *ret, Context *ctx) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, ref, TableAccessWrite); + struct EtsTable *ets_table = acquire_table(&ctx->global->ets, ctx->process_id, ref, TableAccessWrite); if (IS_NULL_PTR(ets_table)) { return EtsBadAccess; } + if (UNLIKELY(ets_table->table_type == EtsTableDuplicateBag)) { + SMP_UNLOCK(ets_table); + return EtsBadEntry; + } // do not use an invalid term as a root term safe_default_value = term_is_invalid_term(default_value) ? term_nil() : default_value; term roots[] = { key, operation, safe_default_value }; term list; - EtsErrorCode result = ets_table_lookup_maybe_gc(ets_table, key, &list, ctx, 3, roots); + EtsErrorCode result = lookup_maybe_gc(ets_table, key, ETS_NO_INDEX, &list, ctx, 3, roots); if (UNLIKELY(result != EtsOk)) { SMP_UNLOCK(ets_table); return result; @@ -521,21 +583,20 @@ EtsErrorCode ets_update_counter_maybe_gc(term ref, term key, term operation, ter term increment_term; term threshold_term; term set_value_term; - // +1 to position, +1 to elem after key - size_t default_pos = (ets_table->keypos + 1) + 1; + // +1 for index -> position, +1 for targeting elem after key + size_t default_pos = (ets_table->key_index + 1) + 1; if (UNLIKELY(!operation_to_tuple4(operation, default_pos, &position_term, &increment_term, &threshold_term, &set_value_term))) { SMP_UNLOCK(ets_table); return EtsBadEntry; } - int arity = term_get_tuple_arity(to_insert); - avm_int_t position = term_to_int(position_term) - 1; - if (UNLIKELY(arity <= position || position < 1)) { + size_t elem_index = term_to_int(position_term) - 1; + if (UNLIKELY(!has_key_at(to_insert, elem_index) || elem_index == ets_table->key_index)) { SMP_UNLOCK(ets_table); return EtsBadEntry; } - term elem = term_get_tuple_element(to_insert, position); + term elem = term_get_tuple_element(to_insert, elem_index); if (UNLIKELY(!term_is_integer(elem))) { SMP_UNLOCK(ets_table); return EtsBadEntry; @@ -558,8 +619,8 @@ EtsErrorCode ets_update_counter_maybe_gc(term ref, term key, term operation, ter } term final_value = term_from_int(elem_value); - term_put_tuple_element(to_insert, position, final_value); - EtsErrorCode insert_result = ets_table_insert(ets_table, to_insert, ctx); + term_put_tuple_element(to_insert, elem_index, final_value); + EtsErrorCode insert_result = insert_tuple(ets_table, to_insert, ctx->global); if (insert_result == EtsOk) { *ret = final_value; } diff --git a/src/libAtomVM/ets_hashtable.c b/src/libAtomVM/ets_hashtable.c index d162d9c49..519e33822 100644 --- a/src/libAtomVM/ets_hashtable.c +++ b/src/libAtomVM/ets_hashtable.c @@ -20,6 +20,7 @@ #include "ets_hashtable.h" +#include "memory.h" #include "smp.h" #include "term.h" #include "utils.h" @@ -87,22 +88,22 @@ static void print_info(struct EtsHashTable *hash_table) } #endif -struct HNode *ets_hashtable_new_node(term entry, int keypos) +struct HNode *ets_hashtable_new_node(term tuple, int key_index) { + assert(term_is_tuple(tuple)); + assert(term_get_tuple_arity(tuple) >= key_index); struct HNode *new_node = malloc(sizeof(struct HNode)); if (IS_NULL_PTR(new_node)) { goto cleanup; } - size_t size = memory_estimate_usage(entry); + size_t size = memory_estimate_usage(tuple); if (UNLIKELY(memory_init_heap(&new_node->heap, size) != MEMORY_GC_OK)) { goto cleanup; } - term new_entry = memory_copy_term_tree(&new_node->heap, entry); - assert(term_is_tuple(new_entry)); - assert(term_get_tuple_arity(new_entry) >= keypos); - term key = term_get_tuple_element(new_entry, keypos); + term new_entry = memory_copy_term_tree(&new_node->heap, tuple); + term key = term_get_tuple_element(new_entry, key_index); new_node->next = NULL; new_node->key = key; @@ -115,6 +116,39 @@ struct HNode *ets_hashtable_new_node(term entry, int keypos) return NULL; } +// TODO: create list elsewhere, by copying terms from orig heap, appending new copied tuple and using ets_hashtable_new_node +struct HNode *ets_hashtable_new_node_from_list(term old_tuples, term tuple, size_t key_index) +{ + assert(term_is_tuple(tuple)); + assert((size_t) term_get_tuple_arity(tuple) >= key_index); + assert(term_is_list(old_tuples)); + + struct HNode *new_node = malloc(sizeof(struct HNode)); + if (IS_NULL_PTR(new_node)) { + goto oom; + } + + size_t old_list_size = memory_estimate_usage(old_tuples); + size_t new_tuple_size = memory_estimate_usage(tuple); + if (UNLIKELY(memory_init_heap(&new_node->heap, old_list_size + new_tuple_size + CONS_SIZE) != MEMORY_GC_OK)) { + goto oom; + } + term ets_list = memory_copy_term_tree(&new_node->heap, old_tuples); + term ets_tuple = memory_copy_term_tree(&new_node->heap, tuple); + + term new_key = term_get_tuple_element(ets_tuple, key_index); + ets_list = term_list_prepend(ets_tuple, ets_list, &new_node->heap); + + new_node->next = NULL; + new_node->key = new_key; + new_node->entry = ets_list; + return new_node; + +oom: + free(new_node); + return NULL; +} + EtsHashtableStatus ets_hashtable_insert(struct EtsHashTable *hash_table, struct HNode *new_node, EtsHashtableOptions opts, GlobalContext *global) { term key = new_node->key; @@ -167,15 +201,14 @@ EtsHashtableStatus ets_hashtable_insert(struct EtsHashTable *hash_table, struct return EtsHashtableOk; } -term ets_hashtable_lookup(struct EtsHashTable *hash_table, term key, size_t keypos, GlobalContext *global) +term ets_hashtable_lookup(struct EtsHashTable *hash_table, term key, GlobalContext *global) { uint32_t hash = hash_term(key, global); uint32_t index = hash % hash_table->capacity; const struct HNode *node = hash_table->buckets[index]; while (node) { - term key_to_compare = term_get_tuple_element(node->entry, keypos); - if (term_compare(key, key_to_compare, TermCompareExact, global) == TermEquals) { + if (term_compare(key, node->key, TermCompareExact, global) == TermEquals) { return node->entry; } node = node->next; @@ -184,7 +217,7 @@ term ets_hashtable_lookup(struct EtsHashTable *hash_table, term key, size_t keyp return term_nil(); } -bool ets_hashtable_remove(struct EtsHashTable *hash_table, term key, size_t keypos, GlobalContext *global) +bool ets_hashtable_remove(struct EtsHashTable *hash_table, term key, GlobalContext *global) { uint32_t hash = hash_term(key, global); uint32_t index = hash % hash_table->capacity; @@ -192,8 +225,7 @@ bool ets_hashtable_remove(struct EtsHashTable *hash_table, term key, size_t keyp struct HNode *node = hash_table->buckets[index]; struct HNode *prev_node = NULL; while (node) { - term key_to_compare = term_get_tuple_element(node->entry, keypos); - if (term_compare(key, key_to_compare, TermCompareExact, global) == TermEquals) { + if (term_compare(key, node->key, TermCompareExact, global) == TermEquals) { struct HNode *next_node = node->next; ets_hashtable_free_node(node, global); if (prev_node != NULL) { diff --git a/src/libAtomVM/ets_hashtable.h b/src/libAtomVM/ets_hashtable.h index cab87a6f6..52b8b2add 100644 --- a/src/libAtomVM/ets_hashtable.h +++ b/src/libAtomVM/ets_hashtable.h @@ -39,7 +39,7 @@ struct EtsHashTable typedef enum EtsHashtableOptions { - EtsHashtableAllowOverwrite = 1 + EtsHashtableAllowOverwrite = (1 << 0), } EtsHashtableOptions; typedef enum EtsHashtableStatus @@ -52,12 +52,14 @@ typedef enum EtsHashtableStatus struct EtsHashTable *ets_hashtable_new(); void ets_hashtable_destroy(struct EtsHashTable *hash_table, GlobalContext *global); -EtsHashtableStatus ets_hashtable_insert(struct EtsHashTable *hash_table, struct HNode *new_node, EtsHashtableOptions opts, GlobalContext *global); -term ets_hashtable_lookup(struct EtsHashTable *hash_table, term key, size_t keypos, GlobalContext *global); -bool ets_hashtable_remove(struct EtsHashTable *hash_table, term key, size_t keypos, GlobalContext *global); -struct HNode *ets_hashtable_new_node(term entry, int keypos); +struct HNode *ets_hashtable_new_node(term entry, int key_index); +struct HNode *ets_hashtable_new_node_from_list(term old_tuples_or_tuple, term new_tuple, size_t key_index); void ets_hashtable_free_node(struct HNode *node, GlobalContext *global); +EtsHashtableStatus ets_hashtable_insert(struct EtsHashTable *hash_table, struct HNode *new_node, EtsHashtableOptions opts, GlobalContext *global); +term ets_hashtable_lookup(struct EtsHashTable *hash_table, term key, GlobalContext *global); +bool ets_hashtable_remove(struct EtsHashTable *hash_table, term key, GlobalContext *global); + #ifdef __cplusplus } #endif diff --git a/src/libAtomVM/memory.h b/src/libAtomVM/memory.h index e53ef5c9d..a6edd7b23 100644 --- a/src/libAtomVM/memory.h +++ b/src/libAtomVM/memory.h @@ -23,6 +23,7 @@ // #define DEBUG_HEAP_ALLOC +#include #include #include #ifdef DEBUG_HEAP_ALLOC diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 95968d2e7..e9d50d8b7 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -3539,35 +3539,97 @@ static term nif_erlang_raise(Context *ctx, int argc, term argv[]) return term_invalid_term(); } +static inline term get_option_key(term option, term *maybe_value) +{ + *maybe_value = term_invalid_term(); + if (LIKELY(term_is_atom(option))) { + return option; + } + + bool is_tuple = term_is_tuple(option) && term_get_tuple_arity(option) == 2; + if (UNLIKELY(!is_tuple)) { + return term_invalid_term(); + } + term first = term_get_tuple_element(option, 0); + term second = term_get_tuple_element(option, 1); + if (UNLIKELY(!term_is_atom(first))) { + return term_invalid_term(); + } + + *maybe_value = second; + return first; +} + static term nif_ets_new(Context *ctx, int argc, term argv[]) { UNUSED(argc); - term name = argv[0]; - VALIDATE_VALUE(name, term_is_atom); - term options = argv[1]; + VALIDATE_VALUE(name, term_is_atom); VALIDATE_VALUE(options, term_is_list); - term is_named = interop_kv_get_value_default(options, ATOM_STR("\xB", "named_table"), FALSE_ATOM, ctx->global); - term keypos = interop_kv_get_value_default(options, ATOM_STR("\x6", "keypos"), term_from_int(1), ctx->global); + bool is_named = false; + bool private = false; + bool public = false; + bool is_duplicate_bag = false; + size_t key_index = 0; - if (term_to_int(keypos) < 1) { - RAISE_ERROR(BADARG_ATOM); - } + while (term_is_nonempty_list(options)) { + term head = term_get_list_head(options); + term value; + term key_atom = get_option_key(head, &value); + VALIDATE_VALUE(key_atom, term_is_atom); - term private = interop_kv_get_value(options, ATOM_STR("\x7", "private"), ctx->global); - term public = interop_kv_get_value(options, ATOM_STR("\x6", "public"), ctx->global); + switch (key_atom) { + case NAMED_TABLE_ATOM: { + is_named = true; + break; + } + case PRIVATE_ATOM: { + private = true; + public = false; + break; + } + case PUBLIC_ATOM: { + private = false; + public = true; + break; + } + case SET_ATOM: { + is_duplicate_bag = false; + break; + } + case DUPLICATE_BAG_ATOM: { + is_duplicate_bag = true; + break; + } + case KEYPOS_ATOM: { + VALIDATE_VALUE(value, term_is_integer); + avm_int_t keypos = term_to_int(value); + if (UNLIKELY(keypos < 1)) { + RAISE_ERROR(BADARG_ATOM); + } + key_index = keypos - 1; + break; + } + default: + RAISE_ERROR(BADARG_ATOM); + } + options = term_get_list_tail(options); + } + VALIDATE_VALUE(options, term_is_nil); EtsAccessType access = EtsAccessProtected; - if (!term_is_invalid_term(private)) { + if (private) { access = EtsAccessPrivate; - } else if (!term_is_invalid_term(public)) { + } else if (public) { access = EtsAccessPublic; } + EtsTableType type = is_duplicate_bag ? EtsTableDuplicateBag : EtsTableSet; + term table = term_invalid_term(); - EtsErrorCode result = ets_create_table_maybe_gc(name, is_named == TRUE_ATOM, EtsTableSet, access, term_to_int(keypos) - 1, &table, ctx); + EtsErrorCode result = ets_create_table_maybe_gc(name, is_named, type, access, key_index, &table, ctx); switch (result) { case EtsOk: return table; @@ -3638,12 +3700,17 @@ static term nif_ets_lookup_element(Context *ctx, int argc, term argv[]) term ref = argv[0]; VALIDATE_VALUE(ref, is_ets_table_id); - term key = argv[1]; - term pos = argv[2]; - VALIDATE_VALUE(pos, term_is_integer); + term key_term = argv[1]; + term keypos_term = argv[2]; + VALIDATE_VALUE(keypos_term, term_is_integer); + avm_int_t keypos = term_to_int(keypos_term); + if (UNLIKELY(keypos < 1)) { + RAISE_ERROR(BADARG_ATOM); + } + size_t key_index = keypos - 1; term ret = term_invalid_term(); - EtsErrorCode result = ets_lookup_element_maybe_gc(ref, key, term_to_int(pos), &ret, ctx); + EtsErrorCode result = ets_lookup_element_maybe_gc(ref, key_term, key_index, &ret, ctx); switch (result) { case EtsOk: return ret; @@ -3690,13 +3757,12 @@ static term nif_ets_update_counter(Context *ctx, int argc, term argv[]) term key = argv[1]; term operation = argv[2]; - term default_value; + term default_value = term_invalid_term(); if (argc == 4) { default_value = argv[3]; VALIDATE_VALUE(default_value, term_is_tuple); + // FIXME: this should be based on keypos in table term_put_tuple_element(default_value, 0, key); - } else { - default_value = term_invalid_term(); } term ret; EtsErrorCode result = ets_update_counter_maybe_gc(ref, key, operation, default_value, &ret, ctx); diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index 6b766d2df..fc3a24d92 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -1673,7 +1673,7 @@ static inline term term_alloc_tuple(uint32_t size, Heap *heap) /** * @brief Replaces the content of a tuple element. * - * @details Destructively replaces the nth element of an existing tuple, it should be used only on newly allocated tuples. + * @details Destructively replaces the nth (0-based) element of an existing tuple, it should be used only on newly allocated tuples. * @param t the term pointing to the target tuple, fails if not a tuple. * @param elem_index the index of the element that will be replaced. * @param put_value the term that will be put on the nth tuple element. @@ -1685,12 +1685,11 @@ static inline void term_put_tuple_element(term t, uint32_t elem_index, term put_ term *boxed_value = term_to_term_ptr(t); TERM_DEBUG_ASSERT((size_t) elem_index < term_get_size_from_boxed_header(boxed_value[0])); - boxed_value[elem_index + 1] = put_value; } /** - * @brief Returns the nth tuple element + * @brief Returns the nth (0-based) tuple element * * @details Returns the nth element for a given tuple pointed by a term. * @param t a term that points to a tuple, fails otherwise. @@ -1704,7 +1703,6 @@ static inline term term_get_tuple_element(term t, int elem_index) const term *boxed_value = term_to_const_term_ptr(t); TERM_DEBUG_ASSERT((size_t) elem_index < term_get_size_from_boxed_header(boxed_value[0])); - return boxed_value[elem_index + 1]; } diff --git a/tests/erlang_tests/test_ets.erl b/tests/erlang_tests/test_ets.erl index 7909506dd..8c8510f90 100644 --- a/tests/erlang_tests/test_ets.erl +++ b/tests/erlang_tests/test_ets.erl @@ -31,6 +31,7 @@ start() -> ok = isolated(fun test_delete/0), ok = isolated(fun test_lookup_element/0), ok = isolated(fun test_update_counter/0), + ok = isolated(fun test_duplicate_bag/0), 0. test_ets_new() -> @@ -49,20 +50,26 @@ test_ets_new() -> assert_badarg(fun() -> ets:new(keypos_test, [{keypos, -1}]) end), ets:new(type_test, [set]), - - % Unimplemented - ets:new(type_test, [ordered_set]), - ets:new(type_test, [bag]), ets:new(type_test, [duplicate_bag]), - ets:new(heir_test, [{heir, self(), []}]), - ets:new(heir_test, [{heir, none}]), - ets:new(write_conc_test, [{write_concurrency, true}]), - ets:new(read_conc_test, [{read_concurrency, true}]), - case otp_version() of - OTP when OTP >= 23 -> ets:new(decent_counters_test, [{decentralized_counters, true}]); - _ -> ok + + % Unimplemented in AtomVM + Options = [ + fun() -> ets:new(type_test, [ordered_set]) end, + fun() -> ets:new(type_test, [bag]) end, + fun() -> ets:new(heir_test, [{heir, self(), []}]) end, + fun() -> ets:new(heir_test, [{heir, none}]) end, + fun() -> ets:new(write_conc_test, [{write_concurrency, true}]) end, + fun() -> ets:new(read_conc_test, [{read_concurrency, true}]) end, + fun() -> ets:new(compressed_test, [compressed]) end + ], + Otp23Options = [ + fun() -> ets:new(decent_counters_test, [{decentralized_counters, true}]) end | Options + ], + case vm_version() of + atom -> [assert_badarg(NewF) || NewF <- Otp23Options]; + {otp, V} when V >= 23 -> [NewF() || NewF <- Otp23Options]; + {otp, _V} -> [NewF() || NewF <- Options] end, - ets:new(compressed_test, [compressed]), ok. test_permissions() -> @@ -227,8 +234,14 @@ test_update_counter() -> 31 = ets:update_counter(T, key, {4, 10, 39, 31}), 30 = ets:update_counter(T, key, {4, -10, 30, 30}), + % {Position, Increment} with non-default position + T2 = ets:new(test, [{keypos, 2}]), + true = ets:insert(T2, {100, 200}), + 150 = ets:update_counter(T2, 200, {1, 50}), + TErr = ets:new(test, []), true = ets:insert(TErr, {key, 0, not_number}), + true = ets:insert(TErr, {0}), true = ets:insert(TErr, {not_number, ok}), assert_badarg(fun() -> ets:update_counter(TErr, none, 10) end), assert_badarg(fun() -> ets:update_counter(TErr, not_number, 10) end), @@ -237,6 +250,7 @@ test_update_counter() -> assert_badarg(fun() -> ets:update_counter(TErr, key, {0, 10}) end), assert_badarg(fun() -> ets:update_counter(TErr, key, {-1, 10}) end), assert_badarg(fun() -> ets:update_counter(TErr, key, {1, 10}) end), + assert_badarg(fun() -> ets:update_counter(TErr, 0, {1, 10}) end), assert_badarg(fun() -> ets:update_counter(TErr, key, {3, 10}) end), assert_badarg(fun() -> ets:update_counter(TErr, key, not_number) end), assert_badarg(fun() -> ets:update_counter(TErr, key, {not_number, 10}) end), @@ -247,6 +261,49 @@ test_update_counter() -> assert_badarg(fun() -> ets:update_counter(TErr, key, {2, 10, 100, not_number}) end), ok. +test_duplicate_bag() -> + Tid = ets:new(test_duplicate_bag, [duplicate_bag, {keypos, 2}]), + T = {ok, foo, 100, extra}, + T2 = {error, foo, 200}, + _T3 = {error, foo, 300}, + + % true = ets:insert_new(Tid, T), + % false = ets:insert_new(Tid, T), + true = ets:insert(Tid, T), + true = ets:insert(Tid, T), + true = ets:insert(Tid, [T, T]), + true = ets:insert(Tid, [T2]), + true = [T, T, T, T2] == ets:lookup(Tid, foo), + % true = ets:member(Tid, foo), + + % % nothing inserted, T exists in table + % false = ets:insert_new(Tid, [T, {ok, bar, batat}]), + % false = ets:member(Tid, bar), + + [ok, ok, ok, error] = ets:lookup_element(Tid, foo, 1), + [foo, foo, foo, foo] = ets:lookup_element(Tid, foo, 2), + [100, 100, 100, 200] = ets:lookup_element(Tid, foo, 3), + % % some tuples don't have 4 arity + ok = assert_badarg(fun() -> ets:lookup_element(Tid, foo, 4) end), + + % % unsupported for duplicate bag + ok = assert_badarg(fun() -> ets:update_counter(Tid, foo, 10) end), + % ok = assert_badarg(fun() -> ets:update_element(Tid, foo, {1, error}) end), + + % true = ets:delete_object(Tid, {bad, bad}), + % true = [T, T, T, T, T2] == ets:lookup(Tid, foo), + % true = ets:delete_object(Tid, T), + % true = [T2] == ets:lookup(Tid, foo), + + % true = ets:insert(Tid, T3), + % % keeps insertion order + % true = [T2, T3] == ets:take(Tid, foo), + + % true = ets:delete(Tid), + % ok = assert_badarg(fun() -> ets:insert(Tid, T) end), + + ok. + %%----------------------------------------------------------------------------- %% @doc Performs specified operation on ETS table implicitly asserting that no exception is raised. %% [badarg] can be passed as an option to assert that exception was raised. @@ -290,32 +347,32 @@ isolated(Fun) -> end. assert_badarg(Fun) -> - try - Fun(), - erlang:error(no_throw) + try Fun() of + R -> erlang:error({no_throw, R}) catch error:badarg -> ok; - OtherClass:OtherError -> - erlang:error({OtherClass, OtherError}) + C:E -> + erlang:error({C, E}) end. supports_v4_port_encoding() -> + case vm_version() of + % small utf8 atom + atom -> true; + {otp, V} when V < 24 -> false; + % v4 is supported but not the default + {otp, V} when V < 26 -> true; + % small utf8 atom + {otp, _} -> true + end. + +vm_version() -> case erlang:system_info(machine) of "ATOM" -> - % small utf8 atom - true; + atom; "BEAM" -> - OTP = otp_version(), - if - OTP < 24 -> false; - % v4 is supported but not the default - OTP < 26 -> true; - % small utf8 atom - true -> true - end + OTPRelease = erlang:system_info(otp_release), + Version = list_to_integer(OTPRelease), + {otp, Version} end. - -otp_version() -> - OTPRelease = erlang:system_info(otp_release), - list_to_integer(OTPRelease).