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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added xtensa JIT backend for esp32 platform
- Added support for configuring pins and width for sdmmc on ESP32
- Added support for map comprehensions
- Added `persistent_term` module with `get/0,1,2`, `put/2`, `put_new/2`, `erase/1`, and `info/0`

### Changed
- Updated network type db() to dbm() to reflect the actual representation of the type
Expand Down
1 change: 1 addition & 0 deletions libs/estdlib/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ set(ERLANG_MODULES
math
net
os
persistent_term
proc_lib
sys
logger
Expand Down
65 changes: 65 additions & 0 deletions libs/estdlib/src/persistent_term.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
%
% This file is part of AtomVM.
%
% Copyright 2026 Peter M. <petermm@gmail.com>
%
% 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
%

%%-----------------------------------------------------------------------------
%% @doc A limited implementation of the Erlang/OTP `persistent_term' module.
%%
%% Values are stored globally and reads return stored values without copying.
%% Replaced or erased values remain allocated until VM shutdown so references
%% already returned to processes stay valid without a global GC pass. The
%% `memory' value returned by `info/0' includes those retained old values.
%% @end
%%-----------------------------------------------------------------------------
-module(persistent_term).

-export([erase/1, get/0, get/1, get/2, info/0, put/2, put_new/2]).

-export_type([key/0, value/0]).

-type key() :: term().
-type value() :: term().

-spec erase(Key :: key()) -> boolean().
erase(_Key) ->
erlang:nif_error(undefined).

-spec get() -> [{key(), value()}].
get() ->
erlang:nif_error(undefined).

-spec get(Key :: key()) -> value().
get(_Key) ->
erlang:nif_error(undefined).

-spec get(Key :: key(), Default :: value()) -> value().
get(_Key, _Default) ->
erlang:nif_error(undefined).

-spec info() -> #{count := non_neg_integer(), memory := non_neg_integer()}.
info() ->
erlang:nif_error(undefined).

-spec put(Key :: key(), Value :: value()) -> ok.
put(_Key, _Value) ->
erlang:nif_error(undefined).

-spec put_new(Key :: key(), Value :: value()) -> ok.
put_new(_Key, _Value) ->
erlang:nif_error(undefined).
4 changes: 4 additions & 0 deletions src/libAtomVM/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ set(HEADER_FILES
opcodesswitch.h
overflow_helpers.h
nifs.h
persistent_term.h
platform_nifs.h
port.h
posix_nifs.h
Expand All @@ -66,6 +67,7 @@ set(HEADER_FILES
sys.h
term_typedef.h
term.h
term_hash.h
timer_list.h
trace.h
unicode.h
Expand Down Expand Up @@ -99,13 +101,15 @@ set(SOURCE_FILES
memory.c
module.c
nifs.c
persistent_term.c
port.c
posix_nifs.c
refc_binary.c
resources.c
scheduler.c
stacktrace.c
term.c
term_hash.c
timer_list.c
unicode.c
unlocalized.c
Expand Down
219 changes: 5 additions & 214 deletions src/libAtomVM/ets_multimap.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@

#include "globalcontext.h"
#include "term.h"
#include "term_hash.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);
Expand Down Expand Up @@ -176,7 +175,7 @@ ets_result_t ets_multimap_insert(

assert(new_node->entries != NULL);

uint32_t idx = hash_term(key, global) % ETS_MULTIMAP_NUM_BUCKETS;
uint32_t idx = term_hash(key, global) % ETS_MULTIMAP_NUM_BUCKETS;
new_node->next = multimap->buckets[idx];
multimap->buckets[idx] = new_node;
continue;
Expand Down Expand Up @@ -231,7 +230,7 @@ ets_result_t ets_multimap_remove(
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;
uint32_t idx = term_hash(key, global) % ETS_MULTIMAP_NUM_BUCKETS;
EtsMultimapNode *iter = multimap->buckets[idx];
EtsMultimapNode *prev = NULL;

Expand Down Expand Up @@ -338,7 +337,7 @@ ets_result_t ets_multimap_remove_tuple(
}

if (node->entries == NULL) {
uint32_t idx = hash_term(key, global) % ETS_MULTIMAP_NUM_BUCKETS;
uint32_t idx = term_hash(key, global) % ETS_MULTIMAP_NUM_BUCKETS;

EtsMultimapNode *prev_node = NULL;
for (EtsMultimapNode *iter = multimap->buckets[idx]; iter != NULL; prev_node = iter, iter = iter->next) {
Expand Down Expand Up @@ -370,7 +369,7 @@ static ets_result_t node_find(

*out_node = NULL;

uint32_t idx = hash_term(key, global) % ETS_MULTIMAP_NUM_BUCKETS;
uint32_t idx = term_hash(key, global) % ETS_MULTIMAP_NUM_BUCKETS;
EtsMultimapNode *node = multimap->buckets[idx];

while (node) {
Expand Down Expand Up @@ -542,211 +541,3 @@ static void entry_delete(EtsMultimapEntry *entry, GlobalContext *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);
}
2 changes: 2 additions & 0 deletions src/libAtomVM/globalcontext.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ GlobalContext *globalcontext_new(void)
synclist_init(&glb->select_events);

ets_init(&glb->ets);
persistent_term_init(&glb->persistent_term);

glb->last_process_id = 0;

Expand Down Expand Up @@ -284,6 +285,7 @@ COLD_FUNC void globalcontext_destroy(GlobalContext *glb)
synclist_destroy(&glb->select_events);

ets_destroy(&glb->ets, glb);
persistent_term_destroy(&glb->persistent_term, glb);

// Destroy refc binaries including resources
// (this list should be empty if resources were properly refcounted)
Expand Down
Loading
Loading