All checks were successful
Gajumaru Bytecode Tests / tests (push) Successful in -3m34s
Add Gitea tests Rename Remove oracle references Package for zx Reviewed-on: #235 Reviewed-by: dimitar.p.ivanov <dimitarivanov@qpq.swiss> Co-authored-by: Craig Everett <zxq9@zxq9.com> Co-committed-by: Craig Everett <zxq9@zxq9.com>
221 lines
9.6 KiB
Erlang
221 lines
9.6 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @copyright (C) 2025, QPQ AG
|
|
%%% @copyright (C) 2019, Aeternity Anstalt
|
|
%%% @doc
|
|
%%% Functions for manipulating FATE maps. In particular for mediating
|
|
%%% between plain map values (represented by Erlang maps) and maps that are
|
|
%%% fully or partially saved in the contract store.
|
|
%%% @end
|
|
%%% -------------------------------------------------------------------
|
|
-module(gmb_fate_maps).
|
|
-vsn("3.4.1").
|
|
|
|
-include("gmb_fate_data.hrl").
|
|
|
|
-export([ allocate_store_maps/2
|
|
, has_store_maps/1
|
|
, unfold_store_maps/2
|
|
, refcount/1
|
|
, refcount_zero/0
|
|
, refcount_diff/2
|
|
, refcount_union/1
|
|
, refcount_union/2
|
|
, no_used_ids/0 ]).
|
|
|
|
-export_type([used_ids/0, maps/0, refcount/0]).
|
|
|
|
%% Size in bytes of serialization of a map for which we turn it into a store
|
|
%% map. It's not worth turning small maps into store maps.
|
|
%% Under consensus!
|
|
-ifdef(TEST).
|
|
-define(STORE_MAP_THRESHOLD, 0).
|
|
-else.
|
|
-define(STORE_MAP_THRESHOLD, 100).
|
|
-endif.
|
|
|
|
-type fate_value() :: gmb_fate_data:fate_type().
|
|
-type fate_value_or_tombstone() :: fate_value() | ?FATE_MAP_TOMBSTONE.
|
|
-type id() :: integer().
|
|
-type used_ids() :: list(id()).
|
|
-type maps() :: #{ id() => gmb_fate_data:fate_map() | gmb_fate_data:fate_store_map() }.
|
|
|
|
%% -- Allocating store maps --------------------------------------------------
|
|
|
|
-spec allocate_store_maps(used_ids(), [fate_value_or_tombstone()]) -> {[fate_value_or_tombstone()], maps()}.
|
|
allocate_store_maps(Used, Vals) ->
|
|
{_Used, Vals1, Maps} = allocate_store_maps_l(Used, Vals, #{}),
|
|
{Vals1, Maps}.
|
|
|
|
allocate_store_maps(Used, ?FATE_MAP_TOMBSTONE = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_TRUE = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_FALSE = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_UNIT = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_BITS(_) = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_BYTES(_) = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_ADDRESS(_) = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_CONTRACT(_) = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_ORACLE(_) = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_ORACLE_Q(_) = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_CHANNEL(_) = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_TYPEREP(_) = Val, Maps) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, Val, Maps) when ?IS_FATE_INTEGER(Val) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, Val, Maps) when ?IS_FATE_STRING(Val) -> {Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_TUPLE(Val), Maps) ->
|
|
{Used1, Vals, Maps1} = allocate_store_maps_l(Used, tuple_to_list(Val), Maps),
|
|
{Used1, ?FATE_TUPLE(list_to_tuple(Vals)), Maps1};
|
|
allocate_store_maps(Used, Val, Maps) when ?IS_FATE_LIST(Val) ->
|
|
{Used1, Vals, Maps1} = allocate_store_maps_l(Used, ?FATE_LIST_VALUE(Val), Maps),
|
|
{Used1, ?MAKE_FATE_LIST(Vals), Maps1};
|
|
allocate_store_maps(Used, ?FATE_VARIANT(Arities, Tag, Vals), Maps) ->
|
|
{Used1, Vals1, Maps1} = allocate_store_maps_l(Used, tuple_to_list(Vals), Maps),
|
|
{Used1, ?FATE_VARIANT(Arities, Tag, list_to_tuple(Vals1)), Maps1};
|
|
allocate_store_maps(Used, Val, Maps) when ?IS_FATE_MAP(Val) ->
|
|
{Used1, KVs, Maps1} = allocate_store_maps_m(Used, ?FATE_MAP_VALUE(Val), Maps),
|
|
Val1 = ?MAKE_FATE_MAP(KVs),
|
|
case byte_size(gmb_fate_encoding:serialize(Val1)) < ?STORE_MAP_THRESHOLD of
|
|
true -> {Used1, Val1, Maps1};
|
|
false ->
|
|
{Id, Used2} = next_id(Used1),
|
|
{Used2, ?FATE_STORE_MAP(#{}, Id), Maps1#{Id => Val1}}
|
|
end;
|
|
allocate_store_maps(Used, ?FATE_STORE_MAP(Cache, _Id) = Val, Maps) when Cache =:= #{} ->
|
|
{Used, Val, Maps};
|
|
allocate_store_maps(Used, ?FATE_STORE_MAP(Cache, Id), Maps) ->
|
|
{NewId, Used1} = next_id(Used),
|
|
{Used2, Cache1, Maps1} = allocate_store_maps_m(Used1, Cache, Maps),
|
|
{Used2, ?FATE_STORE_MAP(#{}, NewId), Maps1#{NewId => ?FATE_STORE_MAP(Cache1, Id)}}.
|
|
|
|
allocate_store_maps_l(Used, [], Maps) -> {Used, [], Maps};
|
|
allocate_store_maps_l(Used, [H | T], Maps) ->
|
|
{Used1, H1, Maps1} = allocate_store_maps(Used, H, Maps),
|
|
{Used2, T1, Maps2} = allocate_store_maps(Used1, T, Maps1),
|
|
{Used2, [H1 | T1], Maps2}.
|
|
|
|
allocate_store_maps_m(Used, Val, Maps) ->
|
|
maps:fold(fun(K, V, {Us, M, Ms}) ->
|
|
{Us1, V1, Ms1} = allocate_store_maps(Us, V, Ms),
|
|
{Us1, M#{ K => V1 }, Ms1}
|
|
end, {Used, #{}, Maps}, Val).
|
|
|
|
%% -- Unfolding store maps ---------------------------------------------------
|
|
|
|
-type unfold_fun() :: fun((id()) -> gmb_fate_data:fate_map()).
|
|
|
|
-spec unfold_store_maps(unfold_fun(), fate_value_or_tombstone()) -> fate_value_or_tombstone().
|
|
unfold_store_maps(_Unfold, ?FATE_MAP_TOMBSTONE = Val) -> Val;
|
|
unfold_store_maps(_Unfold, ?FATE_TRUE = Val) -> Val;
|
|
unfold_store_maps(_Unfold, ?FATE_FALSE = Val) -> Val;
|
|
unfold_store_maps(_Unfold, ?FATE_UNIT = Val) -> Val;
|
|
unfold_store_maps(_Unfold, ?FATE_BITS(_) = Val) -> Val;
|
|
unfold_store_maps(_Unfold, ?FATE_BYTES(_) = Val) -> Val;
|
|
unfold_store_maps(_Unfold, ?FATE_ADDRESS(_) = Val) -> Val;
|
|
unfold_store_maps(_Unfold, ?FATE_CONTRACT(_) = Val) -> Val;
|
|
unfold_store_maps(_Unfold, ?FATE_ORACLE(_) = Val) -> Val;
|
|
unfold_store_maps(_Unfold, ?FATE_ORACLE_Q(_) = Val) -> Val;
|
|
unfold_store_maps(_Unfold, ?FATE_CHANNEL(_) = Val) -> Val;
|
|
unfold_store_maps(_Unfold, ?FATE_TYPEREP(_) = Val) -> Val;
|
|
unfold_store_maps(_Unfold, Val) when ?IS_FATE_INTEGER(Val) -> Val;
|
|
unfold_store_maps(_Unfold, Val) when ?IS_FATE_STRING(Val) -> Val;
|
|
unfold_store_maps(Unfold, ?FATE_TUPLE(Val)) ->
|
|
Vals = unfold_store_maps_l(Unfold, tuple_to_list(Val)),
|
|
?FATE_TUPLE(list_to_tuple(Vals));
|
|
unfold_store_maps(Unfold, Val) when ?IS_FATE_LIST(Val) ->
|
|
?MAKE_FATE_LIST(unfold_store_maps_l(Unfold, ?FATE_LIST_VALUE(Val)));
|
|
unfold_store_maps(Unfold, ?FATE_VARIANT(Arities, Tag, Vals)) ->
|
|
Vals1 = unfold_store_maps_l(Unfold, tuple_to_list(Vals)),
|
|
?FATE_VARIANT(Arities, Tag, list_to_tuple(Vals1));
|
|
unfold_store_maps(Unfold, Val) when ?IS_FATE_MAP(Val) ->
|
|
?MAKE_FATE_MAP(unfold_store_maps_m(Unfold, ?FATE_MAP_VALUE(Val)));
|
|
unfold_store_maps(Unfold, ?FATE_STORE_MAP(Cache, Id)) ->
|
|
StoreMap = Unfold(Id),
|
|
maps:fold(fun write_cache/3, unfold_store_maps(Unfold, StoreMap),
|
|
unfold_store_maps_m(Unfold, Cache)).
|
|
|
|
unfold_store_maps_l(Unfold, Vals) ->
|
|
[ unfold_store_maps(Unfold, Val) || Val <- Vals ].
|
|
|
|
unfold_store_maps_m(Unfold, Val) ->
|
|
maps:map(fun(_, V) -> unfold_store_maps(Unfold, V) end, Val).
|
|
|
|
write_cache(Key, ?FATE_MAP_TOMBSTONE, Map) ->
|
|
maps:remove(Key, Map);
|
|
write_cache(Key, Val, Map) ->
|
|
Map#{ Key => Val }.
|
|
|
|
%% -- Reference counting -----------------------------------------------------
|
|
|
|
-type refcount() :: #{id() => integer()}.
|
|
|
|
-spec refcount_zero() -> refcount().
|
|
refcount_zero() -> #{}.
|
|
|
|
-spec refcount_diff(refcount(), refcount()) -> refcount().
|
|
refcount_diff(New, Old) ->
|
|
maps:fold(fun(K, N, C) -> maps:update_with(K, fun(M) -> M - N end, -N, C) end,
|
|
New, Old).
|
|
|
|
-spec refcount_union([refcount()]) -> refcount().
|
|
refcount_union(Counts) -> lists:foldl(fun refcount_union/2, #{}, Counts).
|
|
|
|
-spec refcount_union(refcount(), refcount()) -> refcount().
|
|
refcount_union(A, B) ->
|
|
maps:fold(fun(K, N, C) -> maps:update_with(K, fun(M) -> M + N end, N, C) end,
|
|
B, A).
|
|
|
|
-spec has_store_maps(fate_value()) -> boolean().
|
|
has_store_maps(Val) ->
|
|
refcount_zero() /= refcount(Val).
|
|
|
|
-spec refcount(fate_value()) -> refcount().
|
|
refcount(Val) -> refcount(Val, #{}).
|
|
|
|
-spec refcount(fate_value_or_tombstone(), refcount()) -> refcount().
|
|
refcount(?FATE_MAP_TOMBSTONE, Count) -> Count;
|
|
refcount(?FATE_TRUE, Count) -> Count;
|
|
refcount(?FATE_FALSE, Count) -> Count;
|
|
refcount(?FATE_UNIT, Count) -> Count;
|
|
refcount(?FATE_BITS(_), Count) -> Count;
|
|
refcount(?FATE_BYTES(_), Count) -> Count;
|
|
refcount(?FATE_ADDRESS(_), Count) -> Count;
|
|
refcount(?FATE_CONTRACT(_), Count) -> Count;
|
|
refcount(?FATE_ORACLE(_), Count) -> Count;
|
|
refcount(?FATE_ORACLE_Q(_), Count) -> Count;
|
|
refcount(?FATE_CHANNEL(_), Count) -> Count;
|
|
refcount(?FATE_TYPEREP(_), Count) -> Count;
|
|
refcount(Val, Count) when ?IS_FATE_INTEGER(Val) -> Count;
|
|
refcount(Val, Count) when ?IS_FATE_STRING(Val) -> Count;
|
|
refcount(?FATE_TUPLE(Val), Count) ->
|
|
refcount_l(tuple_to_list(Val), Count);
|
|
refcount(Val, Count) when ?IS_FATE_LIST(Val) ->
|
|
refcount_l(?FATE_LIST_VALUE(Val), Count);
|
|
refcount(?FATE_VARIANT(_Arities, _Tag, Vals), Count) ->
|
|
refcount_l(tuple_to_list(Vals), Count);
|
|
refcount(Val, Count) when ?IS_FATE_MAP(Val) ->
|
|
refcount_m(?FATE_MAP_VALUE(Val), Count);
|
|
refcount(?FATE_STORE_MAP(Cache, Id), Count) ->
|
|
refcount_m(Cache, maps:update_with(Id, fun(N) -> N + 1 end, 1, Count)).
|
|
|
|
refcount_l(Vals, Count) ->
|
|
lists:foldl(fun refcount/2, Count, Vals).
|
|
|
|
refcount_m(Val, Count) ->
|
|
%% No maps in map keys
|
|
maps:fold(fun(_, ?FATE_MAP_TOMBSTONE, C) -> C;
|
|
(_, V, C) -> refcount(V, C) end, Count, Val).
|
|
|
|
%% -- Map id allocation ------------------------------------------------------
|
|
|
|
-spec no_used_ids() -> used_ids().
|
|
no_used_ids() -> [].
|
|
|
|
-spec next_id(used_ids()) -> {id(), used_ids()}.
|
|
next_id(UsedIds) ->
|
|
next_id(UsedIds, 0, []).
|
|
|
|
next_id(Used, J, Acc) when Used == []; J < hd(Used) ->
|
|
{J, lists:reverse(Acc) ++ [J | Used]};
|
|
next_id([I | Used], I, Acc) ->
|
|
next_id(Used, I + 1, [I | Acc]);
|
|
next_id([I | Used], J, Acc) when J > I ->
|
|
next_id(Used, J, [I | Acc]).
|