From 3fe0051a9e86cdd829fbb489eccbe1e1f7225f97 Mon Sep 17 00:00:00 2001 From: Mateusz Furga Date: Tue, 3 Mar 2026 16:35:42 +0100 Subject: [PATCH 1/8] Replace ets_hashtable with ets_multimap supporting set, bag, and duplicate_bag Signed-off-by: Mateusz Furga --- src/libAtomVM/CMakeLists.txt | 4 +- src/libAtomVM/ets.c | 984 ++++++++++++++++++++------------ src/libAtomVM/ets.h | 71 ++- src/libAtomVM/ets_hashtable.c | 416 -------------- src/libAtomVM/ets_hashtable.h | 65 --- src/libAtomVM/ets_multimap.c | 752 ++++++++++++++++++++++++ src/libAtomVM/ets_multimap.h | 147 +++++ src/libAtomVM/nifs.c | 219 ++++--- src/libAtomVM/nifs.gperf | 4 +- tests/erlang_tests/test_ets.erl | 518 ++++++++++++++--- 10 files changed, 2150 insertions(+), 1030 deletions(-) delete mode 100644 src/libAtomVM/ets_hashtable.c delete mode 100644 src/libAtomVM/ets_hashtable.h create mode 100644 src/libAtomVM/ets_multimap.c create mode 100644 src/libAtomVM/ets_multimap.h diff --git a/src/libAtomVM/CMakeLists.txt b/src/libAtomVM/CMakeLists.txt index 8fdb354b65..076cae0d40 100644 --- a/src/libAtomVM/CMakeLists.txt +++ b/src/libAtomVM/CMakeLists.txt @@ -36,7 +36,7 @@ set(HEADER_FILES erl_nif.h erl_nif_priv.h ets.h - ets_hashtable.h + ets_multimap.h exportedfunction.h external_term.h globalcontext.h @@ -86,7 +86,7 @@ set(SOURCE_FILES dictionary.c dist_nifs.c ets.c - ets_hashtable.c + ets_multimap.c external_term.c globalcontext.c iff.c diff --git a/src/libAtomVM/ets.c b/src/libAtomVM/ets.c index 36f51e83bd..e88a5f3a86 100644 --- a/src/libAtomVM/ets.c +++ b/src/libAtomVM/ets.c @@ -2,6 +2,7 @@ * This file is part of AtomVM. * * Copyright 2024 Fred Dushin + * Copyright 2025 Mateusz Furga * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,551 +19,824 @@ * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later */ -#include "ets.h" +#include #include "context.h" #include "defaultatoms.h" -#include "ets_hashtable.h" #include "list.h" #include "memory.h" #include "overflow_helpers.h" #include "term.h" +#include "utils.h" + +#include "ets.h" +#include "ets_multimap.h" -#define ETS_NO_INDEX SIZE_MAX #define ETS_ANY_PROCESS -1 +#define ETS_WHOLE_TUPLE SIZE_MAX #ifndef AVM_NO_SMP +#include "smp.h" #define SMP_RDLOCK(table) smp_rwlock_rdlock(table->lock) #define SMP_WRLOCK(table) smp_rwlock_wrlock(table->lock) #define SMP_UNLOCK(table) smp_rwlock_unlock(table->lock) #else -#define SMP_RDLOCK(table) -#define SMP_WRLOCK(table) -#define SMP_UNLOCK(table) -#endif - -#ifndef AVM_NO_SMP -#ifndef TYPEDEF_RWLOCK -#define TYPEDEF_RWLOCK -typedef struct RWLock RWLock; -#endif +#define SMP_RDLOCK(table) UNUSED(table) +#define SMP_WRLOCK(table) UNUSED(table) +#define SMP_UNLOCK(table) UNUSED(table) #endif struct EtsTable { struct ListHead head; - uint64_t ref_ticks; + term name; - bool is_named; + bool named; + size_t key_index; + EtsTableType type; + EtsTableAccess access; + + EtsMultimap *multimap; + int32_t owner_process_id; - size_t keypos; - EtsTableType table_type; - // In the future, we might support rb-trees for sorted sets - // For this MVP, we only support unsorted sets - struct EtsHashTable *hashtable; - EtsAccessType access_type; + uint64_t ref_ticks; #ifndef AVM_NO_SMP RWLock *lock; #endif }; -typedef enum TableAccessType + +typedef enum TableAccess { TableAccessNone, TableAccessRead, TableAccessWrite -} TableAccessType; - -static void ets_delete_all_tables(struct Ets *ets, GlobalContext *global); - -static void ets_add_table(struct Ets *ets, struct EtsTable *ets_table) -{ - struct ListHead *ets_tables_list = synclist_wrlock(&ets->ets_tables); - - list_append(ets_tables_list, &ets_table->head); - - 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) -{ - uint64_t ref = 0; - term name = term_invalid_term(); - bool is_atom = term_is_atom(name_or_ref); - if (is_atom) { - name = name_or_ref; - } else { - ref = term_to_ref_ticks(name_or_ref); - } - - struct EtsTable *ret = NULL; - struct ListHead *ets_tables_list = synclist_rdlock(&ets->ets_tables); - struct ListHead *item; - LIST_FOR_EACH (item, ets_tables_list) { - struct EtsTable *table = GET_LIST_ENTRY(item, struct EtsTable, head); - bool found = is_atom ? table->is_named && table->name == name : table->ref_ticks == ref; - if (found) { - bool is_owner = table->owner_process_id == process_id; - bool is_non_private = table->access_type != EtsAccessPrivate; - bool is_public = table->access_type == EtsAccessPublic; - - bool can_read = requested_access_type == TableAccessRead && (is_non_private || is_owner); - bool can_write = requested_access_type == TableAccessWrite && (is_public || is_owner); - bool access_none = requested_access_type == TableAccessNone; - if (can_read) { - SMP_RDLOCK(table); - ret = table; - } else if (can_write) { - SMP_WRLOCK(table); - ret = table; - } else if (access_none) { - ret = table; - } - break; - } - } - synclist_unlock(&ets->ets_tables); - return ret; -} - -void ets_init(struct Ets *ets) +} TableAccess; + +static struct EtsTable *get_table( + Ets *ets, + term name_or_ref, + int32_t process_id, + TableAccess access); +static EtsStatus add_table(Ets *ets, struct EtsTable *table); +static void delete_all_tables(Ets *ets, GlobalContext *global); +static void table_destroy(struct EtsTable *table, GlobalContext *global); +static EtsStatus insert_one( + struct EtsTable *table, + term tuple, + bool as_new, + Context *ctx); +static EtsStatus insert_many( + struct EtsTable *table, + term tuples, + bool as_new, + Context *ctx); +static EtsStatus lookup_select_maybe_gc( + struct EtsTable *table, + term key, + size_t index, + size_t num_roots, + term *roots, + term *ret, + Context *ctx); +static EtsStatus lookup_or_default( + struct EtsTable *table, + term key, + term default_tuple, + Heap *ret_heap, + term *ret, + Context *ctx); +static EtsStatus apply_op(term tuple, term opt, avm_int_t *ret, size_t key_index); + +void ets_init(Ets *ets) { synclist_init(&ets->ets_tables); } -void ets_destroy(struct Ets *ets, GlobalContext *global) +void ets_destroy(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) +EtsStatus ets_create_table_maybe_gc( + term name, + bool named, + EtsTableType type, + EtsTableAccess access, + size_t key_index, + term *ret, + Context *ctx) { - if (is_named) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ETS_ANY_PROCESS, name, TableAccessNone); - if (ets_table != NULL) { - return EtsTableNameInUse; + assert(ret != NULL); + + if (named) { + struct EtsTable *table = get_table( + &ctx->global->ets, + name, + ETS_ANY_PROCESS, + TableAccessNone); + + if (table != NULL) { + // Don't need to drop lock as we used TableAccessNone + return EtsTableNameExists; } } - struct EtsTable *ets_table = malloc(sizeof(struct EtsTable)); - if (IS_NULL_PTR(ets_table)) { - return EtsAllocationFailure; + struct EtsTable *table = malloc(sizeof(struct EtsTable)); + if (IS_NULL_PTR(table)) { + return EtsAllocationError; } - list_init(&ets_table->head); - - ets_table->name = name; - ets_table->is_named = is_named; - ets_table->access_type = access_type; - - ets_table->table_type = table_type; - struct EtsHashTable *hashtable = ets_hashtable_new(); - if (IS_NULL_PTR(hashtable)) { - free(ets_table); - return EtsAllocationFailure; + EtsMultimapType multimap_type = EtsMultimapTypeSingle; + if (type == EtsTableBag) { + multimap_type = EtsMultimapTypeSet; + } else if (type == EtsTableDuplicateBag) { + multimap_type = EtsMultimapTypeList; } - ets_table->hashtable = hashtable; - ets_table->owner_process_id = ctx->process_id; + EtsMultimap *multimap = ets_multimap_new(multimap_type, key_index); + if (IS_NULL_PTR(multimap)) { + free(table); + return EtsAllocationError; + } - uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); - ets_table->ref_ticks = ref_ticks; + list_init(&table->head); - ets_table->keypos = keypos; + table->name = name; + table->named = named; + table->type = type; + table->access = access; + table->key_index = key_index; + table->owner_process_id = ctx->process_id; + table->ref_ticks = globalcontext_get_ref_ticks(ctx->global); + table->multimap = multimap; #ifndef AVM_NO_SMP - ets_table->lock = smp_rwlock_create(); + table->lock = smp_rwlock_create(); #endif - if (is_named) { + if (named) { *ret = name; } else { if (UNLIKELY(memory_ensure_free_opt(ctx, REF_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - ets_hashtable_destroy(hashtable, ctx->global); - free(ets_table); - return EtsAllocationFailure; + ets_multimap_delete(multimap, ctx->global); +#ifndef AVM_NO_SMP + smp_rwlock_destroy(table->lock); +#endif + free(table); + return EtsAllocationError; } - *ret = term_from_ref_ticks(ref_ticks, &ctx->heap); + *ret = term_from_ref_ticks(table->ref_ticks, &ctx->heap); } - ets_add_table(&ctx->global->ets, ets_table); + EtsStatus result = add_table(&ctx->global->ets, table); + if (UNLIKELY(result == EtsTableNameExists)) { + ets_multimap_delete(multimap, ctx->global); +#ifndef AVM_NO_SMP + smp_rwlock_destroy(table->lock); +#endif + free(table); + return result; + } return EtsOk; } -static void ets_table_destroy(struct EtsTable *table, GlobalContext *global) +EtsStatus ets_lookup_maybe_gc(term name_or_ref, term key, term *ret, Context *ctx) { - SMP_WRLOCK(table); - ets_hashtable_destroy(table->hashtable, global); - SMP_UNLOCK(table); + assert(ret != NULL); -#ifndef AVM_NO_SMP - smp_rwlock_destroy(table->lock); -#endif + struct EtsTable *table = get_table( + &ctx->global->ets, + name_or_ref, + ctx->process_id, + TableAccessRead); - free(table); -} + if (table == NULL) { + return EtsBadAccess; + } -typedef bool (*ets_table_filter_pred)(struct EtsTable *table, void *data); + EtsStatus result = lookup_select_maybe_gc(table, key, ETS_WHOLE_TUPLE, 0, NULL, ret, ctx); -static void ets_delete_tables_internal(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; - struct ListHead *tmp; - MUTABLE_LIST_FOR_EACH (item, tmp, ets_tables_list) { - struct EtsTable *table = GET_LIST_ENTRY(item, struct EtsTable, head); - if (pred(table, data)) { - list_remove(&table->head); - ets_table_destroy(table, global); - } - } - synclist_unlock(&ets->ets_tables); -} + SMP_UNLOCK(table); -static bool equal_process_id_pred(struct EtsTable *table, void *data) -{ - int32_t *process_id = (int32_t *) data; - return table->owner_process_id == *process_id; + return result; } -void ets_delete_owned_tables(struct Ets *ets, int32_t process_id, GlobalContext *global) +EtsStatus ets_lookup_element_maybe_gc(term name_or_ref, term key, size_t index, term *ret, Context *ctx) { - ets_delete_tables_internal(ets, equal_process_id_pred, &process_id, global); -} + assert(ret != NULL); -static bool true_pred(struct EtsTable *table, void *data) -{ - UNUSED(table); - UNUSED(data); + struct EtsTable *table = get_table( + &ctx->global->ets, + name_or_ref, + ctx->process_id, + TableAccessRead); - return true; -} + if (table == NULL) { + return EtsBadAccess; + } -static void ets_delete_all_tables(struct Ets *ets, GlobalContext *global) -{ - ets_delete_tables_internal(ets, true_pred, NULL, global); + EtsStatus result = lookup_select_maybe_gc(table, key, index, 0, NULL, ret, ctx); + + SMP_UNLOCK(table); + + return result; } -static EtsErrorCode ets_table_insert(struct EtsTable *ets_table, term entry, Context *ctx) +EtsStatus ets_insert(term name_or_ref, term entry, bool as_new, Context *ctx) { - size_t keypos = ets_table->keypos; + struct EtsTable *table = get_table( + &ctx->global->ets, + name_or_ref, + ctx->process_id, + TableAccessWrite); - if ((size_t) term_get_tuple_arity(entry) < keypos + 1) { - return EtsBadEntry; + if (table == NULL) { + return EtsBadAccess; } - struct HNode *new_node = ets_hashtable_new_node(entry, keypos); - if (IS_NULL_PTR(new_node)) { - return EtsAllocationFailure; - } + EtsStatus result = EtsBadEntry; - EtsHashtableStatus res = ets_hashtable_insert(ets_table->hashtable, new_node, EtsHashtableAllowOverwrite, ctx->global); - if (UNLIKELY(res != EtsHashtableOk)) { - return EtsAllocationFailure; + if (term_is_tuple(entry)) { + result = insert_one(table, entry, as_new, ctx); + } else if (term_is_list(entry)) { + result = insert_many(table, entry, as_new, ctx); } - return EtsOk; + SMP_UNLOCK(table); + + return result; } -static EtsErrorCode ets_table_insert_list(struct EtsTable *ets_table, term list, Context *ctx) +EtsStatus ets_update_counter_maybe_gc( + term name_or_ref, + term key, + term op, + term default_tuple, + term *ret, + Context *ctx) { - term iter = list; - size_t size = 0; + struct EtsTable *table = get_table( + &ctx->global->ets, + name_or_ref, + ctx->process_id, + TableAccessWrite); - 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)) { - return EtsBadEntry; - } - ++size; - } - if (!term_is_nil(iter)) { - return EtsBadEntry; + if (table == NULL) { + return EtsBadAccess; } - struct HNode **nodes = malloc(size * sizeof(struct HNode *)); - if (IS_NULL_PTR(nodes)) { - return EtsAllocationFailure; + Heap insert_heap; + term insert_tuple; + EtsStatus result = lookup_or_default(table, key, default_tuple, &insert_heap, &insert_tuple, ctx); + if (result != EtsOk) { + SMP_UNLOCK(table); + return result; } - 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); + if (term_is_integer(op)) { + avm_int_t index = (avm_int_t) table->key_index + 1; + + if (index < 0 || index >= term_get_tuple_arity(insert_tuple)) { + result = EtsBadEntry; + goto cleanup; + } + + term value = term_get_tuple_element(insert_tuple, (uint32_t) index); + if (!term_is_integer(value)) { + result = EtsBadEntry; + goto cleanup; + } + + avm_int_t new_value; + if (BUILTIN_ADD_OVERFLOW_INT(term_to_int(value), term_to_int(op), &new_value)) { + result = EtsOverflow; + goto cleanup; + } + + term_put_tuple_element(insert_tuple, (uint32_t) index, term_from_int(new_value)); + + *ret = term_from_int(new_value); + } else if (term_is_tuple(op)) { + avm_int_t value; + + result = apply_op(insert_tuple, op, &value, table->key_index); + if (result != EtsOk) { + goto cleanup; + } + + *ret = term_from_int(value); + } else if (term_is_list(op)) { + size_t num_ops = 0; + for (term iter = op; !term_is_nil(iter); iter = term_get_list_tail(iter)) { + if (!term_is_list(iter)) { + result = EtsBadEntry; + goto cleanup; } - free(nodes); - return EtsAllocationFailure; + num_ops++; } - ++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); - assert(res == EtsHashtableOk); + *ret = term_nil(); + + if (num_ops > 0) { + if (UNLIKELY(memory_ensure_free_with_roots(ctx, num_ops * CONS_SIZE, 1, &op, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + result = EtsAllocationError; + goto cleanup; + } + + avm_int_t *values = malloc(sizeof(avm_int_t) * num_ops); + if (IS_NULL_PTR(values)) { + result = EtsAllocationError; + goto cleanup; + } + + size_t i = 0; + for (term iter = op; !term_is_nil(iter); iter = term_get_list_tail(iter), i++) { + term entry = term_get_list_head(iter); + result = apply_op(insert_tuple, entry, &values[i], table->key_index); + if (result != EtsOk) { + free(values); + goto cleanup; + } + } + + term list = term_nil(); + + assert(num_ops >= 1); + for (size_t j = num_ops; j > 0; j--) { + list = term_list_prepend(term_from_int(values[j - 1]), list, &ctx->heap); + } + free(values); + + *ret = list; + } + } else { + result = EtsBadEntry; + goto cleanup; } - free(nodes); - return EtsOk; + result = ets_multimap_insert(table->multimap, &insert_tuple, 1, ctx->global); + +cleanup: + memory_destroy_heap(&insert_heap, ctx->global); + SMP_UNLOCK(table); + return result; } -EtsErrorCode ets_insert(term name_or_ref, term entry, Context *ctx) +EtsStatus ets_delete(term name_or_ref, term key, Context *ctx) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessWrite); - if (IS_NULL_PTR(ets_table)) { + struct EtsTable *table = get_table( + &ctx->global->ets, + name_or_ref, + ctx->process_id, + TableAccessWrite); + + if (table == NULL) { return EtsBadAccess; } - EtsErrorCode result; - if (term_is_tuple(entry)) { - result = ets_table_insert(ets_table, entry, ctx); - } else if (term_is_list(entry)) { - result = ets_table_insert_list(ets_table, entry, ctx); - } else { - result = EtsBadEntry; - } + EtsStatus result = ets_multimap_remove(table->multimap, key, ctx->global); - SMP_UNLOCK(ets_table); + SMP_UNLOCK(table); return result; } -static EtsErrorCode ets_table_lookup_maybe_gc(struct EtsTable *ets_table, term key, term *ret, Context *ctx, int num_roots, term *roots) +EtsStatus ets_delete_table(term name_or_ref, Context *ctx) { - term res = ets_hashtable_lookup(ets_table->hashtable, key, ets_table->keypos, ctx->global); + struct ListHead *ets_tables = synclist_wrlock(&ctx->global->ets.ets_tables); - if (term_is_nil(res)) { - *ret = term_nil(); + struct ListHead *item; + struct EtsTable *table = NULL; + + uint64_t ref = 0; + term name = term_invalid_term(); + bool is_atom = term_is_atom(name_or_ref); + + if (is_atom) { + name = name_or_ref; } else { + ref = term_to_ref_ticks(name_or_ref); + } - size_t size = (size_t) memory_estimate_usage(res); - // allocate [object] - if (UNLIKELY(memory_ensure_free_with_roots(ctx, size + CONS_SIZE, num_roots, roots, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - return EtsAllocationFailure; + LIST_FOR_EACH (item, ets_tables) { + struct EtsTable *t = GET_LIST_ENTRY(item, struct EtsTable, head); + bool found = is_atom ? t->named && t->name == name : t->ref_ticks == ref; + if (found) { + bool is_owner = t->owner_process_id == ctx->process_id; + if (t->access == EtsTableAccessPublic || is_owner) { + table = t; + } + break; } - term new_res = memory_copy_term_tree(&ctx->heap, res); - *ret = term_list_prepend(new_res, term_nil(), &ctx->heap); } + if (table == NULL) { + synclist_unlock(&ctx->global->ets.ets_tables); + return EtsBadAccess; + } + + list_remove(&table->head); + synclist_unlock(&ctx->global->ets.ets_tables); + + table_destroy(table, ctx->global); + return EtsOk; } -EtsErrorCode ets_lookup_maybe_gc(term name_or_ref, term key, term *ret, Context *ctx) +void ets_delete_owned_tables(Ets *ets, int32_t process_id, GlobalContext *global) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessRead); - if (IS_NULL_PTR(ets_table)) { - return EtsBadAccess; + struct ListHead *ets_tables = synclist_wrlock(&ets->ets_tables); + + struct ListHead *item, *tmp; + MUTABLE_LIST_FOR_EACH (item, tmp, ets_tables) { + struct EtsTable *table = GET_LIST_ENTRY(item, struct EtsTable, head); + + if (table->owner_process_id == process_id) { + list_remove(&table->head); + table_destroy(table, global); + } } - EtsErrorCode result = ets_table_lookup_maybe_gc(ets_table, key, ret, ctx, 0, NULL); - SMP_UNLOCK(ets_table); + synclist_unlock(&ets->ets_tables); +} - return result; +static void table_destroy(struct EtsTable *table, GlobalContext *global) +{ + SMP_WRLOCK(table); + ets_multimap_delete(table->multimap, global); + SMP_UNLOCK(table); + +#ifndef AVM_NO_SMP + smp_rwlock_destroy(table->lock); +#endif + + free(table); } -EtsErrorCode ets_lookup_element_maybe_gc(term name_or_ref, term key, size_t pos, term *ret, Context *ctx) +static void delete_all_tables(Ets *ets, GlobalContext *global) { - if (UNLIKELY(pos == 0)) { - return EtsBadPosition; - } + struct ListHead *ets_tables = synclist_wrlock(&ets->ets_tables); - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessRead); - if (IS_NULL_PTR(ets_table)) { - return EtsBadAccess; + struct ListHead *item, *tmp; + MUTABLE_LIST_FOR_EACH (item, tmp, ets_tables) { + struct EtsTable *table = GET_LIST_ENTRY(item, struct EtsTable, head); + list_remove(&table->head); + table_destroy(table, global); } - term entry = ets_hashtable_lookup(ets_table->hashtable, key, ets_table->keypos, ctx->global); + synclist_unlock(&ets->ets_tables); +} - if (term_is_nil(entry)) { - SMP_UNLOCK(ets_table); - return EtsEntryNotFound; +static EtsStatus add_table(Ets *ets, struct EtsTable *table) +{ + struct ListHead *tables = synclist_wrlock(&ets->ets_tables); + + if (table->named) { + struct ListHead *item; + LIST_FOR_EACH (item, tables) { + struct EtsTable *t = GET_LIST_ENTRY(item, struct EtsTable, head); + if (t->named && t->name == table->name) { + synclist_unlock(&ets->ets_tables); + return EtsTableNameExists; + } + } } - if ((size_t) term_get_tuple_arity(entry) < pos) { - SMP_UNLOCK(ets_table); - return EtsBadPosition; + list_append(tables, &table->head); + synclist_unlock(&ets->ets_tables); + return EtsOk; +} + +static struct EtsTable *get_table( + Ets *ets, + term name_or_ref, + int32_t process_id, + TableAccess access) +{ + struct ListHead *ets_tables = synclist_rdlock(&ets->ets_tables); + struct ListHead *item; + struct EtsTable *ret = NULL; + + uint64_t ref = 0; + term name = term_invalid_term(); + bool is_atom = term_is_atom(name_or_ref); + + if (is_atom) { + name = name_or_ref; + } else { + ref = term_to_ref_ticks(name_or_ref); } - 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)) { - SMP_UNLOCK(ets_table); - return EtsAllocationFailure; + LIST_FOR_EACH (item, ets_tables) { + struct EtsTable *table = GET_LIST_ENTRY(item, struct EtsTable, head); + bool found = is_atom ? table->named && table->name == name : table->ref_ticks == ref; + if (found) { + bool is_owner = table->owner_process_id == process_id; + bool can_read = access == TableAccessRead && (table->access != EtsTableAccessPrivate || is_owner); + bool can_write = access == TableAccessWrite && (table->access == EtsTableAccessPublic || is_owner); + bool access_none = access == TableAccessNone; + if (can_read) { + SMP_RDLOCK(table); + ret = table; + } else if (can_write) { + SMP_WRLOCK(table); + ret = table; + } else if (access_none) { + ret = table; + } + break; + } } - *ret = memory_copy_term_tree(&ctx->heap, res); - SMP_UNLOCK(ets_table); - return EtsOk; + synclist_unlock(&ets->ets_tables); + return ret; } -EtsErrorCode ets_drop_table(term name_or_ref, term *ret, Context *ctx) +static EtsStatus insert_one( + struct EtsTable *table, + term tuple, + bool as_new, + Context *ctx) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessWrite); - if (IS_NULL_PTR(ets_table)) { - return EtsBadAccess; + assert(term_is_tuple(tuple)); + + EtsStatus result = EtsOk; + + if (table->key_index >= (size_t) term_get_tuple_arity(tuple)) { + return EtsBadEntry; } - struct ListHead *ets_tables_list = synclist_wrlock(&ctx->global->ets.ets_tables); - UNUSED(ets_tables_list); - list_remove(&ets_table->head); - SMP_UNLOCK(ets_table); - ets_table_destroy(ets_table, ctx->global); - synclist_unlock(&ctx->global->ets.ets_tables); + if (as_new) { + term key = term_get_tuple_element(tuple, table->key_index); + size_t existing = 0; + result = ets_multimap_lookup(table->multimap, key, NULL, &existing, ctx->global); + if (UNLIKELY(result == EtsAllocationError)) { + return EtsAllocationError; + } + if (existing > 0) { + return EtsKeyExists; + } + } - *ret = TRUE_ATOM; - return EtsOk; + result = ets_multimap_insert(table->multimap, &tuple, 1, ctx->global); + + return result; } -EtsErrorCode ets_delete(term name_or_ref, term key, term *ret, Context *ctx) +static EtsStatus insert_many( + struct EtsTable *table, + term tuples, + bool as_new, + Context *ctx) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, name_or_ref, TableAccessWrite); - if (IS_NULL_PTR(ets_table)) { - return EtsBadAccess; + assert(term_is_list(tuples)); + + EtsStatus result = EtsOk; + + size_t count = 0; + for (term iter = tuples; !term_is_nil(iter); iter = term_get_list_tail(iter), count++) { + if (!term_is_list(iter)) { + return EtsBadEntry; // improper list + } + + term tuple = term_get_list_head(iter); + + if (!term_is_tuple(tuple) || table->key_index >= (size_t) term_get_tuple_arity(tuple)) { + return EtsBadEntry; + } + + if (as_new) { + term key = term_get_tuple_element(tuple, table->key_index); + size_t existing = 0; + result = ets_multimap_lookup(table->multimap, key, NULL, &existing, ctx->global); + if (UNLIKELY(result == EtsAllocationError)) { + return EtsAllocationError; + } + if (existing > 0) { + return EtsKeyExists; + } + } + } + + if (count == 0) { + return EtsOk; } - bool _found = ets_hashtable_remove(ets_table->hashtable, key, ets_table->keypos, ctx->global); - UNUSED(_found); - SMP_UNLOCK(ets_table); + term *to_insert = malloc(sizeof(term) * count); + if (IS_NULL_PTR(to_insert)) { + return EtsAllocationError; + } - *ret = TRUE_ATOM; - return EtsOk; + for (size_t i = 0; !term_is_nil(tuples); tuples = term_get_list_tail(tuples), i++) { + assert(term_is_list(tuples)); + to_insert[i] = term_get_list_head(tuples); + } + + result = ets_multimap_insert(table->multimap, to_insert, count, ctx->global); + + free(to_insert); + + return result; } -static bool operation_to_tuple4(term operation, size_t default_pos, term *position, term *increment, term *threshold, term *set_value) +static EtsStatus lookup_select_maybe_gc( + struct EtsTable *table, + term key, + size_t index, + size_t num_roots, + term *roots, + term *ret, + Context *ctx) { - if (term_is_integer(operation)) { - *increment = operation; - *position = term_from_int(default_pos); - *threshold = term_invalid_term(); - *set_value = term_invalid_term(); - return true; + assert(ret != NULL); + + *ret = term_nil(); + + term *tuples = NULL; + + size_t count; + EtsStatus result = ets_multimap_lookup(table->multimap, key, &tuples, &count, ctx->global); + if (UNLIKELY(result == EtsAllocationError)) { + return EtsAllocationError; } - if (UNLIKELY(!term_is_tuple(operation))) { - return false; + if (count == 0) { + return EtsTupleNotExists; } - int n = term_get_tuple_arity(operation); - if (UNLIKELY(n != 2 && n != 4)) { - return false; + + assert(tuples != NULL); + + size_t elements_size = 0; + for (size_t i = 0; i < count; i++) { + term tuple = tuples[i]; + + if (index == ETS_WHOLE_TUPLE) { + elements_size += memory_estimate_usage(tuple); + } else { + if (index >= (size_t) term_get_tuple_arity(tuple)) { + free(tuples); + return EtsBadIndex; + } + term element = term_get_tuple_element(tuple, index); + elements_size += memory_estimate_usage(element); + } } - term pos = term_get_tuple_element(operation, 0); - term incr = term_get_tuple_element(operation, 1); - if (UNLIKELY(!term_is_integer(pos) || !term_is_integer(incr))) { - return false; + bool return_list = table->type == EtsTableBag || table->type == EtsTableDuplicateBag || index == ETS_WHOLE_TUPLE; + + if (return_list) { + elements_size += count * CONS_SIZE; } - if (n == 2) { - *position = pos; - *increment = incr; - *threshold = term_invalid_term(); - *set_value = term_invalid_term(); - return true; + // Terms in `tuples` come from ETS heap, we need to copy them to process heap before returning. + if (UNLIKELY(memory_ensure_free_with_roots(ctx, elements_size, num_roots, roots, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + free(tuples); + return EtsAllocationError; } - term tresh = term_get_tuple_element(operation, 2); - term set_val = term_get_tuple_element(operation, 3); - if (UNLIKELY(!term_is_integer(tresh) || !term_is_integer(set_val))) { - return false; + if (return_list) { + term list = term_nil(); + + for (size_t i = 0; i < count; i++) { + term tuple = tuples[i]; + term element; + + if (index == ETS_WHOLE_TUPLE) { + element = tuple; + } else { + element = term_get_tuple_element(tuple, index); + } + + element = memory_copy_term_tree(&ctx->heap, element); + list = term_list_prepend(element, list, &ctx->heap); + } + *ret = list; + } else { + assert(index != ETS_WHOLE_TUPLE); + assert(count == 1); + + term element = term_get_tuple_element(tuples[0], index); + *ret = memory_copy_term_tree(&ctx->heap, element); } - *position = pos; - *increment = incr; - *threshold = tresh; - *set_value = set_val; - return true; + free(tuples); + + return EtsOk; } -EtsErrorCode ets_update_counter_maybe_gc(term ref, term key, term operation, term default_value, term *ret, Context *ctx) +static EtsStatus lookup_or_default( + struct EtsTable *table, + term key, + term default_tuple, + Heap *ret_heap, + term *ret, + Context *ctx) { - struct EtsTable *ets_table = ets_acquire_table(&ctx->global->ets, ctx->process_id, ref, TableAccessWrite); - if (IS_NULL_PTR(ets_table)) { + if (table->type != EtsTableSet) { return EtsBadAccess; } - // 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); - if (UNLIKELY(result != EtsOk)) { - SMP_UNLOCK(ets_table); + term *tuple = NULL; + size_t count; + EtsStatus result = ets_multimap_lookup(table->multimap, key, &tuple, &count, ctx->global); + if (result != EtsOk) { return result; } - key = roots[0]; - operation = roots[1]; - default_value = term_is_invalid_term(default_value) ? term_invalid_term() : roots[2]; + bool insert_default = (count == 0); + + if (insert_default && term_is_invalid_term(default_tuple)) { + return EtsTupleNotExists; + } - term to_insert; - if (term_is_nil(list)) { - if (term_is_invalid_term(default_value)) { - SMP_UNLOCK(ets_table); + if (insert_default) { + if ((size_t) term_get_tuple_arity(default_tuple) <= table->key_index) { return EtsBadEntry; } - to_insert = default_value; + tuple = &default_tuple; + } + + size_t size = memory_estimate_usage(*tuple) + memory_estimate_usage(key); + + if (UNLIKELY(memory_init_heap(ret_heap, size) != MEMORY_GC_OK)) { + if (!insert_default) { + free(tuple); + } + return EtsAllocationError; + } + + *ret = memory_copy_term_tree(ret_heap, *tuple); + + if (insert_default) { + key = memory_copy_term_tree(ret_heap, key); + term_put_tuple_element(*ret, (uint32_t) table->key_index, key); } else { - to_insert = term_get_list_head(list); + free(tuple); + } + + return EtsOk; +} + +static EtsStatus apply_op(term tuple, term op, avm_int_t *ret, size_t key_index) +{ + assert(term_is_tuple(op)); + + int arity = term_get_tuple_arity(op); + + if (arity != 2 && arity != 4) { + return EtsBadEntry; } - if (UNLIKELY(!term_is_tuple(to_insert))) { - SMP_UNLOCK(ets_table); + term pos = term_get_tuple_element(op, 0); + term incr = term_get_tuple_element(op, 1); + + if (!term_is_integer(pos) || !term_is_integer(incr)) { return EtsBadEntry; } - term position_term; - 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; - if (UNLIKELY(!operation_to_tuple4(operation, default_pos, &position_term, &increment_term, &threshold_term, &set_value_term))) { - SMP_UNLOCK(ets_table); + avm_int_t index = term_to_int(pos) - 1; + if (index < 0 || index >= term_get_tuple_arity(tuple)) { 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)) { - SMP_UNLOCK(ets_table); + + if ((size_t) index == key_index) { return EtsBadEntry; } - term elem = term_get_tuple_element(to_insert, position); - if (UNLIKELY(!term_is_integer(elem))) { - SMP_UNLOCK(ets_table); + term value = term_get_tuple_element(tuple, (uint32_t) index); + + if (!term_is_integer(value)) { return EtsBadEntry; } - avm_int_t increment = term_to_int(increment_term); - avm_int_t elem_value; - if (BUILTIN_ADD_OVERFLOW_INT(increment, term_to_int(elem), &elem_value)) { - SMP_UNLOCK(ets_table); + + avm_int_t current = term_to_int(value); + avm_int_t delta = term_to_int(incr); + avm_int_t new_value; + if (BUILTIN_ADD_OVERFLOW_INT(current, delta, &new_value)) { return EtsOverflow; } - if (!term_is_invalid_term(threshold_term) && !term_is_invalid_term(set_value_term)) { - avm_int_t threshold = term_to_int(threshold_term); - avm_int_t 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; + if (arity == 4) { + term threshold = term_get_tuple_element(op, 2); + term setvalue = term_get_tuple_element(op, 3); + + if (!term_is_integer(threshold) || !term_is_integer(setvalue)) { + return EtsBadEntry; } - } - 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); - if (insert_result == EtsOk) { - *ret = final_value; + avm_int_t thresh = term_to_int(threshold); + avm_int_t setval = term_to_int(setvalue); + + if ((delta >= 0 && new_value > thresh) || (delta < 0 && new_value < thresh)) { + new_value = setval; + } } - SMP_UNLOCK(ets_table); - return insert_result; + + term_put_tuple_element(tuple, index, term_from_int(new_value)); + *ret = new_value; + + return EtsOk; } diff --git a/src/libAtomVM/ets.h b/src/libAtomVM/ets.h index 5eb2fa17e0..5fdc0c96cf 100644 --- a/src/libAtomVM/ets.h +++ b/src/libAtomVM/ets.h @@ -2,6 +2,7 @@ * This file is part of AtomVM. * * Copyright 2024 Fred Dushin + * Copyright 2025 Mateusz Furga * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +22,11 @@ #ifndef _ETS_H_ #define _ETS_H_ -struct Context; +#include + struct GlobalContext; +struct Context; -#include "list.h" #include "synclist.h" #include "term.h" @@ -32,7 +34,7 @@ struct GlobalContext; extern "C" { #endif -// N.B. Only EtsTableSet currently supported +// NOTE: Ordered set is not currently supported typedef enum EtsTableType { EtsTableSet, @@ -41,46 +43,53 @@ typedef enum EtsTableType EtsTableDuplicateBag } EtsTableType; -typedef enum EtsAccessType +typedef enum EtsTableAccess { - EtsAccessPrivate, - EtsAccessProtected, - EtsAccessPublic -} EtsAccessType; + EtsTableAccessPrivate, + EtsTableAccessProtected, + EtsTableAccessPublic +} EtsTableAccess; -typedef enum EtsErrorCode +typedef enum EtsStatus { EtsOk, - EtsBadAccess, - EtsTableNameInUse, + EtsKeyExists, + EtsTableNameExists, + EtsTupleNotExists, EtsBadEntry, - EtsAllocationFailure, - EtsEntryNotFound, - EtsBadPosition, + EtsBadAccess, + EtsBadIndex, + EtsAllocationError, EtsOverflow -} EtsErrorCode; -struct Ets +} EtsStatus; + +typedef struct Ets { - // TODO Using a list imposes O(len(ets_tables)) cost - // on lookup, so in the future we may want to consider - // a table or map instead of a list. struct SyncList ets_tables; -}; +} Ets; -void ets_init(struct Ets *ets); -void ets_destroy(struct Ets *ets, GlobalContext *global); +void ets_init(Ets *ets); +void ets_destroy(Ets *ets, GlobalContext *global); -EtsErrorCode ets_create_table_maybe_gc(term name, bool is_named, EtsTableType table_type, EtsAccessType access_type, size_t keypos, term *ret, Context *ctx); -void ets_delete_owned_tables(struct Ets *ets, int32_t process_id, GlobalContext *global); +EtsStatus ets_create_table_maybe_gc( + term name, + bool named, + EtsTableType type, + EtsTableAccess access, + size_t index, + term *ret, + Context *ctx); +void ets_delete_owned_tables(Ets *ets, int32_t process_id, GlobalContext *global); + +EtsStatus ets_lookup_maybe_gc(term name_or_ref, term key, term *ret, Context *ctx); +EtsStatus ets_lookup_element_maybe_gc(term name_or_ref, term key, size_t index, term *ret, Context *ctx); +EtsStatus ets_insert(term name_or_ref, term entry, bool as_new, Context *ctx); +EtsStatus ets_update_counter_maybe_gc(term name_or_ref, term key, term op, term default_tuple, term *ret, Context *ctx); +EtsStatus ets_delete(term name_or_ref, term key, Context *ctx); +EtsStatus ets_delete_table(term name_or_ref, Context *ctx); -EtsErrorCode ets_insert(term ref, term entry, Context *ctx); -EtsErrorCode ets_lookup_maybe_gc(term ref, term key, term *ret, Context *ctx); -EtsErrorCode ets_lookup_element_maybe_gc(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_maybe_gc(term ref, term key, term value, term pos, term *ret, Context *ctx); -EtsErrorCode ets_drop_table(term ref, term *ret, Context *ctx); #ifdef __cplusplus } #endif -#endif +#endif // _ETS_H_ diff --git a/src/libAtomVM/ets_hashtable.c b/src/libAtomVM/ets_hashtable.c deleted file mode 100644 index 50bd859c3e..0000000000 --- a/src/libAtomVM/ets_hashtable.c +++ /dev/null @@ -1,416 +0,0 @@ -/* - * This file is part of AtomVM. - * - * Copyright 2024 Fred Dushin - * - * 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 - */ - -#include "ets_hashtable.h" - -#include "smp.h" -#include "term.h" -#include "utils.h" - -#include -#include - -// #define TRACE_ENABLED -#include "trace.h" - -struct HNode -{ - struct HNode *next; - term key; - term entry; - Heap heap; -}; - -static uint32_t hash_term(term t, GlobalContext *global); - -struct EtsHashTable *ets_hashtable_new(void) -{ - struct EtsHashTable *htable = malloc(sizeof(struct EtsHashTable)); - if (IS_NULL_PTR(htable)) { - return NULL; - } - - memset(htable->buckets, 0, NUM_BUCKETS * sizeof(struct HNode *)); - htable->capacity = NUM_BUCKETS; - - return htable; -} - -void ets_hashtable_free_node(struct HNode *node, GlobalContext *global) -{ - memory_destroy_heap(&node->heap, global); - free(node); -} - -void ets_hashtable_destroy(struct EtsHashTable *hash_table, GlobalContext *global) -{ - for (size_t i = 0; i < hash_table->capacity; ++i) { - struct HNode *node = hash_table->buckets[i]; - while (node != NULL) { - struct HNode *next_node = node->next; - ets_hashtable_free_node(node, global); - node = next_node; - } - } -} - -#ifdef TRACE_ENABLED -static void print_info(struct EtsHashTable *hash_table) -{ - fprintf(stderr, "============\n"); - for (size_t i = 0; i < hash_table->capacity; ++i) { - size_t len = 0; - struct HNode *node = hash_table->buckets[i]; - while (node) { - node = node->next; - ++len; - } - fprintf(stderr, "len bucket[%zu]: %zu\n", i, len); - } -} -#endif - -struct HNode *ets_hashtable_new_node(term entry, int keypos) -{ - struct HNode *new_node = malloc(sizeof(struct HNode)); - if (IS_NULL_PTR(new_node)) { - goto cleanup; - } - - size_t size = memory_estimate_usage(entry); - 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); - - new_node->next = NULL; - new_node->key = key; - new_node->entry = new_entry; - - return new_node; - -cleanup: - 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; - uint32_t hash = hash_term(key, global); - uint32_t index = hash % hash_table->capacity; - -#ifdef TRACE_ENABLED - fprintf(stderr, "hash=%u index=%i key=", hash, index); - term_fprint(stderr, key, global); - fprintf(stderr, "\n"); -#endif - - struct HNode *node = hash_table->buckets[index]; - struct HNode *last_node = NULL; - while (node) { - TermCompareResult cmp = term_compare(key, node->key, TermCompareExact, global); - if (UNLIKELY(cmp == TermCompareMemoryAllocFail)) { - return EtsHashtableOutOfMemory; - } - - if (cmp == TermEquals) { - if (opts & EtsHashtableAllowOverwrite) { - if (IS_NULL_PTR(last_node)) { - new_node->next = node->next; - hash_table->buckets[index] = new_node; - } else { - last_node->next = new_node; - new_node->next = node->next; - } - ets_hashtable_free_node(node, global); - return EtsHashtableOk; - } else { - return EtsHashtableKeyAlreadyExists; - } - } - last_node = node; - node = node->next; - } - - if (last_node) { - last_node->next = new_node; - } else { - hash_table->buckets[index] = new_node; - } - -#ifdef TRACE_ENABLED - print_info(hash_table); -#endif - - return EtsHashtableOk; -} - -term ets_hashtable_lookup(struct EtsHashTable *hash_table, term key, size_t keypos, 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) { - return node->entry; - } - node = node->next; - } - - return term_nil(); -} - -bool ets_hashtable_remove(struct EtsHashTable *hash_table, term key, size_t keypos, GlobalContext *global) -{ - uint32_t hash = hash_term(key, global); - uint32_t index = hash % hash_table->capacity; - - 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) { - struct HNode *next_node = node->next; - ets_hashtable_free_node(node, global); - if (prev_node != NULL) { - prev_node->next = next_node; - } else { - hash_table->buckets[index] = next_node; - } - return true; - } else { - prev_node = node; - node = node->next; - } - } - - return false; -} - -// -// hash function -// -// Conceptually similar to (but not identical to) the `make_hash` algorithm described in -// https://github.com/erlang/otp/blob/cbd1378ee1fde835e55614bac9290b281bafe49a/erts/emulator/beam/utils.c#L644 -// -// Also described in character folding algorithm (PJW Hash) -// https://en.wikipedia.org/wiki/Hash_function#Character_folding -// -// TODO: implement erlang:phash2 using the OTP algorithm -// - -// some large (close to 2^24) primes taken from -// http://compoasso.free.fr/primelistweb/page/prime/liste_online_en.php - -#define LARGE_PRIME_INITIAL 16777259 -#define LARGE_PRIME_ATOM 16777643 -#define LARGE_PRIME_INTEGER 16777781 -#define LARGE_PRIME_FLOAT 16777973 -#define LARGE_PRIME_PID 16778147 -#define LARGE_PRIME_REF 16778441 -#define LARGE_PRIME_BINARY 16780483 -#define LARGE_PRIME_TUPLE 16778821 -#define LARGE_PRIME_LIST 16779179 -#define LARGE_PRIME_MAP 16779449 -#define LARGE_PRIME_PORT 16778077 - -static uint32_t hash_atom(term t, int32_t h, GlobalContext *global) -{ - size_t len; - const uint8_t *data = atom_table_get_atom_string(global->atom_table, term_to_atom_index(t), &len); - for (size_t i = 0; i < len; ++i) { - h = h * LARGE_PRIME_ATOM + data[i]; - } - return h * LARGE_PRIME_ATOM; -} - -static uint32_t hash_integer(term t, int32_t h, GlobalContext *global) -{ - UNUSED(global); - uint64_t n = (uint64_t) term_maybe_unbox_int64(t); - while (n) { - h = h * LARGE_PRIME_INTEGER + (n & 0xFF); - n >>= 8; - } - return h * LARGE_PRIME_INTEGER; -} - -static uint32_t hash_float(term t, int32_t h, GlobalContext *global) -{ - UNUSED(global); - avm_float_t f = term_to_float(t); - uint8_t *data = (uint8_t *) &f; - size_t len = sizeof(float); - for (size_t i = 0; i < len; ++i) { - h = h * LARGE_PRIME_FLOAT + data[i]; - } - return h * LARGE_PRIME_FLOAT; -} - -static uint32_t hash_local_pid(term t, int32_t h, GlobalContext *global) -{ - UNUSED(global); - uint32_t n = (uint32_t) term_to_local_process_id(t); - while (n) { - h = h * LARGE_PRIME_PID + (n & 0xFF); - n >>= 8; - } - return h * LARGE_PRIME_PID; -} - -static uint32_t hash_local_port(term t, int32_t h, GlobalContext *global) -{ - UNUSED(global); - uint32_t n = (uint32_t) term_to_local_process_id(t); - while (n) { - h = h * LARGE_PRIME_PORT + (n & 0xFF); - n >>= 8; - } - return h * LARGE_PRIME_PORT; -} - -static uint32_t hash_external_pid(term t, int32_t h, GlobalContext *global) -{ - UNUSED(global); - uint32_t n = (uint32_t) term_get_external_pid_process_id(t); - while (n) { - h = h * LARGE_PRIME_PID + (n & 0xFF); - n >>= 8; - } - return h * LARGE_PRIME_PID; -} - -static uint32_t hash_external_port(term t, int32_t h, GlobalContext *global) -{ - UNUSED(global); - uint32_t n = (uint32_t) term_get_external_port_number(t); - while (n) { - h = h * LARGE_PRIME_PORT + (n & 0xFF); - n >>= 8; - } - return h * LARGE_PRIME_PORT; -} - -static uint32_t hash_local_reference(term t, int32_t h, GlobalContext *global) -{ - UNUSED(global); - uint64_t n = term_to_ref_ticks(t); - while (n) { - h = h * LARGE_PRIME_REF + (n & 0xFF); - n >>= 8; - } - return h * LARGE_PRIME_REF; -} - -static uint32_t hash_external_reference(term t, int32_t h, GlobalContext *global) -{ - UNUSED(global); - uint32_t l = term_get_external_reference_len(t); - const uint32_t *words = term_get_external_reference_words(t); - for (uint32_t i = 0; i < l; i++) { - uint32_t n = words[i]; - while (n) { - h = h * LARGE_PRIME_REF + (n & 0xFF); - n >>= 8; - } - } - return h * LARGE_PRIME_REF; -} - -static uint32_t hash_binary(term t, int32_t h, GlobalContext *global) -{ - UNUSED(global); - size_t len = (size_t) term_binary_size(t); - uint8_t *data = (uint8_t *) term_binary_data(t); - for (size_t i = 0; i < len; ++i) { - h = h * LARGE_PRIME_BINARY + data[i]; - } - return h * LARGE_PRIME_BINARY; -} - -static uint32_t hash_term_incr(term t, int32_t h, GlobalContext *global) -{ - if (term_is_atom(t)) { - return hash_atom(t, h, global); - } else if (term_is_any_integer(t)) { - return hash_integer(t, h, global); - } else if (term_is_float(t)) { - return hash_float(t, h, global); - } else if (term_is_local_pid(t)) { - return hash_local_pid(t, h, global); - } else if (term_is_external_pid(t)) { - return hash_external_pid(t, h, global); - } else if (term_is_local_port(t)) { - return hash_local_port(t, h, global); - } else if (term_is_external_port(t)) { - return hash_external_port(t, h, global); - } else if (term_is_local_reference(t)) { - return hash_local_reference(t, h, global); - } else if (term_is_external_reference(t)) { - return hash_external_reference(t, h, global); - } else if (term_is_binary(t)) { - return hash_binary(t, h, global); - } else if (term_is_tuple(t)) { - size_t arity = term_get_tuple_arity(t); - for (size_t i = 0; i < arity; ++i) { - term elt = term_get_tuple_element(t, (int) i); - h = h * LARGE_PRIME_TUPLE + hash_term_incr(elt, h, global); - } - return h * LARGE_PRIME_TUPLE; - } else if (term_is_list(t)) { - while (!term_is_nonempty_list(t)) { - term elt = term_get_list_head(t); - h = h * LARGE_PRIME_LIST + hash_term_incr(elt, h, global); - t = term_get_list_tail(t); - if (term_is_nil(t)) { - h = h * LARGE_PRIME_LIST; - break; - } else if (!term_is_list(t)) { - h = h * LARGE_PRIME_LIST + hash_term_incr(elt, h, global); - break; - } - } - return h * LARGE_PRIME_TUPLE; - } else if (term_is_map(t)) { - size_t size = term_get_map_size(t); - for (size_t i = 0; i < size; ++i) { - term key = term_get_map_key(t, (avm_uint_t) i); - h = h * LARGE_PRIME_MAP + hash_term_incr(key, h, global); - term value = term_get_map_value(t, (avm_uint_t) i); - h = h * LARGE_PRIME_MAP + hash_term_incr(value, h, global); - } - return h * LARGE_PRIME_MAP; - } else { - fprintf(stderr, "hash_term: unsupported term type\n"); - return h; - } -} - -static uint32_t hash_term(term t, GlobalContext *global) -{ - return hash_term_incr(t, LARGE_PRIME_INITIAL, global); -} diff --git a/src/libAtomVM/ets_hashtable.h b/src/libAtomVM/ets_hashtable.h deleted file mode 100644 index 0951a1c6ec..0000000000 --- a/src/libAtomVM/ets_hashtable.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This file is part of AtomVM. - * - * Copyright 2024 Fred Dushin - * - * 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 - */ - -#ifndef _ETS_HASHTABLE_H_ -#define _ETS_HASHTABLE_H_ - -#include "globalcontext.h" -#include "term.h" -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define NUM_BUCKETS 16 - -struct EtsHashTable -{ - size_t capacity; - struct HNode *buckets[NUM_BUCKETS]; -}; - -typedef enum EtsHashtableOptions -{ - EtsHashtableAllowOverwrite = 1 -} EtsHashtableOptions; - -typedef enum EtsHashtableStatus -{ - EtsHashtableOk = 0, - EtsHashtableKeyAlreadyExists, - EtsHashtableOutOfMemory -} EtsHashtableStatus; - -struct EtsHashTable *ets_hashtable_new(void); -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); -void ets_hashtable_free_node(struct HNode *node, GlobalContext *global); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/src/libAtomVM/ets_multimap.c b/src/libAtomVM/ets_multimap.c new file mode 100644 index 0000000000..ded6b57bc6 --- /dev/null +++ b/src/libAtomVM/ets_multimap.c @@ -0,0 +1,752 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2025 Mateusz Furga + * + * 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 + */ + +#include +#include + +#include "globalcontext.h" +#include "term.h" + +#include "ets_multimap.h" + +#define DYNARRAY_INITIAL_CAPACITY 8 +#define DYNARRAY_GROWTH_FACTOR 2 + +static uint32_t hash_term(term t, GlobalContext *global); + +static EtsMultimapEntry *entry_new(term tuple); +static void entry_delete(EtsMultimapEntry *entry, GlobalContext *global); +static EtsMultimapNode *node_new(EtsMultimapNode *next, EtsMultimapEntry *entries); +static void node_delete(EtsMultimapNode *node, GlobalContext *global); +static EtsStatus node_find( + EtsMultimap *multimap, + term key, + EtsMultimapNode **out_node, + GlobalContext *global); +static term node_key(EtsMultimap *multimap, EtsMultimapNode *node); +static void multimap_to_single(EtsMultimap *multimap, GlobalContext *global); +static void insert_revert( + EtsMultimap *multimap, + EtsMultimapEntry **entries, + size_t count, + GlobalContext *global); +static EtsStatus tuple_exists( + EtsMultimapNode *node, + term tuple, + bool *exists, + GlobalContext *global); + +EtsMultimap *ets_multimap_new(EtsMultimapType type, size_t key_index) +{ + EtsMultimap *multimap = malloc(sizeof(struct EtsMultimap)); + if (IS_NULL_PTR(multimap)) { + return NULL; + } + + multimap->type = type; + multimap->key_index = key_index; + + for (size_t i = 0; i < ETS_MULTIMAP_NUM_BUCKETS; i++) { + multimap->buckets[i] = NULL; + } + + return multimap; +} + +void ets_multimap_delete(EtsMultimap *multimap, GlobalContext *global) +{ + for (size_t i = 0; i < ETS_MULTIMAP_NUM_BUCKETS; i++) { + EtsMultimapNode *node = multimap->buckets[i]; + while (node != NULL) { + EtsMultimapNode *next = node->next; + node_delete(node, global); + node = next; + } + } + free(multimap); +} + +EtsStatus ets_multimap_lookup( + EtsMultimap *multimap, + term key, + term **tuples, + size_t *count, + GlobalContext *global) +{ + assert(count != NULL); + + *count = 0; + + EtsMultimapNode *node; + EtsStatus result = node_find(multimap, key, &node, global); + if (UNLIKELY(result == EtsAllocationError)) { + return result; + } + + if (node == NULL) { + return EtsOk; + } + + assert(node->entries != NULL); + + for (EtsMultimapEntry *iter = node->entries; iter != NULL; iter = iter->next) { + (*count)++; + } + + if (tuples == NULL) { + // only return number of tuples found + return EtsOk; + } + + *tuples = malloc(sizeof(term) * (*count)); + if (IS_NULL_PTR(*tuples)) { + return EtsAllocationError; + } + + size_t i = 0; + for (EtsMultimapEntry *iter = node->entries; iter != NULL; iter = iter->next, i++) { + assert(i < *count); + (*tuples)[i] = iter->tuple; + } + + return EtsOk; +} + +EtsStatus ets_multimap_insert( + EtsMultimap *multimap, + term *tuples, + size_t count, + GlobalContext *global) +{ + if (tuples == NULL || count == 0) { + return EtsOk; + } + + EtsMultimapEntry **entries = malloc(sizeof(EtsMultimapEntry *) * count); + if (IS_NULL_PTR(entries)) { + return EtsAllocationError; + } + + for (size_t i = 0; i < count; i++) { + entries[i] = entry_new(tuples[i]); + if (IS_NULL_PTR(entries[i])) { + for (size_t j = 0; j < i; j++) { + entry_delete(entries[j], global); + } + free(entries); + return EtsAllocationError; + } + } + + EtsStatus status = EtsOk; + + for (size_t i = 0; i < count; i++) { + EtsMultimapEntry *entry = entries[i]; + term key = term_get_tuple_element(entry->tuple, multimap->key_index); + + EtsMultimapNode *node; + if (UNLIKELY(node_find(multimap, key, &node, global) == EtsAllocationError)) { + status = EtsAllocationError; + break; + } + + if (node == NULL) { + EtsMultimapNode *new_node = node_new(NULL, entry); + if (IS_NULL_PTR(new_node)) { + status = EtsAllocationError; + break; + } + + assert(new_node->entries != NULL); + + uint32_t idx = hash_term(key, global) % ETS_MULTIMAP_NUM_BUCKETS; + new_node->next = multimap->buckets[idx]; + multimap->buckets[idx] = new_node; + continue; + } + + assert(node->entries != NULL); + + if (multimap->type == EtsMultimapTypeSet) { + bool exists; + + if (UNLIKELY(tuple_exists(node, entry->tuple, &exists, global) == EtsAllocationError)) { + status = EtsAllocationError; + break; + } + + if (exists) { + entry_delete(entry, global); + entries[i] = NULL; + continue; + } + } + + entry->next = node->entries; + node->entries = entry; + } + + if (status != EtsOk) { + insert_revert(multimap, entries, count, global); + } else if (multimap->type == EtsMultimapTypeSingle) { + multimap_to_single(multimap, global); + } + + free(entries); + + return status; +} + +EtsStatus ets_multimap_remove( + EtsMultimap *multimap, + term key, + GlobalContext *global) +{ + EtsMultimapNode *node; + if (UNLIKELY(node_find(multimap, key, &node, global) == EtsAllocationError)) { + return EtsAllocationError; + } + + if (node == NULL) { + return EtsOk; + } + + assert(node->entries != NULL); + assert(term_compare(key, node_key(multimap, node), TermCompareExact, global) == TermEquals); + + uint32_t idx = hash_term(key, global) % ETS_MULTIMAP_NUM_BUCKETS; + EtsMultimapNode *iter = multimap->buckets[idx]; + EtsMultimapNode *prev = NULL; + + while (iter) { + if (iter == node) { + if (prev == NULL) { + multimap->buckets[idx] = iter->next; + } else { + prev->next = iter->next; + } + break; + } + prev = iter; + iter = iter->next; + } + + node_delete(node, global); + + return EtsOk; +} + +EtsStatus ets_multimap_remove_tuple( + EtsMultimap *multimap, + term tuple, + GlobalContext *global) +{ + assert(term_is_tuple(tuple)); + + if (multimap->key_index >= (size_t) term_get_tuple_arity(tuple)) { + return EtsBadEntry; + } + + term key = term_get_tuple_element(tuple, multimap->key_index); + + EtsMultimapNode *node; + if (UNLIKELY(node_find(multimap, key, &node, global) == EtsAllocationError)) { + return EtsAllocationError; + } + + if (node == NULL) { + return EtsOk; + } + + assert(node->entries != NULL); + + size_t capacity = DYNARRAY_INITIAL_CAPACITY; + size_t count = 0; + + EtsMultimapEntry **to_remove = malloc(sizeof(EtsMultimapEntry *) * capacity); + if (IS_NULL_PTR(to_remove)) { + return EtsAllocationError; + } + + for (EtsMultimapEntry *iter = node->entries; iter != NULL; iter = iter->next) { + TermCompareResult result = term_compare(tuple, iter->tuple, TermCompareExact, global); + + if (UNLIKELY(result == TermCompareMemoryAllocFail)) { + free(to_remove); + return EtsAllocationError; + } + + if (result == TermEquals) { + if (count >= capacity) { + capacity *= DYNARRAY_GROWTH_FACTOR; + EtsMultimapEntry **new_to_remove = realloc(to_remove, sizeof(EtsMultimapEntry *) * capacity); + if (IS_NULL_PTR(new_to_remove)) { +// GCC 12 is raising here a false positive warning, according to man realloc: +// "If realloc() fails, the original block is left untouched; it is not freed or moved." +#pragma GCC diagnostic push +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ == 12 +#pragma GCC diagnostic ignored "-Wuse-after-free" +#endif + free(to_remove); +#pragma GCC diagnostic pop + return EtsAllocationError; + } + to_remove = new_to_remove; + } + to_remove[count++] = iter; + } + } + + EtsMultimapEntry *prev = NULL; + for (EtsMultimapEntry *iter = node->entries; iter != NULL; iter = iter->next) { + bool removed = false; + for (size_t i = 0; i < count; i++) { + if (iter == to_remove[i]) { + if (prev == NULL) { + node->entries = iter->next; + } else { + prev->next = iter->next; + } + removed = true; + break; + } + } + if (!removed) { + prev = iter; + } + } + + for (size_t i = 0; i < count; i++) { + entry_delete(to_remove[i], global); + } + + if (node->entries == NULL) { + uint32_t idx = hash_term(key, global) % ETS_MULTIMAP_NUM_BUCKETS; + + EtsMultimapNode *prev_node = NULL; + for (EtsMultimapNode *iter = multimap->buckets[idx]; iter != NULL; prev_node = iter, iter = iter->next) { + if (iter == node) { + if (prev_node == NULL) { + multimap->buckets[idx] = iter->next; + } else { + prev_node->next = iter->next; + } + break; + } + } + + node_delete(node, global); + } + + free(to_remove); + + return EtsOk; +} + +static EtsStatus node_find( + EtsMultimap *multimap, + term key, + EtsMultimapNode **out_node, + GlobalContext *global) +{ + assert(out_node != NULL); + + *out_node = NULL; + + uint32_t idx = hash_term(key, global) % ETS_MULTIMAP_NUM_BUCKETS; + EtsMultimapNode *node = multimap->buckets[idx]; + + while (node) { + TermCompareResult result = term_compare(key, node_key(multimap, node), TermCompareExact, global); + + if (UNLIKELY(result == TermCompareMemoryAllocFail)) { + return EtsAllocationError; + } + + if (result == TermEquals) { + *out_node = node; + return EtsOk; + } + + node = node->next; + } + + return EtsOk; +} + +// Revert a partial batch insert. Assumes that newly inserted items are always at the head. +static void insert_revert( + EtsMultimap *multimap, + EtsMultimapEntry **entries, + size_t count, + GlobalContext *global) +{ + for (size_t i = 0; i < ETS_MULTIMAP_NUM_BUCKETS; i++) { + EtsMultimapNode *node = multimap->buckets[i]; + + while (node != NULL) { + EtsMultimapNode *next_node = node->next; + EtsMultimapEntry *entry = node->entries; + + assert(entry != NULL); + + while (entry != NULL) { + EtsMultimapEntry *next_entry = entry->next; + + for (size_t j = 0; j < count; j++) { + if (entry == entries[j]) { + node->entries = next_entry; + } + } + + entry = next_entry; + } + + if (node->entries == NULL) { + multimap->buckets[i] = next_node; + node_delete(node, global); + } + + node = next_node; + } + } + + for (size_t i = 0; i < count; i++) { + if (entries[i] != NULL) { + entry_delete(entries[i], global); + } + } +} + +static void multimap_to_single(EtsMultimap *multimap, GlobalContext *global) +{ + for (size_t i = 0; i < ETS_MULTIMAP_NUM_BUCKETS; i++) { + for (EtsMultimapNode *node = multimap->buckets[i]; node != NULL; node = node->next) { + assert(node->entries != NULL); + EtsMultimapEntry *entry = node->entries->next; + + while (entry != NULL) { + EtsMultimapEntry *next = entry->next; + entry_delete(entry, global); + entry = next; + } + + node->entries->next = NULL; + } + } +} + +static EtsStatus tuple_exists( + EtsMultimapNode *node, + term tuple, + bool *exists, + GlobalContext *global) +{ + for (EtsMultimapEntry *iter = node->entries; iter != NULL; iter = iter->next) { + TermCompareResult result = term_compare(tuple, iter->tuple, TermCompareExact, global); + + if (UNLIKELY(result == TermCompareMemoryAllocFail)) { + return EtsAllocationError; + } + + if (result == TermEquals) { + *exists = true; + return EtsOk; + } + } + + *exists = false; + return EtsOk; +} + +static term node_key(EtsMultimap *multimap, EtsMultimapNode *node) +{ + EtsMultimapEntry *entry = node->entries; + assert(entry != NULL); + return term_get_tuple_element(entry->tuple, multimap->key_index); +} + +static EtsMultimapNode *node_new(EtsMultimapNode *next, EtsMultimapEntry *entries) +{ + EtsMultimapNode *node = malloc(sizeof(EtsMultimapNode)); + if (IS_NULL_PTR(node)) { + return NULL; + } + node->next = next; + node->entries = entries; + return node; +} + +static EtsMultimapEntry *entry_new(term tuple) +{ + EtsMultimapEntry *entry = malloc(sizeof(EtsMultimapEntry)); + if (IS_NULL_PTR(entry)) { + return NULL; + } + + Heap *heap = malloc(sizeof(Heap)); + if (IS_NULL_PTR(heap)) { + free(entry); + return NULL; + } + + size_t size = memory_estimate_usage(tuple); + if (UNLIKELY(memory_init_heap(heap, size) != MEMORY_GC_OK)) { + free(entry); + free(heap); + return NULL; + } + + tuple = memory_copy_term_tree(heap, tuple); + + entry->tuple = tuple; + entry->heap = heap; + entry->next = NULL; + + return entry; +} + +static void node_delete(EtsMultimapNode *node, GlobalContext *global) +{ + EtsMultimapEntry *entry = node->entries; + + while (entry != NULL) { + EtsMultimapEntry *next = entry->next; + entry_delete(entry, global); + entry = next; + } + + free(node); +} + +static void entry_delete(EtsMultimapEntry *entry, GlobalContext *global) +{ + memory_destroy_heap(entry->heap, global); + free(entry->heap); + free(entry); +} + +// +// hash function +// +// Conceptually similar to (but not identical to) the `make_hash` algorithm described in +// https://github.com/erlang/otp/blob/cbd1378ee1fde835e55614bac9290b281bafe49a/erts/emulator/beam/utils.c#L644 +// +// Also described in character folding algorithm (PJW Hash) +// https://en.wikipedia.org/wiki/Hash_function#Character_folding +// +// TODO: implement erlang:phash2 using the OTP algorithm +// + +// some large (close to 2^24) primes taken from +// http://compoasso.free.fr/primelistweb/page/prime/liste_online_en.php + +#define LARGE_PRIME_INITIAL 16777259 +#define LARGE_PRIME_ATOM 16777643 +#define LARGE_PRIME_INTEGER 16777781 +#define LARGE_PRIME_FLOAT 16777973 +#define LARGE_PRIME_PID 16778147 +#define LARGE_PRIME_REF 16778441 +#define LARGE_PRIME_BINARY 16780483 +#define LARGE_PRIME_TUPLE 16778821 +#define LARGE_PRIME_LIST 16779179 +#define LARGE_PRIME_MAP 16779449 +#define LARGE_PRIME_PORT 16778077 + +static uint32_t hash_atom(term t, uint32_t h, GlobalContext *global) +{ + size_t len; + const uint8_t *data = atom_table_get_atom_string(global->atom_table, term_to_atom_index(t), &len); + for (size_t i = 0; i < len; ++i) { + h = h * LARGE_PRIME_ATOM + data[i]; + } + return h * LARGE_PRIME_ATOM; +} + +static uint32_t hash_integer(term t, uint32_t h, GlobalContext *global) +{ + UNUSED(global); + uint64_t n = (uint64_t) term_maybe_unbox_int64(t); + while (n) { + h = h * LARGE_PRIME_INTEGER + (n & 0xFF); + n >>= 8; + } + return h * LARGE_PRIME_INTEGER; +} + +static uint32_t hash_float(term t, uint32_t h, GlobalContext *global) +{ + UNUSED(global); + avm_float_t f = term_to_float(t); + // Normalize -0.0 to +0.0 so that hash is consistent with term_compare (-0.0 == +0.0). + if (f == 0.0) { + f = 0.0; + } + uint8_t *data = (uint8_t *) &f; + size_t len = sizeof(avm_float_t); + for (size_t i = 0; i < len; ++i) { + h = h * LARGE_PRIME_FLOAT + data[i]; + } + return h * LARGE_PRIME_FLOAT; +} + +static uint32_t hash_local_pid(term t, uint32_t h, GlobalContext *global) +{ + UNUSED(global); + uint32_t n = (uint32_t) term_to_local_process_id(t); + while (n) { + h = h * LARGE_PRIME_PID + (n & 0xFF); + n >>= 8; + } + return h * LARGE_PRIME_PID; +} + +static uint32_t hash_local_port(term t, uint32_t h, GlobalContext *global) +{ + UNUSED(global); + uint32_t n = (uint32_t) term_to_local_process_id(t); + while (n) { + h = h * LARGE_PRIME_PORT + (n & 0xFF); + n >>= 8; + } + return h * LARGE_PRIME_PORT; +} + +static uint32_t hash_external_pid(term t, uint32_t h, GlobalContext *global) +{ + UNUSED(global); + uint32_t n = (uint32_t) term_get_external_pid_process_id(t); + while (n) { + h = h * LARGE_PRIME_PID + (n & 0xFF); + n >>= 8; + } + return h * LARGE_PRIME_PID; +} + +static uint32_t hash_external_port(term t, uint32_t h, GlobalContext *global) +{ + UNUSED(global); + uint32_t n = (uint32_t) term_get_external_port_number(t); + while (n) { + h = h * LARGE_PRIME_PORT + (n & 0xFF); + n >>= 8; + } + return h * LARGE_PRIME_PORT; +} + +static uint32_t hash_local_reference(term t, uint32_t h, GlobalContext *global) +{ + UNUSED(global); + uint64_t n = term_to_ref_ticks(t); + while (n) { + h = h * LARGE_PRIME_REF + (n & 0xFF); + n >>= 8; + } + return h * LARGE_PRIME_REF; +} + +static uint32_t hash_external_reference(term t, uint32_t h, GlobalContext *global) +{ + UNUSED(global); + uint32_t l = term_get_external_reference_len(t); + const uint32_t *words = term_get_external_reference_words(t); + for (uint32_t i = 0; i < l; i++) { + uint32_t n = words[i]; + while (n) { + h = h * LARGE_PRIME_REF + (n & 0xFF); + n >>= 8; + } + } + return h * LARGE_PRIME_REF; +} + +static uint32_t hash_binary(term t, uint32_t h, GlobalContext *global) +{ + UNUSED(global); + size_t len = (size_t) term_binary_size(t); + uint8_t *data = (uint8_t *) term_binary_data(t); + for (size_t i = 0; i < len; ++i) { + h = h * LARGE_PRIME_BINARY + data[i]; + } + return h * LARGE_PRIME_BINARY; +} + +static uint32_t hash_term_incr(term t, uint32_t h, GlobalContext *global) +{ + if (term_is_atom(t)) { + return hash_atom(t, h, global); + } else if (term_is_any_integer(t)) { + return hash_integer(t, h, global); + } else if (term_is_float(t)) { + return hash_float(t, h, global); + } else if (term_is_local_pid(t)) { + return hash_local_pid(t, h, global); + } else if (term_is_external_pid(t)) { + return hash_external_pid(t, h, global); + } else if (term_is_local_port(t)) { + return hash_local_port(t, h, global); + } else if (term_is_external_port(t)) { + return hash_external_port(t, h, global); + } else if (term_is_local_reference(t)) { + return hash_local_reference(t, h, global); + } else if (term_is_external_reference(t)) { + return hash_external_reference(t, h, global); + } else if (term_is_binary(t)) { + return hash_binary(t, h, global); + } else if (term_is_tuple(t)) { + size_t arity = term_get_tuple_arity(t); + for (size_t i = 0; i < arity; ++i) { + term elt = term_get_tuple_element(t, (int) i); + h = h * LARGE_PRIME_TUPLE + hash_term_incr(elt, h, global); + } + return h * LARGE_PRIME_TUPLE; + } else if (term_is_list(t)) { + while (term_is_nonempty_list(t)) { + term elt = term_get_list_head(t); + h = h * LARGE_PRIME_LIST + hash_term_incr(elt, h, global); + t = term_get_list_tail(t); + if (term_is_nil(t)) { + h = h * LARGE_PRIME_LIST; + break; + } else if (!term_is_list(t)) { + h = h * LARGE_PRIME_LIST + hash_term_incr(t, h, global); + break; + } + } + return h * LARGE_PRIME_LIST; + } else if (term_is_map(t)) { + size_t size = term_get_map_size(t); + for (size_t i = 0; i < size; ++i) { + term key = term_get_map_key(t, (avm_uint_t) i); + h = h * LARGE_PRIME_MAP + hash_term_incr(key, h, global); + term value = term_get_map_value(t, (avm_uint_t) i); + h = h * LARGE_PRIME_MAP + hash_term_incr(value, h, global); + } + return h * LARGE_PRIME_MAP; + } else { + fprintf(stderr, "hash_term: unsupported term type\n"); + return h; + } +} + +static uint32_t hash_term(term t, GlobalContext *global) +{ + return hash_term_incr(t, LARGE_PRIME_INITIAL, global); +} diff --git a/src/libAtomVM/ets_multimap.h b/src/libAtomVM/ets_multimap.h new file mode 100644 index 0000000000..3c60dc105c --- /dev/null +++ b/src/libAtomVM/ets_multimap.h @@ -0,0 +1,147 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2025 Mateusz Furga + * + * 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 + */ + +#ifndef _ETS_MULTIMAP_H_ +#define _ETS_MULTIMAP_H_ + +#include "globalcontext.h" +#include "term.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define ETS_MULTIMAP_NUM_BUCKETS 16 + +typedef enum EtsMultimapType +{ + EtsMultimapTypeSingle, // Only one value per key + EtsMultimapTypeSet, // Only unique values per key + EtsMultimapTypeList // Allow duplicate values per key +} EtsMultimapType; + +typedef struct EtsMultimap +{ + EtsMultimapType type; + size_t key_index; + struct EtsMultimapNode *buckets[ETS_MULTIMAP_NUM_BUCKETS]; +} EtsMultimap; + +typedef struct EtsMultimapNode +{ + struct EtsMultimapNode *next; + struct EtsMultimapEntry *entries; +} EtsMultimapNode; + +typedef struct EtsMultimapEntry +{ + struct EtsMultimapEntry *next; + term tuple; + Heap *heap; +} EtsMultimapEntry; + +/** + * @brief Create a new multimap. + * + * @param type the multimap type + * @param index the index of the tuple element to use as key + * @return the created multimap, or NULL on error + */ +EtsMultimap *ets_multimap_new(EtsMultimapType type, size_t index); + +/** + * @brief Delete the multimap and all its contents. + * + * @param multimap the multimap + * @param global the global context + */ +void ets_multimap_delete(EtsMultimap *multimap, GlobalContext *global); + +/** + * @brief Lookup tuples by key. + * + * @param multimap the multimap + * @param key the key to lookup + * @param[out] tuples the found tuples (or NULL to only get the count) + * @param[out] count the number of found tuples; must not be NULL + * @param global the global context + * @return EtsOk on success, otherwise an error status + * + * @note Terms returned by this function come from the ETS heap and should be copied + * to the process heap if needed. + * @note The returned tuples are ordered in reverse insertion order + * (most recently added elements first). + * @warning The caller is responsible for freeing the memory pointed to by `tuples` + * using `free()`. When count is zero, memory is not allocated. + */ +EtsStatus ets_multimap_lookup( + EtsMultimap *multimap, + term key, + term **tuples, + size_t *count, + GlobalContext *global); + +/** + * @brief Insert one or more tuples into the multimap. + * + * @param multimap the multimap + * @param tuples the tuples to insert + * @param count the number of tuples to insert + * @return EtsOk on success, otherwise an error status + * + * @note Terms passed to this function will be copied to the ETS heap. + */ +EtsStatus ets_multimap_insert( + EtsMultimap *multimap, + term *tuples, + size_t count, + GlobalContext *global); + +/** + * @brief Remove all tuples with the given key. + * + * @param multimap the multimap + * @param key the key to lookup + * @param global the global context + * @return EtsOk on success, otherwise an error status + */ +EtsStatus ets_multimap_remove( + EtsMultimap *multimap, + term key, + GlobalContext *global); + +/** + * @brief Remove a given tuple from the multimap. + * + * @param multimap the multimap + * @param tuple the tuple to remove + * @param global the global context + * @return EtsOk on success, otherwise an error status + */ +EtsStatus ets_multimap_remove_tuple( + EtsMultimap *multimap, + term tuple, + GlobalContext *global); + +#ifdef __cplusplus +} +#endif + +#endif // _ETS_MULTIMAP_H_ diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 42abf74289..02f5115fc0 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -221,6 +221,7 @@ static term nif_erlang_throw(Context *ctx, int argc, term argv[]); static term nif_erlang_raise(Context *ctx, int argc, term argv[]); static term nif_ets_new(Context *ctx, int argc, term argv[]); static term nif_ets_insert(Context *ctx, int argc, term argv[]); +static term nif_ets_insert_new(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[]); @@ -729,6 +730,11 @@ static const struct Nif ets_insert_nif = { .nif_ptr = nif_ets_insert }; +static const struct Nif ets_insert_new_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_insert_new +}; + static const struct Nif ets_lookup_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_ets_lookup @@ -3771,177 +3777,258 @@ static term nif_erlang_raise(Context *ctx, int argc, term argv[]) static term nif_ets_new(Context *ctx, int argc, term argv[]) { - UNUSED(argc); + assert(argc == 2); 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); - if (term_to_int(keypos) < 1) { - RAISE_ERROR(BADARG_ATOM); - } + VALIDATE_VALUE(keypos, term_is_pos_int); + + size_t key_index = term_to_int(keypos) - 1; 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); - EtsAccessType access = EtsAccessProtected; + // NOTE: If multiple accesses are specified, the precedence is: public > private > protected + EtsTableAccess access = EtsTableAccessProtected; if (!term_is_invalid_term(private)) { - access = EtsAccessPrivate; - } else if (!term_is_invalid_term(public)) { - access = EtsAccessPublic; + access = EtsTableAccessPrivate; + } + if (!term_is_invalid_term(public)) { + access = EtsTableAccessPublic; + } + + term bag = interop_kv_get_value(options, ATOM_STR("\x3", "bag"), ctx->global); + term duplicate_bag = interop_kv_get_value(options, ATOM_STR("\xd", "duplicate_bag"), ctx->global); + + // NOTE: If multiple table types are specified, the precedence is: duplicate_bag > bag > set + EtsTableType type = EtsTableSet; + if (!term_is_invalid_term(bag)) { + type = EtsTableBag; + } + if (!term_is_invalid_term(duplicate_bag)) { + type = EtsTableDuplicateBag; } 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); + + EtsStatus result = ets_create_table_maybe_gc( + name, + is_named == TRUE_ATOM, + type, + access, + key_index, + &table, + ctx); + switch (result) { case EtsOk: return table; - case EtsTableNameInUse: + case EtsTableNameExists: RAISE_ERROR(BADARG_ATOM); - case EtsAllocationFailure: + case EtsAllocationError: RAISE_ERROR(OUT_OF_MEMORY_ATOM); default: + // unreachable AVM_ABORT(); } } static inline bool is_ets_table_id(term t) { - return term_is_local_reference(t) || term_is_atom(t); + return term_is_reference(t) || term_is_atom(t); } static term nif_ets_insert(Context *ctx, int argc, term argv[]) { - UNUSED(argc); + assert(argc == 2); - term ref = argv[0]; - VALIDATE_VALUE(ref, is_ets_table_id); + term name_or_ref = argv[0]; + term entry = argv[1]; + + VALIDATE_VALUE(name_or_ref, is_ets_table_id); + + EtsStatus result = ets_insert(name_or_ref, entry, false, ctx); + switch (result) { + case EtsOk: + return TRUE_ATOM; + case EtsBadAccess: + case EtsBadEntry: + RAISE_ERROR(BADARG_ATOM); + case EtsAllocationError: + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + default: + // unreachable + AVM_ABORT(); + } +} + +static term nif_ets_insert_new(Context *ctx, int argc, term argv[]) +{ + assert(argc == 2); + + term name_or_ref = argv[0]; term entry = argv[1]; - EtsErrorCode result = ets_insert(ref, entry, ctx); + VALIDATE_VALUE(name_or_ref, is_ets_table_id); + + EtsStatus result = ets_insert(name_or_ref, entry, true, ctx); + switch (result) { case EtsOk: return TRUE_ATOM; + case EtsKeyExists: + return FALSE_ATOM; case EtsBadAccess: case EtsBadEntry: RAISE_ERROR(BADARG_ATOM); - case EtsAllocationFailure: + case EtsAllocationError: RAISE_ERROR(OUT_OF_MEMORY_ATOM); default: + // unreachable AVM_ABORT(); } } static term nif_ets_lookup(Context *ctx, int argc, term argv[]) { - UNUSED(argc); - - term ref = argv[0]; - VALIDATE_VALUE(ref, is_ets_table_id); + assert(argc == 2); + term name_or_ref = argv[0]; term key = argv[1]; + VALIDATE_VALUE(name_or_ref, is_ets_table_id); + term ret = term_invalid_term(); - EtsErrorCode result = ets_lookup_maybe_gc(ref, key, &ret, ctx); + + EtsStatus result = ets_lookup_maybe_gc(name_or_ref, key, &ret, ctx); + switch (result) { case EtsOk: return ret; + case EtsTupleNotExists: + return term_nil(); case EtsBadAccess: RAISE_ERROR(BADARG_ATOM); - case EtsAllocationFailure: + case EtsAllocationError: RAISE_ERROR(OUT_OF_MEMORY_ATOM); default: + // unreachable AVM_ABORT(); } } -static term nif_ets_lookup_element(Context *ctx, int argc, term argv[]) +static term nif_ets_update_counter(Context *ctx, int argc, term argv[]) { - UNUSED(argc); - - term ref = argv[0]; - VALIDATE_VALUE(ref, is_ets_table_id); + assert(argc == 3 || argc == 4); + term name_or_ref = argv[0]; term key = argv[1]; - term pos = argv[2]; - VALIDATE_VALUE(pos, term_is_integer); + term operation = argv[2]; + + VALIDATE_VALUE(name_or_ref, is_ets_table_id); + + term default_tuple = term_invalid_term(); + if (argc == 4) { + default_tuple = argv[3]; + VALIDATE_VALUE(default_tuple, term_is_tuple); + } term ret = term_invalid_term(); - EtsErrorCode result = ets_lookup_element_maybe_gc(ref, key, term_to_int(pos), &ret, ctx); + + EtsStatus result = ets_update_counter_maybe_gc(name_or_ref, key, operation, default_tuple, &ret, ctx); + switch (result) { case EtsOk: return ret; - case EtsEntryNotFound: - case EtsBadPosition: case EtsBadAccess: + case EtsBadEntry: + case EtsTupleNotExists: RAISE_ERROR(BADARG_ATOM); - case EtsAllocationFailure: + case EtsAllocationError: RAISE_ERROR(OUT_OF_MEMORY_ATOM); + case EtsOverflow: + RAISE_ERROR(OVERFLOW_ATOM); default: + // unreachable AVM_ABORT(); } } -static term nif_ets_delete(Context *ctx, int argc, term argv[]) +static term nif_ets_lookup_element(Context *ctx, int argc, term argv[]) { - term ref = argv[0]; - VALIDATE_VALUE(ref, is_ets_table_id); - term ret = term_invalid_term(); - EtsErrorCode result; - if (argc == 2) { - term key = argv[1]; - result = ets_delete(ref, key, &ret, ctx); - } else { - result = ets_drop_table(ref, &ret, ctx); + assert(argc == 3 || argc == 4); + + term name_or_ref = argv[0]; + term key = argv[1]; + term pos = argv[2]; + + VALIDATE_VALUE(name_or_ref, is_ets_table_id); + VALIDATE_VALUE(pos, term_is_pos_int); + + size_t index = term_to_int(pos) - 1; + + term default_value = term_invalid_term(); + if (argc == 4) { + default_value = argv[3]; } + term ret = term_invalid_term(); + + EtsStatus result = ets_lookup_element_maybe_gc(name_or_ref, key, index, &ret, ctx); + switch (result) { case EtsOk: return ret; + case EtsTupleNotExists: + if (!term_is_invalid_term(default_value)) { + return default_value; + } + RAISE_ERROR(BADARG_ATOM); case EtsBadAccess: + case EtsBadIndex: RAISE_ERROR(BADARG_ATOM); - case EtsAllocationFailure: + case EtsAllocationError: RAISE_ERROR(OUT_OF_MEMORY_ATOM); default: + // unreachable AVM_ABORT(); } } -static term nif_ets_update_counter(Context *ctx, int argc, term argv[]) +static term nif_ets_delete(Context *ctx, int argc, term argv[]) { - term ref = argv[0]; - VALIDATE_VALUE(ref, is_ets_table_id); + assert(argc == 1 || argc == 2); - term key = argv[1]; - term operation = argv[2]; - term default_value; - if (argc == 4) { - default_value = argv[3]; - VALIDATE_VALUE(default_value, term_is_tuple); - term_put_tuple_element(default_value, 0, key); + term name_or_ref = argv[0]; + + VALIDATE_VALUE(name_or_ref, is_ets_table_id); + + EtsStatus result; + + if (argc == 1) { + result = ets_delete_table(name_or_ref, ctx); } else { - default_value = term_invalid_term(); + result = ets_delete(name_or_ref, argv[1], ctx); } - term ret; - EtsErrorCode result = ets_update_counter_maybe_gc(ref, key, operation, default_value, &ret, ctx); + switch (result) { case EtsOk: - return ret; + return TRUE_ATOM; case EtsBadAccess: - case EtsBadEntry: RAISE_ERROR(BADARG_ATOM); - case EtsAllocationFailure: + case EtsAllocationError: RAISE_ERROR(OUT_OF_MEMORY_ATOM); - case EtsOverflow: - RAISE_ERROR(OVERFLOW_ATOM); default: - UNREACHABLE(); + // unreachable + AVM_ABORT(); } } diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index 60b57e528c..f58148b79c 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -153,12 +153,14 @@ 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 +file:get_cwd/0, IF_HAVE_GETCWD_PATHMAX(&file_get_cwd_nif) ets:new/2, &ets_new_nif ets:insert/2, &ets_insert_nif +ets:insert_new/2, &ets_insert_new_nif ets:lookup/2, &ets_lookup_nif ets:lookup_element/3, &ets_lookup_element_nif +ets:lookup_element/4, &ets_lookup_element_nif ets:delete/1, &ets_delete_nif -file:get_cwd/0, IF_HAVE_GETCWD_PATHMAX(&file_get_cwd_nif) ets:delete/2, &ets_delete_nif ets:update_counter/3, &ets_update_counter_nif ets:update_counter/4, &ets_update_counter_nif diff --git a/tests/erlang_tests/test_ets.erl b/tests/erlang_tests/test_ets.erl index 2b5963d671..02148b8b03 100644 --- a/tests/erlang_tests/test_ets.erl +++ b/tests/erlang_tests/test_ets.erl @@ -27,10 +27,11 @@ start() -> ok = isolated(fun test_permissions/0), ok = isolated(fun test_keys/0), ok = isolated(fun test_keypos/0), - ok = isolated(fun test_insert/0), - ok = isolated(fun test_delete/0), ok = isolated(fun test_lookup_element/0), + ok = isolated(fun test_insert/0), + ok = isolated(fun test_insert_new/0), ok = isolated(fun test_update_counter/0), + ok = isolated(fun test_delete/0), 0. test_ets_new() -> @@ -49,19 +50,16 @@ test_ets_new() -> assert_badarg(fun() -> ets:new(keypos_test, [{keypos, -1}]) end), ets:new(type_test, [set]), + ets:new(type_test, [bag]), + ets:new(type_test, [duplicate_bag]), % 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 - end, + if_otp_version(23, fun() -> ets:new(decent_counters_test, [{decentralized_counters, true}]) end), ets:new(compressed_test, [compressed]), ok. @@ -124,6 +122,7 @@ test_keys() -> ok = assert_stored_key(T, <<"bin">>), ok = assert_stored_key(T, <<"">>), ok = assert_stored_key(T, {ok, 1}), + ok = assert_stored_key(T, []), ok = assert_stored_key(T, [x, self(), 1.0]), ok = assert_stored_key(T, [improper | list]), ok = assert_stored_key(T, #{ @@ -148,103 +147,407 @@ test_keypos() -> assert_badarg(fun() -> ets:insert(T, {value}) end), ok. -test_insert() -> - T = ets:new(test, []), - [] = ets:lookup(T, key), - true = ets:insert(T, {key, value}), - [{key, value}] = ets:lookup(T, key), - % Overwrite - true = ets:insert(T, {key, new_value}), - [{key, new_value}] = ets:lookup(T, key), - - TList = ets:new(test, []), - true = ets:insert(TList, []), - true = ets:insert(TList, [{key, value}, {key2, value2}]), - true = ets:insert(TList, [{key2, new_value2}, {key3, value3}]), - [{key, value}] = ets:lookup(TList, key), - [{key2, new_value2}] = ets:lookup(TList, key2), - [{key3, value3}] = ets:lookup(TList, key3), - - TErr = ets:new(test, []), - assert_badarg(fun() -> ets:insert(TErr, {}) end), - assert_badarg(fun() -> ets:insert(TErr, [{}]) end), - assert_badarg(fun() -> ets:insert(TErr, [{key, value}, not_a_tuple]) end), - assert_badarg(fun() -> ets:insert(TErr, [{improper, true} | {list, true}]) end), - assert_badarg(fun() -> ets:insert(TErr, not_a_tuple) end), - assert_badarg(fun() -> ets:insert([not_a_ref], {key, value}) end), - [] = ets:lookup(TErr, key), - [] = ets:lookup(TErr, improper), - [] = ets:lookup(TErr, list), - ok. +test_lookup_element() -> + PosKey = 1, + PosValue = 2, + + AssertBadArgs = + fun(Tab) -> + PosPastBounds = 3, + PosZero = 0, + PosNegative = -1, + + assert_badarg(fun() -> ets:lookup_element(Tab, key_not_exist, PosKey) end), + assert_badarg(fun() -> ets:lookup_element(Tab, key, PosZero) end), + assert_badarg(fun() -> ets:lookup_element(Tab, key, PosPastBounds) end), + assert_badarg(fun() -> ets:lookup_element(Tab, key, PosNegative) end), + % lookup_element/4 with Default since OTP 26.0 + if_otp_version(26, fun() -> + assert_badarg(fun() -> ets:lookup_element(Tab, key, PosNegative, default) end) + end) + end, -test_delete() -> - T = ets:new(test, []), + % Set + S = new_table([{key, value}]), + key = ets:lookup_element(S, key, PosKey), + value = ets:lookup_element(S, key, PosValue), + % lookup_element/4 with Default since OTP 26.0 + if_otp_version(26, fun() -> default = ets:lookup_element(S, key_not_exist, PosKey, default) end), + + % Bag + B = new_table(bag, [{key, value}, {key, value2}]), + [key, key] = ets:lookup_element(B, key, PosKey), + [value, value2] = ets:lookup_element(B, key, PosValue), + if_otp_version(26, fun() -> default = ets:lookup_element(B, key_not_exist, PosKey, default) end), + + true = ets:insert(B, {key, value3}), + [key, key, key] = ets:lookup_element(B, key, PosKey), + [value, value2, value3] = ets:lookup_element(B, key, PosValue), + + % Duplicate bag + DB = new_table(duplicate_bag, [{key, value}, {key, value}]), + [key, key] = ets:lookup_element(DB, key, PosKey), + [value, value] = ets:lookup_element(DB, key, PosValue), + if_otp_version(26, fun() -> default = ets:lookup_element(DB, key_not_exist, PosKey, default) end), + + true = ets:insert(DB, {key, value2}), + [key, key, key] = ets:lookup_element(DB, key, PosKey), + [value, value, value2] = ets:lookup_element(DB, key, PosValue), + + true = ets:insert(DB, {key2, value2}), + [key2] = ets:lookup_element(DB, key2, PosKey), + [value2] = ets:lookup_element(DB, key2, PosValue), + + % Badargs + assert_badarg(fun() -> ets:lookup_element(bad_table, key, 1) end), + AssertBadArgs(ets:new(test, [set])), + AssertBadArgs(ets:new(test, [bag])), + AssertBadArgs(ets:new(test, [duplicate_bag])), - % Not existing - true = ets:delete(T, key), - [] = ets:lookup(T, key), - - % Keep key2 - true = ets:insert(T, {key, value}), - true = ets:insert(T, {key2, value2}), - [{key, value}] = ets:lookup(T, key), - true = ets:delete(T, key), - [] = ets:lookup(T, key), - [{key2, value2}] = ets:lookup(T, key2), + ok. - % Re-add key - true = ets:insert(T, {key, new_value}), - [{key, new_value}] = ets:lookup(T, key), +test_insert() -> + % Set + S1 = ets:new(test, []), + [] = ets:lookup(S1, key), + + % {Key, Value} + true = ets:insert(S1, {key, value}), + [{key, value}] = ets:lookup(S1, key), + true = ets:insert(S1, {key, new_value}), + [{key, new_value}] = ets:lookup(S1, key), + + % [{Key, Value}, ...] + S2 = ets:new(test, []), + true = ets:insert(S2, []), + true = ets:insert(S2, [{key, value}, {key2, value2}]), + true = ets:insert(S2, [{key2, new_value2}, {key3, value3}]), + [{key, value}] = ets:lookup(S2, key), + [{key2, new_value2}] = ets:lookup(S2, key2), + [{key3, value3}] = ets:lookup(S2, key3), + + % Bag + B1 = ets:new(test, [bag]), + [] = ets:lookup(B1, key), + + % {Key, Value} + true = ets:insert(B1, {key, value}), + [{key, value}] = ets:lookup(B1, key), + true = ets:insert(B1, {key, new_value}), + [{key, value}, {key, new_value}] = ets:lookup(B1, key), + true = ets:insert(B1, {key, new_value}), + [{key, value}, {key, new_value}] = ets:lookup(B1, key), + + % [{Key, Value}, ...] + B2 = ets:new(test, [bag]), + true = ets:insert(B2, []), + true = ets:insert(B2, [{key, value}, {key2, value2}]), + true = ets:insert(B2, [{key2, new_value2}, {key3, value3}]), + true = ets:insert(B2, [{key2, new_value2}, {key3, new_value3}]), + [{key, value}] = ets:lookup(B2, key), + [{key2, value2}, {key2, new_value2}] = ets:lookup(B2, key2), + [{key3, value3}, {key3, new_value3}] = ets:lookup(B2, key3), + + % Duplicate bag + DB1 = ets:new(test, [duplicate_bag]), + [] = ets:lookup(DB1, key), + + % {Key, Value} + true = ets:insert(DB1, {key, value}), + [{key, value}] = ets:lookup(DB1, key), + true = ets:insert(DB1, {key, new_value}), + [{key, value}, {key, new_value}] = ets:lookup(DB1, key), + true = ets:insert(DB1, {key, new_value}), + [{key, value}, {key, new_value}, {key, new_value}] = ets:lookup(DB1, key), + + % [{Key, Value}, ...] + DB2 = ets:new(test, [duplicate_bag]), + true = ets:insert(DB2, []), + true = ets:insert(DB2, [{key, value}, {key2, value2}]), + true = ets:insert(DB2, [{key2, new_value2}, {key3, value3}]), + true = ets:insert(DB2, [{key2, new_value2}, {key3, new_value3}]), + [{key, value}] = ets:lookup(DB2, key), + [{key2, value2}, {key2, new_value2}, {key2, new_value2}] = ets:lookup(DB2, key2), + [{key3, value3}, {key3, new_value3}] = ets:lookup(DB2, key3), + + % Badargs + assert_insert_badargs(ets:new(test, []), fun ets:insert/2), + assert_insert_badargs(ets:new(test, [bag]), fun ets:insert/2), + assert_insert_badargs(ets:new(test, [duplicate_bag]), fun ets:insert/2), - % Drop entire table - true = ets:delete(T), - assert_badarg(fun() -> ets:lookup(T, key) end), - assert_badarg(fun() -> ets:delete(T) end), - assert_badarg(fun() -> ets:delete(none) end), ok. -test_lookup_element() -> - T = ets:new(test, []), - true = ets:insert(T, {key, value}), - key = ets:lookup_element(T, key, 1), - value = ets:lookup_element(T, key, 2), - assert_badarg(fun() -> ets:lookup_element(T, none, 1) end), - assert_badarg(fun() -> ets:lookup_element(T, key, 3) end), - assert_badarg(fun() -> ets:lookup_element(T, key, 0) end), - assert_badarg(fun() -> ets:lookup_element(T, key, -1) end), +test_insert_new() -> + % Set + S1 = ets:new(test, []), + [] = ets:lookup(S1, key), + + % {Key, Value} + true = ets:insert_new(S1, {key, value}), + [{key, value}] = ets:lookup(S1, key), + false = ets:insert_new(S1, {key, new_value}), + [{key, value}] = ets:lookup(S1, key), + + % [{Key, Value}, ...] + S2 = ets:new(test, []), + true = ets:insert_new(S2, []), + true = ets:insert_new(S2, [{key, value}, {key2, value2}]), + false = ets:insert_new(S2, [{key2, new_value2}, {key3, value3}]), + [{key, value}] = ets:lookup(S2, key), + [{key2, value2}] = ets:lookup(S2, key2), + [] = ets:lookup(S2, key3), + + % Bag + B1 = ets:new(test, [bag]), + [] = ets:lookup(B1, key), + + % {Key, Value} + true = ets:insert_new(B1, {key, value}), + [{key, value}] = ets:lookup(B1, key), + false = ets:insert_new(B1, {key, new_value}), + [{key, value}] = ets:lookup(B1, key), + + % [{Key, Value}, ...] + B2 = ets:new(test, [bag]), + true = ets:insert_new(B2, []), + true = ets:insert_new(B2, [{key, value}, {key2, value2}]), + false = ets:insert_new(B2, [{key2, new_value2}, {key3, value3}]), + true = ets:insert_new(B2, [{key4, value4}, {key3, value3}, {key4, new_value4}]), + [{key, value}] = ets:lookup(B2, key), + [{key2, value2}] = ets:lookup(B2, key2), + [{key3, value3}] = ets:lookup(B2, key3), + [{key4, value4}, {key4, new_value4}] = ets:lookup(B2, key4), + + % Duplicate bag + DB1 = ets:new(test, [duplicate_bag]), + [] = ets:lookup(DB1, key), + + % {Key, Value} + true = ets:insert_new(DB1, {key, value}), + [{key, value}] = ets:lookup(DB1, key), + false = ets:insert_new(DB1, {key, new_value}), + [{key, value}] = ets:lookup(DB1, key), + + % [{Key, Value}, ...] + DB2 = ets:new(test, [duplicate_bag]), + true = ets:insert_new(DB2, []), + true = ets:insert_new(DB2, [{key, value}, {key2, value2}]), + false = ets:insert_new(DB2, [{key2, new_value2}, {key3, value3}]), + true = ets:insert_new(DB2, [{key4, value4}, {key3, value3}, {key4, value4}]), + [{key, value}] = ets:lookup(DB2, key), + [{key2, value2}] = ets:lookup(DB2, key2), + [{key3, value3}] = ets:lookup(DB2, key3), + [{key4, value4}, {key4, value4}] = ets:lookup(DB2, key4), + + % Badargs + assert_insert_badargs(ets:new(test, []), fun ets:insert_new/2), + assert_insert_badargs(ets:new(test, [bag]), fun ets:insert_new/2), + assert_insert_badargs(ets:new(test, [duplicate_bag]), fun ets:insert_new/2), + ok. test_update_counter() -> - T = ets:new(test, []), - true = ets:insert(T, {key, 10, 20, 30}), % Increment - 15 = ets:update_counter(T, key, 5), - 10 = ets:update_counter(T, key, -5), + S1 = new_table({key, 10, not_number, 30}), + 15 = ets:update_counter(S1, key, 5), + [{key, 15, not_number, 30}] = ets:lookup(S1, key), + + S2 = new_table(3, {not_number, 20, key, 30}), + -5 = ets:update_counter(S2, key, -35), + [{not_number, 20, key, -5}] = ets:lookup(S2, key), + % {Position, Increment} - 25 = ets:update_counter(T, key, {3, 5}), - 20 = ets:update_counter(T, key, {3, -5}), + S3 = new_table({key, 10, 20, not_number}), + 25 = ets:update_counter(S3, key, {3, 5}), + [{key, 10, 25, not_number}] = ets:lookup(S3, key), + + S4 = new_table({key, 10, not_number, 30}), + 0 = ets:update_counter(S4, key, {2, -10}), + [{key, 0, not_number, 30}] = ets:lookup(S4, key), + + % [] + S5 = new_table({key, 10, not_number, 30}), + [] = ets:update_counter(S5, key, []), + [{key, 10, not_number, 30}] = ets:lookup(S5, key), + + % [{Position, Increment}, ...] + S6 = new_table({key, 10, 20, not_number}), + [0, 5, 30] = ets:update_counter(S6, key, [{2, -10}, {2, 5}, {3, 10}]), + [{key, 5, 30, not_number}] = ets:lookup(S6, key), + % {Position, Increment, Threshold, SetValue} - 31 = ets:update_counter(T, key, {4, 10, 39, 31}), - 30 = ets:update_counter(T, key, {4, -10, 30, 30}), - - TErr = ets:new(test, []), - true = ets:insert(TErr, {key, 0, not_number}), - 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), - assert_badarg(fun() -> ets:update_counter(TErr, not_number, {1, 10}) end), - assert_badarg(fun() -> ets:update_counter(TErr, not_number, {1, 10, 100, 0}) end), - 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), + S7 = new_table({key, not_number, 20, 30}), + 31 = ets:update_counter(S7, key, {4, 10, 39, 31}), + [{key, not_number, 20, 31}] = ets:lookup(S7, key), + + S8 = new_table({key, 10, not_number, 30}), + 29 = ets:update_counter(S8, key, {4, -10, 21, 29}), + [{key, 10, not_number, 29}] = ets:lookup(S8, key), + + % [{Position, Increment, Threshold, SetValue}, ...] + S9 = new_table({key, 10, 20, not_number}), + [20, 31, 26] = ets:update_counter( + S9, + key, + [{2, 10, 20, 21}, {2, 10, 29, 31}, {3, 5, 24, 26}] + ), + [{key, 31, 26, not_number}] = ets:lookup(S9, key), + + % Default object + S10 = new_table({key, 10, 20, not_number}), + 15 = ets:update_counter(S10, key_not_exist, {2, 5}, {key, 10, 20, 30}), + [{key, 10, 20, not_number}] = ets:lookup(S10, key), + [{key_not_exist, 15, 20, 30}] = ets:lookup(S10, key_not_exist), + + % [] with default object + S11 = new_table({key, 10, 20, not_number}), + [] = ets:update_counter(S11, key_not_exist2, [], {key, 10, 20, 30}), + [{key, 10, 20, not_number}] = ets:lookup(S11, key), + [{key_not_exist2, 10, 20, 30}] = ets:lookup(S11, key_not_exist2), + + % Badargs + % The table type is not set + TErrBag = ets:new(test, [bag]), + TErrDuplBag = ets:new(test, [duplicate_bag]), + assert_badarg(fun() -> ets:update_counter(TErrBag, key, 10) end), + assert_badarg(fun() -> ets:update_counter(TErrDuplBag, key, 10) end), + assert_badarg(fun() -> ets:update_counter(bad_table, key, 10) end), + + TErr = new_table(2, {0, key, not_number}), + + % Pos > KeyPos + TErrLastKey = new_table(2, {0, key}), + assert_badarg(fun() -> ets:update_counter(TErrLastKey, key, 10) end), + + % No object with the correct key exists and no default object was supplied + assert_badarg(fun() -> ets:update_counter(TErr, key_not_exist, 10) end), + assert_badarg(fun() -> ets:update_counter(TErr, key_not_exist, {1, 10}) end), + assert_badarg(fun() -> ets:update_counter(TErr, key_not_exist, {1, 10, 20, 30}) end), + + % The object has the wrong arity + % Pos > TupleArity + assert_badarg(fun() -> ets:update_counter(TErr, key, {4, 10}) end), + + % Default object arity < KeyPos + % NOTE: This fails on OTP, see https://github.com/erlang/otp/issues/10603 + if_atomvm(fun() -> + assert_badarg(fun() -> ets:update_counter(TErr, key_not_exist, [{1, 10}], {10}) end) + end), + + % Any field from the default object that is updated is not an integer + assert_badarg(fun() -> + ets:update_counter( + TErr, key_not_exist, {2, 10}, {0, key, not_number} + ) + end), + assert_badarg(fun() -> + ets:update_counter( + TErr, key_not_exist, {3, 10}, {0, key, not_number} + ) + end), + + % The element to update is not an integer + assert_badarg(fun() -> ets:update_counter(TErr, key, 10) end), assert_badarg(fun() -> ets:update_counter(TErr, key, {3, 10}) end), + assert_badarg(fun() -> ets:update_counter(TErr, key, [{1, 10}, {3, 10}]) end), + + % The element to update is also the key + assert_badarg(fun() -> ets:update_counter(TErr, key, {2, 10}) end), + assert_badarg(fun() -> ets:update_counter(TErr, key, [{1, 10}, {2, 10}]) end), + + TErrIntKey = new_table(2, {10, 0}), + assert_badarg(fun() -> ets:update_counter(TErrIntKey, 0, {2, 10}) end), + assert_badarg(fun() -> ets:update_counter(TErrIntKey, 0, [{1, 10}, {2, 10}]) end), + [{10, 0}] = ets:lookup(TErrIntKey, 0), + + Key = 10, + assert_badarg(fun() -> + ets:update_counter( + TErr, key_not_exist, {2, 10}, {0, Key, not_number} + ) + end), + + % Any of Pos, Incr, Threshold, or SetValue is not an integer assert_badarg(fun() -> ets:update_counter(TErr, key, not_number) end), + assert_badarg(fun() -> ets:update_counter(TErr, key, {1, not_number}) end), assert_badarg(fun() -> ets:update_counter(TErr, key, {not_number, 10}) end), - assert_badarg(fun() -> ets:update_counter(TErr, key, {2, not_number}) end), - assert_badarg(fun() -> ets:update_counter(TErr, key, {not_number, 10, 100, 0}) end), - assert_badarg(fun() -> ets:update_counter(TErr, key, {2, not_number, 100, 0}) end), - assert_badarg(fun() -> ets:update_counter(TErr, key, {2, 10, not_number, 0}) end), - assert_badarg(fun() -> ets:update_counter(TErr, key, {2, 10, 100, not_number}) end), + assert_badarg(fun() -> ets:update_counter(TErr, key, [{1, 10}, {1, not_number}]) end), + assert_badarg(fun() -> ets:update_counter(TErr, key, [{1, 10}, {not_number, 10}]) end), + assert_badarg(fun() -> ets:update_counter(TErr, key, {1, 10, not_number, 30}) end), + assert_badarg(fun() -> ets:update_counter(TErr, key, {1, 10, 20, not_number}) end), + assert_badarg(fun() -> + ets:update_counter(TErr, key, [{1, 10, 20, 30}, {1, 10, not_number, 30}]) + end), + assert_badarg(fun() -> + ets:update_counter(TErr, key, [{1, 10, 20, 30}, {1, 10, 20, not_number}]) + end), + + [{0, key, not_number}] = ets:lookup(TErr, key), + + ok. + +test_delete() -> + % Set + S = new_table([{key, value}, {key2, value2}]), + true = ets:delete(S, key_not_exist), + + true = ets:delete(S, key), + [] = ets:lookup(S, key), + [{key2, value2}] = ets:lookup(S, key2), + + true = ets:delete(S, key2), + [] = ets:lookup(S, key2), + + true = ets:delete(S, key2), + true = ets:delete(S), + + % Bag + B = new_table(bag, [{key, value}, {key, value2}, {key2, value3}]), + true = ets:delete(B, key_not_exist), + + true = ets:delete(B, key), + [] = ets:lookup(B, key), + [{key2, value3}] = ets:lookup(B, key2), + + true = ets:delete(B, key2), + [] = ets:lookup(B, key2), + + true = ets:delete(B, key2), + true = ets:delete(B), + + % Duplicate bag + DB = new_table(duplicate_bag, [{key, value}, {key, value}, {key2, value2}]), + true = ets:delete(DB, key_not_exist), + + true = ets:delete(DB, key), + [] = ets:lookup(DB, key), + [{key2, value2}] = ets:lookup(DB, key2), + + true = ets:delete(DB, key2), + [] = ets:lookup(DB, key2), + + true = ets:delete(DB, key2), + true = ets:delete(DB), + + % Badargs + TErr = new_table(2, []), + true = ets:delete(TErr), + assert_badarg(fun() -> ets:delete(bad_table) end), + assert_badarg(fun() -> ets:lookup(TErr, key) end), + assert_badarg(fun() -> ets:delete(TErr) end), + + ok. + +assert_insert_badargs(T, Insert) -> + assert_badarg(fun() -> Insert(T, {}) end), + assert_badarg(fun() -> Insert(T, [{}]) end), + assert_badarg(fun() -> Insert(T, [{key, value}, not_a_tuple]) end), + assert_badarg(fun() -> Insert(T, [{improper, true} | {list, true}]) end), + assert_badarg(fun() -> Insert(T, not_a_tuple) end), + assert_badarg(fun() -> Insert([not_a_ref], {key, value}) end), + [] = ets:lookup(T, key), + [] = ets:lookup(T, improper), + [] = ets:lookup(T, list), ok. %%----------------------------------------------------------------------------- @@ -274,6 +577,18 @@ assert_stored_key(T, Key) -> [{Key, value}] = ets:lookup(T, Key), ok. +new_table(Tuples) -> + new_table([], Tuples). + +new_table(Type, Tuples) when is_atom(Type) -> + new_table([Type], Tuples); +new_table(Keypos, Tuples) when is_integer(Keypos) -> + new_table([{keypos, Keypos}], Tuples); +new_table(Opts, Tuples) when is_list(Opts) -> + T = ets:new(test, Opts), + true = ets:insert(T, Tuples), + T. + isolated(Fun) -> Ref = make_ref(), Self = self(), @@ -316,7 +631,7 @@ supports_v4_port_encoding() -> % small utf8 atom true; "BEAM" -> - OTP = otp_version(), + OTP = get_otp_version(), if OTP < 24 -> false; % v4 is supported but not the default @@ -326,6 +641,21 @@ supports_v4_port_encoding() -> end end. -otp_version() -> - OTPRelease = erlang:system_info(otp_release), - list_to_integer(OTPRelease). +get_otp_version() -> + case erlang:system_info(machine) of + "BEAM" -> list_to_integer(erlang:system_info(otp_release)); + _ -> atomvm + end. + +if_otp_version(MinVersion, Fun) -> + case get_otp_version() of + atomvm -> Fun(); + OTP when OTP >= MinVersion -> Fun(); + _ -> ok + end. + +if_atomvm(Fun) -> + case get_otp_version() of + atomvm -> Fun(); + _ -> ok + end. From 5d31fd34d82bdcbc629e59244667b537279a4040 Mon Sep 17 00:00:00 2001 From: Mateusz Furga Date: Tue, 3 Mar 2026 16:46:48 +0100 Subject: [PATCH 2/8] Implement member/2 Signed-off-by: Mateusz Furga --- src/libAtomVM/ets.c | 24 ++++++++++++++++++++++ src/libAtomVM/ets.h | 1 + src/libAtomVM/nifs.c | 36 +++++++++++++++++++++++++++++++-- src/libAtomVM/nifs.gperf | 5 +++-- tests/erlang_tests/test_ets.erl | 22 ++++++++++++++++++++ 5 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/libAtomVM/ets.c b/src/libAtomVM/ets.c index e88a5f3a86..0241833591 100644 --- a/src/libAtomVM/ets.c +++ b/src/libAtomVM/ets.c @@ -245,6 +245,30 @@ EtsStatus ets_lookup_element_maybe_gc(term name_or_ref, term key, size_t index, return result; } +EtsStatus ets_member(term name_or_ref, term key, Context *ctx) +{ + struct EtsTable *table = get_table( + &ctx->global->ets, + name_or_ref, + ctx->process_id, + TableAccessRead); + + if (table == NULL) { + return EtsBadAccess; + } + + size_t count; + EtsStatus result = ets_multimap_lookup(table->multimap, key, NULL, &count, ctx->global); + + if (result == EtsOk && count == 0) { + result = EtsTupleNotExists; + } + + SMP_UNLOCK(table); + + return result; +} + EtsStatus ets_insert(term name_or_ref, term entry, bool as_new, Context *ctx) { struct EtsTable *table = get_table( diff --git a/src/libAtomVM/ets.h b/src/libAtomVM/ets.h index 5fdc0c96cf..7d65af58d7 100644 --- a/src/libAtomVM/ets.h +++ b/src/libAtomVM/ets.h @@ -83,6 +83,7 @@ void ets_delete_owned_tables(Ets *ets, int32_t process_id, GlobalContext *global EtsStatus ets_lookup_maybe_gc(term name_or_ref, term key, term *ret, Context *ctx); EtsStatus ets_lookup_element_maybe_gc(term name_or_ref, term key, size_t index, term *ret, Context *ctx); +EtsStatus ets_member(term name_or_ref, term key, Context *ctx); EtsStatus ets_insert(term name_or_ref, term entry, bool as_new, Context *ctx); EtsStatus ets_update_counter_maybe_gc(term name_or_ref, term key, term op, term default_tuple, term *ret, Context *ctx); EtsStatus ets_delete(term name_or_ref, term key, Context *ctx); diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 02f5115fc0..b9bc6b750c 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -220,10 +220,11 @@ static term nif_erlang_split_binary(Context *ctx, int argc, term argv[]); static term nif_erlang_throw(Context *ctx, int argc, term argv[]); static term nif_erlang_raise(Context *ctx, int argc, term argv[]); static term nif_ets_new(Context *ctx, int argc, term argv[]); -static term nif_ets_insert(Context *ctx, int argc, term argv[]); -static term nif_ets_insert_new(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_member(Context *ctx, int argc, term argv[]); +static term nif_ets_insert(Context *ctx, int argc, term argv[]); +static term nif_ets_insert_new(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[]); @@ -745,6 +746,11 @@ static const struct Nif ets_lookup_element_nif = { .nif_ptr = nif_ets_lookup_element }; +static const struct Nif ets_member_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_member +}; + static const struct Nif ets_delete_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_ets_delete @@ -4003,6 +4009,32 @@ static term nif_ets_lookup_element(Context *ctx, int argc, term argv[]) } } +static term nif_ets_member(Context *ctx, int argc, term argv[]) +{ + assert(argc == 2); + + term name_or_ref = argv[0]; + term key = argv[1]; + + VALIDATE_VALUE(name_or_ref, is_ets_table_id); + + EtsStatus result = ets_member(name_or_ref, key, ctx); + + switch (result) { + case EtsOk: + return TRUE_ATOM; + case EtsTupleNotExists: + return FALSE_ATOM; + case EtsBadAccess: + RAISE_ERROR(BADARG_ATOM); + case EtsAllocationError: + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + default: + // unreachable + AVM_ABORT(); + } +} + static term nif_ets_delete(Context *ctx, int argc, term argv[]) { assert(argc == 1 || argc == 2); diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index f58148b79c..7f7ac1e4b8 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -155,11 +155,12 @@ erlang:list_to_bitstring/1, &list_to_bitstring_nif erts_debug:flat_size/1, &flat_size_nif file:get_cwd/0, IF_HAVE_GETCWD_PATHMAX(&file_get_cwd_nif) ets:new/2, &ets_new_nif -ets:insert/2, &ets_insert_nif -ets:insert_new/2, &ets_insert_new_nif ets:lookup/2, &ets_lookup_nif ets:lookup_element/3, &ets_lookup_element_nif ets:lookup_element/4, &ets_lookup_element_nif +ets:member/2, &ets_member_nif +ets:insert/2, &ets_insert_nif +ets:insert_new/2, &ets_insert_new_nif ets:delete/1, &ets_delete_nif ets:delete/2, &ets_delete_nif ets:update_counter/3, &ets_update_counter_nif diff --git a/tests/erlang_tests/test_ets.erl b/tests/erlang_tests/test_ets.erl index 02148b8b03..c581f4daf8 100644 --- a/tests/erlang_tests/test_ets.erl +++ b/tests/erlang_tests/test_ets.erl @@ -28,6 +28,7 @@ start() -> ok = isolated(fun test_keys/0), ok = isolated(fun test_keypos/0), ok = isolated(fun test_lookup_element/0), + ok = isolated(fun test_member/0), ok = isolated(fun test_insert/0), ok = isolated(fun test_insert_new/0), ok = isolated(fun test_update_counter/0), @@ -206,6 +207,27 @@ test_lookup_element() -> ok. +test_member() -> + % Set + S = new_table([{key, value}]), + true = ets:member(S, key), + false = ets:member(S, key_not_exist), + + % Bag + B = new_table(bag, [{key, value}, {key, value2}]), + true = ets:member(B, key), + false = ets:member(B, key_not_exist), + + % Duplicate bag + DB = new_table(duplicate_bag, [{key, value}, {key, value}]), + true = ets:member(DB, key), + false = ets:member(DB, key_not_exist), + + % Badargs + assert_badarg(fun() -> ets:member(bad_table, key) end), + + ok. + test_insert() -> % Set S1 = ets:new(test, []), From c8f066a6cee985ccee752a055bb7ab5b9236117a Mon Sep 17 00:00:00 2001 From: Mateusz Furga Date: Tue, 3 Mar 2026 17:07:59 +0100 Subject: [PATCH 3/8] Implement take/2 Signed-off-by: Mateusz Furga --- src/libAtomVM/ets.c | 23 +++++++++++++++++ src/libAtomVM/ets.h | 1 + src/libAtomVM/nifs.c | 44 +++++++++++++++++++++++++++++---- src/libAtomVM/nifs.gperf | 1 + tests/erlang_tests/test_ets.erl | 34 +++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 5 deletions(-) diff --git a/src/libAtomVM/ets.c b/src/libAtomVM/ets.c index 0241833591..bf16468fdb 100644 --- a/src/libAtomVM/ets.c +++ b/src/libAtomVM/ets.c @@ -409,6 +409,29 @@ EtsStatus ets_update_counter_maybe_gc( return result; } +EtsStatus ets_take_maybe_gc(term name_or_ref, term key, term *ret, Context *ctx) +{ + struct EtsTable *table = get_table( + &ctx->global->ets, + name_or_ref, + ctx->process_id, + TableAccessWrite); + + if (table == NULL) { + return EtsBadAccess; + } + + EtsStatus result = lookup_select_maybe_gc(table, key, ETS_WHOLE_TUPLE, 1, &key, ret, ctx); + + if (result == EtsOk) { + result = ets_multimap_remove(table->multimap, key, ctx->global); + } + + SMP_UNLOCK(table); + + return result; +} + EtsStatus ets_delete(term name_or_ref, term key, Context *ctx) { struct EtsTable *table = get_table( diff --git a/src/libAtomVM/ets.h b/src/libAtomVM/ets.h index 7d65af58d7..5503f56561 100644 --- a/src/libAtomVM/ets.h +++ b/src/libAtomVM/ets.h @@ -86,6 +86,7 @@ EtsStatus ets_lookup_element_maybe_gc(term name_or_ref, term key, size_t index, EtsStatus ets_member(term name_or_ref, term key, Context *ctx); EtsStatus ets_insert(term name_or_ref, term entry, bool as_new, Context *ctx); EtsStatus ets_update_counter_maybe_gc(term name_or_ref, term key, term op, term default_tuple, term *ret, Context *ctx); +EtsStatus ets_take_maybe_gc(term name_or_ref, term key, term *ret, Context *ctx); EtsStatus ets_delete(term name_or_ref, term key, Context *ctx); EtsStatus ets_delete_table(term name_or_ref, Context *ctx); diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index b9bc6b750c..7a1b124f52 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -225,8 +225,9 @@ static term nif_ets_lookup_element(Context *ctx, int argc, term argv[]); static term nif_ets_member(Context *ctx, int argc, term argv[]); static term nif_ets_insert(Context *ctx, int argc, term argv[]); static term nif_ets_insert_new(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_ets_take(Context *ctx, int argc, term argv[]); +static term nif_ets_delete(Context *ctx, int argc, term argv[]); static term nif_erlang_pid_to_list(Context *ctx, int argc, term argv[]); 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[]); @@ -751,14 +752,19 @@ static const struct Nif ets_member_nif = { .nif_ptr = nif_ets_member }; -static const struct Nif ets_delete_nif = { +static const struct Nif ets_update_counter_nif = { .base.type = NIFFunctionType, - .nif_ptr = nif_ets_delete + .nif_ptr = nif_ets_update_counter }; -static const struct Nif ets_update_counter_nif = { +static const struct Nif ets_take_nif = { .base.type = NIFFunctionType, - .nif_ptr = nif_ets_update_counter + .nif_ptr = nif_ets_take +}; + +static const struct Nif ets_delete_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_delete }; static const struct Nif atomvm_add_avm_pack_binary_nif = { @@ -3968,6 +3974,34 @@ static term nif_ets_update_counter(Context *ctx, int argc, term argv[]) } } +static term nif_ets_take(Context *ctx, int argc, term argv[]) +{ + assert(argc == 2); + + term name_or_ref = argv[0]; + term key = argv[1]; + + VALIDATE_VALUE(name_or_ref, is_ets_table_id); + + term ret = term_invalid_term(); + + EtsStatus result = ets_take_maybe_gc(name_or_ref, key, &ret, ctx); + + switch (result) { + case EtsOk: + return ret; + case EtsTupleNotExists: + return term_nil(); + case EtsBadAccess: + RAISE_ERROR(BADARG_ATOM); + case EtsAllocationError: + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + default: + // unreachable + AVM_ABORT(); + } +} + static term nif_ets_lookup_element(Context *ctx, int argc, term argv[]) { assert(argc == 3 || argc == 4); diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index 7f7ac1e4b8..f3865c6edd 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -161,6 +161,7 @@ ets:lookup_element/4, &ets_lookup_element_nif ets:member/2, &ets_member_nif ets:insert/2, &ets_insert_nif ets:insert_new/2, &ets_insert_new_nif +ets:take/2, &ets_take_nif ets:delete/1, &ets_delete_nif ets:delete/2, &ets_delete_nif ets:update_counter/3, &ets_update_counter_nif diff --git a/tests/erlang_tests/test_ets.erl b/tests/erlang_tests/test_ets.erl index c581f4daf8..921cd67fe4 100644 --- a/tests/erlang_tests/test_ets.erl +++ b/tests/erlang_tests/test_ets.erl @@ -32,6 +32,7 @@ start() -> ok = isolated(fun test_insert/0), ok = isolated(fun test_insert_new/0), ok = isolated(fun test_update_counter/0), + ok = isolated(fun test_take/0), ok = isolated(fun test_delete/0), 0. @@ -508,6 +509,39 @@ test_update_counter() -> ok. +test_take() -> + % Set + S1 = new_table([]), + [] = ets:take(S1, key_not_exist), + + S2 = new_table([{key, value}, {key2, value2}]), + [{key, value}] = ets:take(S2, key), + [] = ets:lookup(S2, key), + [{key2, value2}] = ets:lookup(S2, key2), + + % Bag + B1 = new_table(bag, []), + [] = ets:take(B1, key_not_exist), + + B2 = new_table(bag, [{key, value}, {key, value2}, {key2, value3}]), + [{key, value}, {key, value2}] = ets:take(B2, key), + [] = ets:lookup(B2, key), + [{key2, value3}] = ets:lookup(B2, key2), + + % Duplicate bag + DB1 = new_table(duplicate_bag, []), + [] = ets:take(DB1, key_not_exist), + + DB2 = new_table(duplicate_bag, [{key, value}, {key, value}, {key2, value2}]), + [{key, value}, {key, value}] = ets:take(DB2, key), + [] = ets:lookup(DB2, key), + [{key2, value2}] = ets:lookup(DB2, key2), + + % Badargs + assert_badarg(fun() -> ets:take(bad_table, key) end), + + ok. + test_delete() -> % Set S = new_table([{key, value}, {key2, value2}]), From 25329f46c5b04efe315a8d280096b61343abfbb5 Mon Sep 17 00:00:00 2001 From: Mateusz Furga Date: Wed, 4 Mar 2026 10:42:24 +0100 Subject: [PATCH 4/8] Implement update_element Signed-off-by: Mateusz Furga --- src/libAtomVM/ets.c | 86 ++++++++++++++++++++++++++++++++ src/libAtomVM/ets.h | 1 + src/libAtomVM/nifs.c | 40 +++++++++++++++ src/libAtomVM/nifs.gperf | 6 ++- tests/erlang_tests/test_ets.erl | 87 +++++++++++++++++++++++++++++++++ 5 files changed, 218 insertions(+), 2 deletions(-) diff --git a/src/libAtomVM/ets.c b/src/libAtomVM/ets.c index bf16468fdb..f373846aa6 100644 --- a/src/libAtomVM/ets.c +++ b/src/libAtomVM/ets.c @@ -106,6 +106,7 @@ static EtsStatus lookup_or_default( Heap *ret_heap, term *ret, Context *ctx); +static EtsStatus apply_spec(term tuple, term spec, size_t key_index); static EtsStatus apply_op(term tuple, term opt, avm_int_t *ret, size_t key_index); void ets_init(Ets *ets) @@ -294,6 +295,63 @@ EtsStatus ets_insert(term name_or_ref, term entry, bool as_new, Context *ctx) return result; } +EtsStatus ets_update_element( + term name_or_ref, + term key, + term element_spec, + term default_tuple, + Context *ctx) +{ + struct EtsTable *table = get_table( + &ctx->global->ets, + name_or_ref, + ctx->process_id, + TableAccessWrite); + + if (table == NULL) { + return EtsBadAccess; + } + + Heap insert_heap; + term insert_tuple; + EtsStatus result = lookup_or_default(table, key, default_tuple, &insert_heap, &insert_tuple, ctx); + if (result != EtsOk) { + SMP_UNLOCK(table); + return result; + } + + if (term_is_tuple(element_spec)) { + result = apply_spec(insert_tuple, element_spec, table->key_index); + if (result != EtsOk) { + goto cleanup; + } + } else if (term_is_list(element_spec)) { + for (term iter = element_spec; !term_is_nil(iter); iter = term_get_list_tail(iter)) { + if (!term_is_list(iter)) { + result = EtsBadEntry; + goto cleanup; + } + + term spec = term_get_list_head(iter); + + result = apply_spec(insert_tuple, spec, table->key_index); + if (result != EtsOk) { + goto cleanup; + } + } + } else { + result = EtsBadEntry; + goto cleanup; + } + + result = ets_multimap_insert(table->multimap, &insert_tuple, 1, ctx->global); + +cleanup: + memory_destroy_heap(&insert_heap, ctx->global); + SMP_UNLOCK(table); + return result; +} + EtsStatus ets_update_counter_maybe_gc( term name_or_ref, term key, @@ -827,6 +885,34 @@ static EtsStatus lookup_or_default( return EtsOk; } +static EtsStatus apply_spec(term tuple, term spec, size_t key_index) +{ + if (!term_is_tuple(spec) || term_get_tuple_arity(spec) != 2) { + return EtsBadEntry; + } + + term pos = term_get_tuple_element(spec, 0); + term value = term_get_tuple_element(spec, 1); + + if (!term_is_integer(pos)) { + return EtsBadEntry; + } + + avm_int_t index = term_to_int(pos) - 1; + + if (index < 0 || index >= term_get_tuple_arity(tuple)) { + return EtsBadEntry; + } + + if ((size_t) index == key_index) { + return EtsBadEntry; + } + + term_put_tuple_element(tuple, (uint32_t) index, value); + + return EtsOk; +} + static EtsStatus apply_op(term tuple, term op, avm_int_t *ret, size_t key_index) { assert(term_is_tuple(op)); diff --git a/src/libAtomVM/ets.h b/src/libAtomVM/ets.h index 5503f56561..9afd7a7c47 100644 --- a/src/libAtomVM/ets.h +++ b/src/libAtomVM/ets.h @@ -85,6 +85,7 @@ EtsStatus ets_lookup_maybe_gc(term name_or_ref, term key, term *ret, Context *ct EtsStatus ets_lookup_element_maybe_gc(term name_or_ref, term key, size_t index, term *ret, Context *ctx); EtsStatus ets_member(term name_or_ref, term key, Context *ctx); EtsStatus ets_insert(term name_or_ref, term entry, bool as_new, Context *ctx); +EtsStatus ets_update_element(term name_or_ref, term key, term element_spec, term default_tuple, Context *ctx); EtsStatus ets_update_counter_maybe_gc(term name_or_ref, term key, term op, term default_tuple, term *ret, Context *ctx); EtsStatus ets_take_maybe_gc(term name_or_ref, term key, term *ret, Context *ctx); EtsStatus ets_delete(term name_or_ref, term key, Context *ctx); diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 7a1b124f52..a8fe09db06 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -225,6 +225,7 @@ static term nif_ets_lookup_element(Context *ctx, int argc, term argv[]); static term nif_ets_member(Context *ctx, int argc, term argv[]); static term nif_ets_insert(Context *ctx, int argc, term argv[]); static term nif_ets_insert_new(Context *ctx, int argc, term argv[]); +static term nif_ets_update_element(Context *ctx, int argc, term argv[]); static term nif_ets_update_counter(Context *ctx, int argc, term argv[]); static term nif_ets_take(Context *ctx, int argc, term argv[]); static term nif_ets_delete(Context *ctx, int argc, term argv[]); @@ -752,6 +753,11 @@ static const struct Nif ets_member_nif = { .nif_ptr = nif_ets_member }; +static const struct Nif ets_update_element_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_update_element +}; + static const struct Nif ets_update_counter_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_ets_update_counter @@ -3937,6 +3943,40 @@ static term nif_ets_lookup(Context *ctx, int argc, term argv[]) } } +static term nif_ets_update_element(Context *ctx, int argc, term argv[]) +{ + assert(argc == 3 || argc == 4); + + term name_or_ref = argv[0]; + term key = argv[1]; + term element_spec = argv[2]; + + VALIDATE_VALUE(name_or_ref, is_ets_table_id); + + term default_tuple = term_invalid_term(); + if (argc == 4) { + default_tuple = argv[3]; + VALIDATE_VALUE(default_tuple, term_is_tuple); + } + + EtsStatus result = ets_update_element(name_or_ref, key, element_spec, default_tuple, ctx); + + switch (result) { + case EtsOk: + return TRUE_ATOM; + case EtsTupleNotExists: + return FALSE_ATOM; + case EtsBadAccess: + case EtsBadEntry: + RAISE_ERROR(BADARG_ATOM); + case EtsAllocationError: + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + default: + // unreachable + AVM_ABORT(); + } +} + static term nif_ets_update_counter(Context *ctx, int argc, term argv[]) { assert(argc == 3 || argc == 4); diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index f3865c6edd..babb85329d 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -162,10 +162,12 @@ ets:member/2, &ets_member_nif ets:insert/2, &ets_insert_nif ets:insert_new/2, &ets_insert_new_nif ets:take/2, &ets_take_nif -ets:delete/1, &ets_delete_nif -ets:delete/2, &ets_delete_nif +ets:update_element/3, &ets_update_element_nif +ets:update_element/4, &ets_update_element_nif ets:update_counter/3, &ets_update_counter_nif ets:update_counter/4, &ets_update_counter_nif +ets:delete/1, &ets_delete_nif +ets:delete/2, &ets_delete_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 921cd67fe4..dd9b112acf 100644 --- a/tests/erlang_tests/test_ets.erl +++ b/tests/erlang_tests/test_ets.erl @@ -31,6 +31,7 @@ start() -> ok = isolated(fun test_member/0), ok = isolated(fun test_insert/0), ok = isolated(fun test_insert_new/0), + ok = isolated(fun test_update_element/0), ok = isolated(fun test_update_counter/0), ok = isolated(fun test_take/0), ok = isolated(fun test_delete/0), @@ -369,6 +370,92 @@ test_insert_new() -> ok. +test_update_element() -> + % {Position, Value} + S1 = new_table({key, value1, value2}), + true = ets:update_element(S1, key, {2, new_value1}), + [{key, new_value1, value2}] = ets:lookup(S1, key), + + S2 = new_table({key, value1, value2}), + true = ets:update_element(S2, key, {3, new_value2}), + [{key, value1, new_value2}] = ets:lookup(S2, key), + + S3 = new_table({key, value1, value2}), + false = ets:update_element(S3, key_not_exist, {2, new_value1}), + [{key, value1, value2}] = ets:lookup(S3, key), + + % [{Position, Value}, ...] + S4 = new_table({key, value1, value2}), + true = ets:update_element(S4, key, [{3, new_value2}, {2, new_value1}, {3, new_last_value2}]), + [{key, new_value1, new_last_value2}] = ets:lookup(S4, key), + + % Default object (since OTP 27.0) + if_otp_version(27, fun() -> + S5 = new_table({key, value1, value2}), + true = ets:update_element(S5, key_not_exist, {2, new_value1}, {key, value1, value2}), + [{key_not_exist, new_value1, value2}] = ets:lookup(S5, key_not_exist), + + S6 = new_table({key, value1, value2}), + true = ets:update_element( + S6, + key_not_exist, + [{2, new_value1}, {3, new_value2}, {3, new_last_value2}], + {key, value1, value2} + ), + [{key_not_exist, new_value1, new_last_value2}] = ets:lookup(S6, key_not_exist) + end), + + % Badargs + TErr = new_table(3, [{value1, value2, key}]), + OkDefault = {value, value, value}, + + % The table type is not set + TErrBag = ets:new(test, [bag]), + TErrDuplBag = ets:new(test, [duplicate_bag]), + assert_badarg(fun() -> ets:update_element(TErrBag, key, {1, value}) end), + assert_badarg(fun() -> ets:update_element(TErrDuplBag, key, {1, value}) end), + assert_badarg(fun() -> ets:update_element(bad_table, key, {2, value}) end), + + % Pos < 1 + assert_badarg(fun() -> ets:update_element(TErr, key, {-1, pos_neg}) end), + assert_badarg(fun() -> ets:update_element(TErr, key, {0, pos_zero}) end), + assert_badarg(fun() -> ets:update_element(TErr, key, [{1, pos_ok}, {0, pos_zero}]) end), + + % Pos = KeyPos + assert_badarg(fun() -> ets:update_element(TErr, key, {3, pos_key}) end), + assert_badarg(fun() -> ets:update_element(TErr, key, [{1, pos_ok}, {3, pos_key}]) end), + + % Pos > TupleArity + assert_badarg(fun() -> ets:update_element(TErr, key, {4, pos_past}) end), + assert_badarg(fun() -> ets:update_element(TErr, key, [{1, pos_ok}, {4, pos_past}]) end), + + % 4-arg update_element/4 badargs (since OTP 27.0) + if_otp_version(27, fun() -> + assert_badarg(fun() -> + ets:update_element(TErr, key_not_exist, [{1, pos_ok}, {0, pos_zero}], OkDefault) + end), + assert_badarg(fun() -> + ets:update_element(TErr, key_not_exist, [{1, pos_ok}, {3, pos_key}], OkDefault) + end), + assert_badarg(fun() -> + ets:update_element(TErr, key_not_exist, [{1, pos_ok}, {4, pos_past}], OkDefault) + end) + end), + + % Default object arity < KeyPos + % NOTE: This fails on OTP, see https://github.com/erlang/otp/issues/10603 + if_atomvm(fun() -> + assert_badarg(fun() -> ets:update_element(TErr, key_not_exist, {1, pos_ok}, {value}) end), + assert_badarg(fun() -> + ets:update_element(TErr, key_not_exist, [{1, pos_ok}, {2, pos_ok}], {value, value}) + end) + end), + + [{value1, value2, key}] = ets:lookup(TErr, key), + [] = ets:lookup(TErr, key_not_exist), + + ok. + test_update_counter() -> % Increment S1 = new_table({key, 10, not_number, 30}), From 2e8f69304b65d7908515668bb70e6f7839b5cead Mon Sep 17 00:00:00 2001 From: Mateusz Furga Date: Wed, 4 Mar 2026 10:54:27 +0100 Subject: [PATCH 5/8] Implement delete_object/2 Signed-off-by: Mateusz Furga --- src/libAtomVM/ets.c | 19 ++++++++++++ src/libAtomVM/ets.h | 1 + src/libAtomVM/nifs.c | 32 +++++++++++++++++++ src/libAtomVM/nifs.gperf | 1 + tests/erlang_tests/test_ets.erl | 54 +++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+) diff --git a/src/libAtomVM/ets.c b/src/libAtomVM/ets.c index f373846aa6..d0032e1428 100644 --- a/src/libAtomVM/ets.c +++ b/src/libAtomVM/ets.c @@ -551,6 +551,25 @@ EtsStatus ets_delete_table(term name_or_ref, Context *ctx) return EtsOk; } +EtsStatus ets_delete_object(term name_or_ref, term tuple, Context *ctx) +{ + struct EtsTable *table = get_table( + &ctx->global->ets, + name_or_ref, + ctx->process_id, + TableAccessWrite); + + if (table == NULL) { + return EtsBadAccess; + } + + EtsStatus result = ets_multimap_remove_tuple(table->multimap, tuple, ctx->global); + + SMP_UNLOCK(table); + + return result; +} + void ets_delete_owned_tables(Ets *ets, int32_t process_id, GlobalContext *global) { struct ListHead *ets_tables = synclist_wrlock(&ets->ets_tables); diff --git a/src/libAtomVM/ets.h b/src/libAtomVM/ets.h index 9afd7a7c47..7a3a9d9eea 100644 --- a/src/libAtomVM/ets.h +++ b/src/libAtomVM/ets.h @@ -90,6 +90,7 @@ EtsStatus ets_update_counter_maybe_gc(term name_or_ref, term key, term op, term EtsStatus ets_take_maybe_gc(term name_or_ref, term key, term *ret, Context *ctx); EtsStatus ets_delete(term name_or_ref, term key, Context *ctx); EtsStatus ets_delete_table(term name_or_ref, Context *ctx); +EtsStatus ets_delete_object(term name_or_ref, term tuple, Context *ctx); #ifdef __cplusplus } diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index a8fe09db06..9135caf4fb 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -229,6 +229,7 @@ static term nif_ets_update_element(Context *ctx, int argc, term argv[]); static term nif_ets_update_counter(Context *ctx, int argc, term argv[]); static term nif_ets_take(Context *ctx, int argc, term argv[]); static term nif_ets_delete(Context *ctx, int argc, term argv[]); +static term nif_ets_delete_object(Context *ctx, int argc, term argv[]); static term nif_erlang_pid_to_list(Context *ctx, int argc, term argv[]); 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[]); @@ -773,6 +774,11 @@ static const struct Nif ets_delete_nif = { .nif_ptr = nif_ets_delete }; +static const struct Nif ets_delete_object_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_ets_delete_object +}; + static const struct Nif atomvm_add_avm_pack_binary_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_atomvm_add_avm_pack_binary @@ -4138,6 +4144,32 @@ static term nif_ets_delete(Context *ctx, int argc, term argv[]) } } +static term nif_ets_delete_object(Context *ctx, int argc, term argv[]) +{ + assert(argc == 2); + + term name_or_ref = argv[0]; + term tuple = argv[1]; + + VALIDATE_VALUE(name_or_ref, is_ets_table_id); + VALIDATE_VALUE(tuple, term_is_tuple); + + EtsStatus result = ets_delete_object(name_or_ref, tuple, ctx); + + switch (result) { + case EtsOk: + return TRUE_ATOM; + case EtsBadAccess: + case EtsBadEntry: + RAISE_ERROR(BADARG_ATOM); + case EtsAllocationError: + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + default: + // unreachable + 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 babb85329d..85c1fa70ca 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -168,6 +168,7 @@ ets:update_counter/3, &ets_update_counter_nif ets:update_counter/4, &ets_update_counter_nif ets:delete/1, &ets_delete_nif ets:delete/2, &ets_delete_nif +ets:delete_object/2, &ets_delete_object_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 dd9b112acf..2253d99e74 100644 --- a/tests/erlang_tests/test_ets.erl +++ b/tests/erlang_tests/test_ets.erl @@ -35,6 +35,7 @@ start() -> ok = isolated(fun test_update_counter/0), ok = isolated(fun test_take/0), ok = isolated(fun test_delete/0), + ok = isolated(fun test_delete_object/0), 0. test_ets_new() -> @@ -681,6 +682,59 @@ test_delete() -> ok. +test_delete_object() -> + % Set + S = new_table([{key, value}, {key2, value2}]), + true = ets:delete_object(S, {key_not_exist, value_not_exist}), + + true = ets:delete_object(S, {key, value_not_exist}), + [{key, value}] = ets:lookup(S, key), + + true = ets:delete_object(S, {key, value}), + [] = ets:lookup(S, key), + + true = ets:delete_object(S, {key2, value2}), + [] = ets:lookup(S, key2), + + % Bag + B = new_table(bag, [{key, value}, {key, value2}, {key2, value2}]), + true = ets:delete_object(B, {key_not_exist, value_not_exist}), + + true = ets:delete_object(B, {key, value_not_exist}), + [{key, value}, {key, value2}] = ets:lookup(B, key), + + true = ets:delete_object(B, {key, value}), + [{key, value2}] = ets:lookup(B, key), + + true = ets:delete_object(B, {key, value2}), + [] = ets:lookup(B, key), + + true = ets:delete_object(B, {key2, value2}), + [] = ets:lookup(B, key2), + + % Duplicate bag + DB = new_table(duplicate_bag, [{key, value}, {key, value}, {key, value2}]), + true = ets:delete_object(DB, {key_not_exist, value_not_exist}), + + true = ets:delete_object(DB, {key, value_not_exist}), + [{key, value}, {key, value}, {key, value2}] = ets:lookup(DB, key), + + true = ets:delete_object(DB, {key, value}), + [{key, value2}] = ets:lookup(DB, key), + + true = ets:delete_object(DB, {key, value2}), + [] = ets:lookup(DB, key), + + % Badargs + TErr = new_table(2, []), + assert_badarg(fun() -> ets:delete_object(bad_table, {key, value}) end), + assert_badarg(fun() -> ets:delete_object(TErr, not_a_tuple) end), + assert_badarg(fun() -> ets:delete_object(TErr, [{key, value}, {key, value2}]) end), + assert_badarg(fun() -> ets:delete_object(TErr, {}) end), + assert_badarg(fun() -> ets:delete_object(TErr, {bad_keypos}) end), + + ok. + assert_insert_badargs(T, Insert) -> assert_badarg(fun() -> Insert(T, {}) end), assert_badarg(fun() -> Insert(T, [{}]) end), From 8e7c9e7d8819bed5396a80d2fc607c7839d9bb47 Mon Sep 17 00:00:00 2001 From: Mateusz Furga Date: Wed, 4 Mar 2026 12:34:46 +0100 Subject: [PATCH 6/8] Add nif stubs Signed-off-by: Mateusz Furga --- libs/estdlib/src/ets.erl | 225 ++++++++++++++++++++++++++++++++------- 1 file changed, 184 insertions(+), 41 deletions(-) diff --git a/libs/estdlib/src/ets.erl b/libs/estdlib/src/ets.erl index 5c3144db33..063444d147 100644 --- a/libs/estdlib/src/ets.erl +++ b/libs/estdlib/src/ets.erl @@ -26,33 +26,46 @@ -export([ new/2, - insert/2, lookup/2, lookup_element/3, + lookup_element/4, + member/2, + insert/2, + insert_new/2, + update_element/3, + update_element/4, + update_counter/3, + update_counter/4, + take/2, delete/1, delete/2, - update_counter/3, - update_counter/4 + delete_object/2 ]). -export_type([ table/0, options/0, table_type/0, - access_type/0 + access_type/0, + update_op/0 ]). -opaque table() :: atom | reference(). --type table_type() :: set. +-type table_type() :: set | bag | duplicate_bag. -type access_type() :: private | protected | public. -type option() :: table_type() | {keypos, non_neg_integer()} | access_type(). -type options() :: [option()]. +-type update_op() :: + {pos_integer(), integer()} | {pos_integer(), integer(), integer(), integer()}. %%----------------------------------------------------------------------------- %% @param Name the ets table name %% @param Options the options used to create the table %% @returns A new ets table %% @doc Create a new ets table. +%% +%% Supported table types are `set', `bag', and `duplicate_bag'. +%% The `ordered_set' type is not currently supported. %% @end %%----------------------------------------------------------------------------- -spec new(Name :: atom(), Options :: options()) -> table(). @@ -61,87 +74,190 @@ new(_Name, _Options) -> %%----------------------------------------------------------------------------- %% @param Table a reference to the ets table -%% @param Entry the entry to insert -%% @returns true; otherwise, an error is raised if arguments are bad -%% @doc Insert an entry into an ets table. +%% @param Key the key used to lookup one or more entries +%% @returns a list of matching tuples, or an empty list if none found +%% @doc Look up an entry in an ets table. +%% +%% For `set' tables, returns at most one element. For `bag' and `duplicate_bag' +%% tables, returns all objects with the matching key. %% @end %%----------------------------------------------------------------------------- --spec insert(Table :: table(), Entry :: tuple() | [tuple()]) -> true. -insert(_Table, _Entry) -> +-spec lookup(Table :: table(), Key :: term()) -> [tuple()]. +lookup(_Table, _Key) -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param Table a reference to the ets table %% @param Key the key used to lookup one or more entries -%% @returns the entry in a set, or a list of entries, if the table permits -%% @doc Look up an entry in an ets table. +%% @param Pos index of the element to retrieve (1-based) +%% @returns the element at position Pos from the matching tuple, or a list of +%% such elements if the table is of type `bag' or `duplicate_bag' +%% @doc Look up an element from an entry in an ets table. %% @end %%----------------------------------------------------------------------------- --spec lookup(Table :: table(), Key :: term()) -> [tuple()]. -lookup(_Table, _Key) -> +-spec lookup_element(Table :: table(), Key :: term(), Pos :: pos_integer()) -> term() | [term()]. +lookup_element(_Table, _Key, _Pos) -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param Table a reference to the ets table %% @param Key the key used to lookup one or more entries %% @param Pos index of the element to retrieve (1-based) -%% @returns the Pos:nth element of entry in a set, or a list of entries, if the -%% table permits -%% @doc Look up an element from an entry in an ets table. +%% @param Default value returned if the key does not exist +%% @returns the element at position Pos from the matching tuple, or a list of +%% such elements if the table is of type `bag' or `duplicate_bag', +%% or Default if the key does not exist +%% @doc Look up an element from an entry in an ets table with a default value. +%% +%% Unlike `lookup_element/3', returns Default instead of raising `badarg' when +%% the key does not exist. %% @end %%----------------------------------------------------------------------------- --spec lookup_element(Table :: table(), Key :: term(), Pos :: pos_integer()) -> term(). -lookup_element(_Table, _Key, _Pos) -> +-spec lookup_element(Table :: table(), Key :: term(), Pos :: pos_integer(), Default :: term()) -> + term() | [term()]. +lookup_element(_Table, _Key, _Pos, _Default) -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- %% @param Table a reference to the ets table -%% @param Key the key used to lookup one or more entries to delete +%% @param Key the key to check for existence +%% @returns true if the key exists in the table; false otherwise +%% @doc Check if a key exists in an ets table. +%% @end +%%----------------------------------------------------------------------------- +-spec member(Table :: table(), Key :: term()) -> boolean(). +member(_Table, _Key) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Table a reference to the ets table +%% @param Entry the entry or list of entries to insert %% @returns true; otherwise, an error is raised if arguments are bad -%% @doc Delete an entry from an ets table. +%% @doc Insert an entry into an ets table. +%% +%% For `set' tables, an existing entry with the same key is overwritten. +%% For `bag' tables, the object is added unless an identical object already +%% exists. For `duplicate_bag' tables, the object is always added. +%% The operation is atomic. %% @end %%----------------------------------------------------------------------------- --spec delete(Table :: table(), Key :: term()) -> true. -delete(_Table, _Key) -> + +-spec insert(Table :: table(), Entry :: tuple() | [tuple()]) -> true. +insert(_Table, _Entry) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Table a reference to the ets table +%% @param Entry the entry or list of entries to insert +%% @returns true if all entries were inserted; false if any key already exists +%% @doc Insert an entry into an ets table only if the key does not already exist. +%% +%% Returns `false' without inserting any entries if any of the given objects +%% have a key that already exists in the table. For `bag' and `duplicate_bag' +%% table types, returns `false' if an identical object already exists. +%% @end +%%----------------------------------------------------------------------------- +-spec insert_new(Table :: table(), Entry :: tuple() | [tuple()]) -> boolean(). +insert_new(_Table, _Entry) -> 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. +%% @param Key the key used to look up the entry to update +%% @param ElementSpec a tuple {Pos, Value} or a list of such tuples, specifying +%% the position(s) (1-based) and new value(s) to set +%% @returns true if the entry was updated; false if the key does not exist +%% @doc Update one or more elements of an existing entry in an ets table. +%% +%% The key field itself cannot be updated. Returns `false' if no entry with +%% the given key exists. +%% @end +%%----------------------------------------------------------------------------- +-spec update_element( + Table :: table(), + Key :: term(), + ElementSpec :: {pos_integer(), term()} | [{pos_integer(), term()}] +) -> boolean(). +update_element(_Table, _Key, _ElementSpec) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Table a reference to the ets table +%% @param Key the key used to look up the entry to update +%% @param ElementSpec a tuple {Pos, Value} or a list of such tuples, specifying +%% the position(s) (1-based) and new value(s) to set +%% @param Default a default tuple to insert if the key does not exist +%% @returns true if the entry was updated or inserted; false if insertion failed +%% @doc Update one or more elements of an existing entry, inserting Default if missing. +%% +%% If no entry with the given key exists, inserts Default into the table, +%% then applies the element updates. +%% @end +%%----------------------------------------------------------------------------- +-spec update_element( + Table :: table(), + Key :: term(), + ElementSpec :: {pos_integer(), term()} | [{pos_integer(), term()}], + Default :: tuple() +) -> boolean(). +update_element(_Table, _Key, _ElementSpec, _Default) -> + 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 an integer increment, a single update operation, or a list +%% of update operations. An update operation is a tuple +%% `{Pos, Increment}' or `{Pos, Increment, Threshold, SetValue}', +%% where Pos is a 1-based index. +%% @returns the new counter value, or a list of new values when Params is a list +%% @doc Updates one or more counter values at Key in the table. %% @end %%----------------------------------------------------------------------------- -spec update_counter( Table :: table(), Key :: term(), - Params :: - integer() | {pos_integer(), integer()} | {pos_integer(), integer(), integer(), integer()} -) -> integer(). + Params :: integer() | update_op() | [update_op()] +) -> 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. +%% @param Key the key used to look up the entry expecting to contain a tuple +%% of integers or a single integer +%% @param Params an integer increment, a single update operation, or a list +%% of update operations (see `update_counter/3' for the format) +%% @param Default a default object (tuple) to insert if the key does not +%% exist, after which the update operation is applied to it +%% @returns the new counter value, or a list of new values when Params is a list +%% @doc Updates one or more counter values at Key in the table. +%% +%% Equivalent to `update_counter/3', but inserts Default as a new entry if +%% no object with Key exists, then performs the counter update on it. %% @end %%----------------------------------------------------------------------------- -spec update_counter( Table :: table(), Key :: term(), - Params :: - integer() | {pos_integer(), integer()} | {pos_integer(), integer(), integer(), integer()}, - Default :: integer() -) -> integer(). + Params :: integer() | update_op() | [update_op()], + Default :: tuple() +) -> integer() | [integer()]. update_counter(_Table, _Key, _Params, _Default) -> erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Table a reference to the ets table +%% @param Key the key used to look up and remove entries +%% @returns a list of the removed objects, or an empty list if none found +%% @doc Return and delete all entries with the given key from an ets table. +%% @end +%%----------------------------------------------------------------------------- +-spec take(Table :: table(), Key :: term()) -> [tuple()]. +take(_Table, _Key) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Table a reference to the ets table %% @returns true; @@ -151,3 +267,30 @@ update_counter(_Table, _Key, _Params, _Default) -> -spec delete(Table :: table()) -> true. delete(_Table) -> erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Table a reference to the ets table +%% @param Key the key used to lookup one or more entries to delete +%% @returns true; otherwise, an error is raised if arguments are bad +%% @doc Delete all entries with the given key from an ets table. +%% @end +%%----------------------------------------------------------------------------- +-spec delete(Table :: table(), Key :: term()) -> true. +delete(_Table, _Key) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Table a reference to the ets table +%% @param Object the exact object to delete +%% @returns true; otherwise, an error is raised if arguments are bad +%% @doc Delete a specific object from an ets table. +%% +%% Unlike `delete/2', which deletes all entries matching a key, this function +%% deletes only entries that exactly match the given object. For `bag' tables, +%% other objects sharing the same key are left intact. For `duplicate_bag' +%% tables, all instances of the identical object are removed. +%% @end +%%----------------------------------------------------------------------------- +-spec delete_object(Table :: table(), Object :: tuple()) -> true. +delete_object(_Table, _Object) -> + erlang:nif_error(undefined). From 76270e35ab70bf6bfd48a0407116bd9d02afae3f Mon Sep 17 00:00:00 2001 From: Mateusz Furga Date: Thu, 5 Mar 2026 10:43:33 +0100 Subject: [PATCH 7/8] Update ETS documentation for bag, duplicate_bag, and new functions Signed-off-by: Mateusz Furga --- doc/src/programmers-guide.md | 70 +++++++++++++++++++++++++++++++----- libs/estdlib/src/ets.erl | 4 --- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/doc/src/programmers-guide.md b/doc/src/programmers-guide.md index 32524b1462..bd74c55612 100644 --- a/doc/src/programmers-guide.md +++ b/doc/src/programmers-guide.md @@ -868,9 +868,13 @@ The `packbeam` tool does include file and line information in the AVM files it c AtomVM includes a limited implementation of the Erlang [`ets`](https://www.erlang.org/doc/man/ets) interface, allowing applications to efficiently store term data in a potentially shared key-value store. Conceptually, and ETS table is a collection of key-value pairs (represented as Erlang tuples), which can be efficiently stored, retrieved, and deleted using insertion, lookup, and deletion functions across processes. Storage and retrieval of data in ETS tables is typically faster than communicating with a process which stores state, but still comes at a cost of copying data in and out of the ETS tables. -The current AtomVM implementation of ETS is limited to the `set` table type, meaning that all entries in an ETS table are unique, and that the entries in the ETS table are unordered, when enumerated. +AtomVM supports the `set`, `bag`, and `duplicate_bag` table types: -> The `ordered_set`, `bag`, and `duplicate_bag` OTP ETS types are not currently supported. +- `set` (default): Each key is unique; inserting an object with an existing key overwrites the previous entry. +- `bag`: Multiple objects with the same key are allowed, but identical objects (same key and values) are not duplicated. +- `duplicate_bag`: Multiple objects with the same key are allowed, including identical duplicates. + +> The `ordered_set` OTP ETS type is not currently supported. The lifecycle of an ETS table is associated with the lifecycle of the Erlang process that created it. An Erlang process may create as many ETS tables as memory permits, but ETS tables are automatically destroyed when the process with which they are associated terminates. @@ -888,8 +892,11 @@ The process that creates an ETS table becomes the "owner" of the ETS table. ETS The following configuration options are supported: -| Access Type | Description | -|-------------|-------------| +| Option | Description | +|--------|-------------| +| `set` | Each key is unique; inserting with an existing key overwrites. This is the default table type. | +| `bag` | Multiple objects per key are allowed; identical objects are not duplicated. | +| `duplicate_bag` | Multiple objects per key are allowed, including identical duplicates. | | `named_table` | If set, the table name is registered internally and can be used as the table id for subsequent ETS operations. If this option is set, the return value from `ets:new/2` is the table name specified in the first parameter. By default, ETS tables are not named. | | `{keypos, K}` | The position of the key field in table entries (Erlang tuples). Key position values should be in the range `{1..n}`, where `n` is the minimum arity of any entry in the table. If unspecified, the default key position is `1`. An attempt to insert an entry into a ETS table whose arity is less than the specified key position will result in a `badarg` error. | | `private` | Only the owning process may read from or write to the ETS table. | @@ -912,26 +919,47 @@ If the arity of the supplied tuple entry is less than the configured key positio > Note that the fields of a tuple entry, whether they are designated key fields or arbitrary data, can be any term type, not just atoms, as in this example. -If an entry already exists with the same key field, the entry will be over-written in the table. +For `set` tables, if an entry already exists with the same key field, the entry will be over-written in the table. For `bag` tables, duplicate entries (same key and values) are silently ignored. For `duplicate_bag` tables, all entries including duplicates are stored. The return type from this function is the atom `true`. Any errors in insertion will resulting in raising an `error` with an appropriate reason, e.g., `badarg`. > Note that a process may only insert values into an ETS table if they are permitted; i.e., either they are the owner of the table, or if the table is `public`. +To insert an entry only if the key does not already exist, use the `ets:insert_new/2` function: + +```erlang +true = ets:insert_new(TableId, {foo, bar}) +``` + +This function returns `false` without inserting if any of the given objects already have a matching key in the table. + To retrieve an entry from an ETS table, use the `ets:lookup/2` function: ```erlang [{foo, bar}] = ets:lookup(TableId, foo) ``` -Specify the table identifier returned from `ets:new/2`, as well as a key with which you would like to search the table. This function will search the ETS table using the `keypos`'th field of tuples in the ETS table for retrieval. -The return value is a list containing the found object(s). An empty list (`[]`) indicates that there is no such entry in the specified ETS table. +Specify the table identifier returned from `ets:new/2`, as well as a key with which you would like to search the table. This function will search the ETS table using the `keypos`'th field of tuples in the ETS table for retrieval. -> Note. Since the only table type currently supported is `set`, the return value will only contain a singleton value, if an entry exists in the table under the specified key. +The return value is a list containing the found object(s). An empty list (`[]`) indicates that there is no such entry in the specified ETS table. For `set` tables, the list will contain at most one element. For `bag` and `duplicate_bag` tables, the list may contain multiple elements. > Note that a process may only look up values from an ETS table if they are permitted; i.e., either they are the owner of the table, or if the table is `protected` or `public`. -To delete an entry from an ETS table, use the `ets:delete/2` function: +To check if a key exists in an ETS table without retrieving the full object, use `ets:member/2`: + +```erlang +true = ets:member(TableId, foo) +``` + +To retrieve a specific element from a matching entry, use `ets:lookup_element/3,4`: + +```erlang +bar = ets:lookup_element(TableId, foo, 2) +``` + +The third argument is the 1-based position of the element to return. `ets:lookup_element/3` raises `badarg` if the key does not exist; `ets:lookup_element/4` accepts a default value to return instead. + +To delete all entries with a given key from an ETS table, use the `ets:delete/2` function: ```erlang true = ets:delete(Table, foo) @@ -943,6 +971,30 @@ The return value from this function is the atom `true`, regardless of whether th > Note that a process may only delete values from an ETS table if they are permitted; i.e., either they are the owner of the table, or if the table is `public`. +To delete a specific object (rather than all objects matching a key), use `ets:delete_object/2`: + +```erlang +true = ets:delete_object(Table, {foo, bar}) +``` + +This is primarily useful for `bag` and `duplicate_bag` tables where multiple objects may share the same key. + +To atomically look up and remove all entries with a given key, use `ets:take/2`: + +```erlang +[{foo, bar}] = ets:take(TableId, foo) +``` + +This is equivalent to `ets:lookup/2` followed by `ets:delete/2`, but performed atomically. Returns an empty list if no matching entries exist. + +To update one or more elements of an existing entry in-place, use `ets:update_element/3,4`: + +```erlang +true = ets:update_element(TableId, foo, {2, new_value}) +``` + +The third argument is a `{Pos, Value}` tuple or a list of such tuples, specifying which positions (1-based) to update. Returns `false` if no entry with the given key exists. `ets:update_element/4` accepts a default tuple to insert if the key is not found. + ### Reading data from AVM files AVM files are generally packed BEAM files, but they can also contain non-BEAM files, such as plain text files, binary data, or even encoded Erlang terms. diff --git a/libs/estdlib/src/ets.erl b/libs/estdlib/src/ets.erl index 063444d147..8f8fac9732 100644 --- a/libs/estdlib/src/ets.erl +++ b/libs/estdlib/src/ets.erl @@ -151,10 +151,6 @@ insert(_Table, _Entry) -> %% @param Entry the entry or list of entries to insert %% @returns true if all entries were inserted; false if any key already exists %% @doc Insert an entry into an ets table only if the key does not already exist. -%% -%% Returns `false' without inserting any entries if any of the given objects -%% have a key that already exists in the table. For `bag' and `duplicate_bag' -%% table types, returns `false' if an identical object already exists. %% @end %%----------------------------------------------------------------------------- -spec insert_new(Table :: table(), Entry :: tuple() | [tuple()]) -> boolean(). From e06fb132e6bc4df5673a641e225a8f398a71151b Mon Sep 17 00:00:00 2001 From: Mateusz Furga Date: Thu, 5 Mar 2026 10:56:55 +0100 Subject: [PATCH 8/8] Update CHANGELOG.md Signed-off-by: Mateusz Furga --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb32ec0afa..009000652e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,8 @@ encoding/decoding options, also Elixir `(url_)encode64`/`(url_)decode64` have be - Added `nanosecond` and `native` time unit support to `erlang:system_time/1`, `erlang:monotonic_time/1`, and `calendar:system_time_to_universal_time/2` - Added `erlang:system_time/0`, `erlang:monotonic_time/0`, and `os:system_time/0,1` NIFs - Added `filename:join/1` and `filename:split/1` +- Added support for `bag` and `duplicate_bag` table types in `ets` +- Added `ets:insert_new/2`, `ets:member/2`, `ets:lookup_element/4`, `ets:delete_object/2`, `ets:take/2`, `ets:update_element/3` and `ets:update_element/4` ### Changed