%% @doc Clojure's persistent vector implementation in Erlang. %% %% This is an implementation of the persistent vector data structure %% found in Clojure JVM. A very nice series of posts explaining in %% detail the Clojure vector implementation can be found %% here. %% %% The goal of implementing this data structure in Erlang was to %% compare its performance to the one provided by the `array' module. %% The results of the comparison can be found here. -module(clj_vector). -include("clojerl.hrl"). -include("clojerl_int.hrl"). -export([ new/0 , new/1 , empty/0 ]). -export([ cons/2 , reduce/2 , reduce/3 , get/2 , set/3 , size/1 , to_list/1 , tuple_for/2 , pop/1 ]). -define(NODE_SIZE, 32). -define(MASK, (?NODE_SIZE - 1)). -define(SHIFT, 5). -define(EMPTY_NODE, { ?NIL, ?NIL, ?NIL, ?NIL , ?NIL, ?NIL, ?NIL, ?NIL , ?NIL, ?NIL, ?NIL, ?NIL , ?NIL, ?NIL, ?NIL, ?NIL , ?NIL, ?NIL, ?NIL, ?NIL , ?NIL, ?NIL, ?NIL, ?NIL , ?NIL, ?NIL, ?NIL, ?NIL , ?NIL, ?NIL, ?NIL, ?NIL } ). -define( EMPTY , #clj_vector{ size = 0 , shift = ?SHIFT , root = ?EMPTY_NODE , tail = ?EMPTY_NODE , tailoff = 0 } ). -record(clj_vector, { size = 0 :: size() , shift = ?SHIFT :: shift() , root :: tree_node() , tail :: tree_node() , tailoff = 0 :: offset() }). -type size() :: non_neg_integer(). -type index() :: non_neg_integer(). -type shift() :: non_neg_integer(). -type tree_node() :: tuple(). -type offset() :: non_neg_integer(). -type vector() :: #clj_vector{}. %% @doc Creates a new empty vector. new() -> new([]). %% @doc Creates a new vector containing the items. -spec new(list()) -> vector(). new([]) -> ?EMPTY; new(Items) when is_list(Items) -> Length = length(Items), case Length =< ?NODE_SIZE of true -> IndexedValues = index_values_asc(Items, 1, []), Node = erlang:make_tuple(?NODE_SIZE, ?NIL, IndexedValues), cons_node(Node, Length, ?EMPTY); _ -> from_list([], ?NODE_SIZE, ?EMPTY, Items) end. %% @doc Returns an empty vector. -spec empty() -> vector(). empty() -> ?EMPTY. %% @doc Returns the size of the vector. -spec size(vector()) -> size(). size(#clj_vector{size = Size}) -> Size. %% @doc Reduces over the items in the vector. %% %% The behaviour of this function is analogous to %% `clojure.core/reduce'. -spec reduce(function(), vector()) -> any(). reduce(F, #clj_vector{size = 0}) -> clj_rt:apply(F, []); reduce(_F, #clj_vector{size = 1} = Vector) -> get(0, Vector); reduce( F , #clj_vector{ size = Size , shift = Shift , root = Root , tail = Tail , tailoff = TailOffset } ) -> Init = element(1, tuple_for(0, Shift, Root, Tail, TailOffset)), reduce_loop(F, Init, 1, Size, Shift, Root, Tail, TailOffset). %% @doc Reduces over the items in the vector using an initial value. %% %% The behaviour of this function is analogous to %% `clojure.core/reduce'. -spec reduce(function(), any(), vector()) -> any(). reduce(_F, Init, #clj_vector{size = 0}) -> Init; reduce( F , Init , #clj_vector{ size = Size , shift = Shift , root = Root , tail = Tail , tailoff = TailOffset } ) -> reduce_loop(F, Init, 0, Size, Shift, Root, Tail, TailOffset). %% @doc Returns an Erlang list containing all items in the vector. -spec to_list(vector()) -> [any()]. to_list(#clj_vector{size = 0}) -> []; to_list(#clj_vector{ size = Size , shift = Shift , root = Root , tail = Tail , tailoff = TailOffset } ) -> case Size =< ?NODE_SIZE of true -> to_list_loop_tuple([], Tail, Size); false -> to_list_loop([], Size - 1, Shift, Root, Tail, TailOffset) end. %% @private -spec tuple_for(index(), vector()) -> tree_node(). tuple_for( Index , #clj_vector{ shift = Shift , root = Root , tail = Tail , tailoff = TailOffset }) -> tuple_for(Index, Shift, Root, Tail, TailOffset). %% @doc Adds an item at the end of the vector. -spec cons(any(), vector()) -> vector(). cons( Value , #clj_vector{ size = Size , shift = Shift , root = Root , tail = Tail , tailoff = TailOffset } = Vector ) -> case Size - TailOffset of %% Space available in the tail Idx when Idx < ?NODE_SIZE -> Vector#clj_vector{ size = Size + 1 , tail = setelement(Idx + 1, Tail, Value) }; %% Root overflow _ when (Size bsr ?SHIFT) > (1 bsl Shift) -> NewPath = new_path(Shift, Tail), InitValues = [{1, Root}, {2, NewPath}], NewRoot = erlang:make_tuple(?NODE_SIZE, ?NIL, InitValues), NewTail = setelement(1, Tail, Value), #clj_vector{ size = Size + 1 , shift = Shift + ?SHIFT , root = NewRoot , tail = NewTail , tailoff = tailoff(Size + 1) }; %% Otherwise just push the tail _ -> NewRoot = push_tail(Size, Shift, Root, Tail), NewTail = setelement(1, Tail, Value), #clj_vector{ size = Size + 1 , shift = Shift , root = NewRoot , tail = NewTail , tailoff = tailoff(Size + 1) } end. %% @doc Returns the item at the `Index' position in the vector. -spec get(index(), vector()) -> any(). get( Index , #clj_vector{ size = Size , shift = Shift , root = Root , tail = Tail , tailoff = TailOffset } ) when is_integer(Index), Index >= 0, Index < Size -> case Index >= TailOffset of true -> Idx = Index band ?MASK, element(Idx + 1, Tail); false -> get_from_node(Index, Shift, Root) end; get(_Index, _Vector) -> ?ERROR(<<"Index out of bounds">>). %% @doc Sets the item at the `Index' position in the vector. -spec set(index(), any(), vector()) -> vector(). set( Index , Value , #clj_vector{ size = Size , shift = Shift , root = Root , tail = Tail , tailoff = TailOffset } = Vector ) when is_integer(Index), Index >= 0, Index < Size -> case Index >= TailOffset of true -> Idx = Index band ?MASK + 1, Vector#clj_vector{tail = setelement(Idx, Tail, Value)}; false -> Vector#clj_vector{root = set_new_root(Shift, Root, Index, Value)} end; set( Index , Value , #clj_vector{size = Size} = Vector ) when Index =:= Size -> cons(Value, Vector); set(_Index, _Value, _Vector) -> ?ERROR(<<"Index out of bounds">>). %% @doc Remove the item at the end of the vector. -spec pop(vector()) -> vector(). pop(#clj_vector{size = 0}) -> ?ERROR(<<"Can't pop empty vector">>); pop(#clj_vector{size = 1}) -> ?EMPTY; %% Pop from tail pop( #clj_vector{ size = Size , tailoff = TailOffset } = Vector ) when Size - TailOffset > 1 -> Vector#clj_vector{size = Size - 1}; %% Replace empty tail pop( #clj_vector{ size = Size0 , shift = Shift0 , root = Root0 } = Vector ) -> Tail1 = tuple_for_loop(Size0 - 2, Shift0, Root0), Root1 = case pop_tail(Size0, Shift0, Root0) of ?NIL -> ?EMPTY_NODE; X -> X end, {Root2, Shift1} = case Shift0 > 5 andalso element(2, Root1) =:= ?NIL of true -> {element(1, Root1), Shift0 - 5}; false -> {Root1, Shift0} end, Size1 = Size0 - 1, Vector#clj_vector{ size = Size1 , shift = Shift1 , root = Root2 , tail = Tail1 , tailoff = tailoff(Size1) }. %%------------------------------------------------------------------------------ %% Helper functions %%------------------------------------------------------------------------------ %% @private -spec from_list(list(), index(), vector(), list()) -> vector(). %% Node list is full with ?NODE_SIZE from_list(_NodeList, ?NODE_SIZE, Vector, []) -> Vector; from_list(NodeList, 0, Vector0, Items) -> Node = list_to_tuple(lists:reverse(NodeList)), Vector1 = cons_node(Node, ?NODE_SIZE, Vector0), from_list([], ?NODE_SIZE, Vector1, Items); %% Node list is not full but there are no more items from_list(NodeList, NodeSizeLeft, Vector, []) -> NodeSize = ?NODE_SIZE - NodeSizeLeft, IndexedValues = index_values(NodeList, NodeSize, []), Node = erlang:make_tuple(?NODE_SIZE, ?NIL, IndexedValues), cons_node(Node, NodeSize, Vector); from_list(NodeList, NodeSizeLeft, Vector, [Item | Items]) -> from_list([Item | NodeList], NodeSizeLeft - 1, Vector, Items). -spec index_values(list(), size(), list()) -> list(). index_values([], _, Acc) -> Acc; index_values([X | Rest], N, Acc) -> index_values(Rest, N - 1, [{N, X} | Acc]). -spec index_values_asc(list(), size(), list()) -> list(). index_values_asc([], _, Acc) -> Acc; index_values_asc([X | Rest], N, Acc) -> index_values_asc(Rest, N + 1, [{N, X} | Acc]). -spec new_path(shift(), tree_node()) -> tree_node(). new_path(_Level = 0, Node) -> Node; new_path(Level, Node) -> NewNode = erlang:make_tuple(?NODE_SIZE, ?NIL), setelement(1, NewNode, new_path(Level - ?SHIFT, Node)). -spec push_tail(size(), shift(), tree_node(), tree_node()) -> tree_node(). push_tail(Size, ?SHIFT, Parent, Tail) -> Idx = ((Size - 1) bsr ?SHIFT) band ?MASK + 1, setelement(Idx, Parent, Tail); push_tail(Size, Level, Parent, Tail) -> Idx = ((Size - 1) bsr Level) band ?MASK + 1, NewNode = case element(Idx, Parent) of ?NIL -> new_path(Level - ?SHIFT, Tail); Child -> push_tail(Size, Level - ?SHIFT, Child, Tail) end, setelement(Idx, Parent, NewNode). -spec tailoff(size()) -> offset(). tailoff(Size) when Size < ?NODE_SIZE -> 0; tailoff(Size) -> ((Size - 1) bsr ?SHIFT) bsl ?SHIFT. -spec get_from_node(index(), shift(), tree_node()) -> any(). get_from_node(Index, 0, Node) -> Idx = Index band (?NODE_SIZE - 1), element(Idx + 1, Node); get_from_node(Index, Level, Node) -> Idx = (Index bsr Level) band ?MASK + 1, get_from_node(Index, Level - ?SHIFT, element(Idx, Node)). -spec set_new_root(shift(), tree_node(), index(), any()) -> tree_node(). set_new_root(_Level = 0, Node, Index, Value) -> setelement(Index band ?MASK + 1, Node, Value); set_new_root(Level, Node, Index, Value) -> Idx = (Index bsr Level) band ?MASK + 1, Child = set_new_root(Level - ?SHIFT, element(Idx, Node), Index, Value), setelement(Idx, Node, Child). -spec tuple_for(index(), shift(), tree_node(), tree_node(), offset()) -> tree_node(). tuple_for(Index, _Shift, _Root, Tail, TailOffset) when Index >= TailOffset -> Tail; tuple_for(Index, Shift, Root, _Tail, _TailOffset) -> tuple_for_loop(Index, Shift, Root). -spec tuple_for_loop(index(), shift(), tree_node()) -> tree_node(). tuple_for_loop(_Index, 0, Node)-> Node; tuple_for_loop(Index, Level, Node) -> Idx = (Index bsr Level) band ?MASK + 1, tuple_for_loop(Index, Level - ?SHIFT, element(Idx, Node)). -spec pop_tail(size(), shift(), tree_node()) -> ?NIL | tree_node(). pop_tail(Size, Level, Node) -> case ((Size - 2) bsr Level) band ?MASK of 0 -> ?NIL; Idx when Level > 5 -> NewChild = pop_tail(Size, Level - ?SHIFT, element(Idx + 1, Node)), setelement(Idx + 1, Node, NewChild); Idx -> setelement(Idx + 1, Node, ?NIL) end. -spec reduce_loop( any(), any(), index(), size(), shift() , tree_node(), tree_node(), offset() ) -> any(). reduce_loop(F, Acc, CurrentIndex, Size, _Shift, _Root, Tail, TailOffset) when CurrentIndex >= TailOffset -> StartPos = CurrentIndex band ?MASK + 1, EndPos = Size - TailOffset, case reduce_loop_tuple(F, Acc, Tail, StartPos, EndPos) of {plain, Val} -> Val; {reduced, Reduced} -> 'clojerl.Reduced':deref(Reduced) end; reduce_loop(F, Acc0, CurrentIndex, Size, Shift, Root, Tail, TailOffset) -> Tuple = tuple_for_loop(CurrentIndex, Shift, Root), StartIndex = CurrentIndex band ?MASK, NextIndex = CurrentIndex + ?NODE_SIZE - StartIndex, StartPos = StartIndex + 1, case reduce_loop_tuple(F, Acc0, Tuple, StartPos, ?NODE_SIZE) of {plain, Acc1} -> reduce_loop(F, Acc1, NextIndex, Size, Shift, Root, Tail, TailOffset); {reduced, Reduced} -> 'clojerl.Reduced':deref(Reduced) end. -spec reduce_loop_tuple(any(), any(), tree_node(), index(), index()) -> any(). reduce_loop_tuple(_F, Acc, _Tuple, Current, End) when Current > End -> {plain, Acc}; reduce_loop_tuple(F, Acc0, Tuple, Current, End) -> Acc1 = clj_rt:apply(F, [Acc0, element(Current, Tuple)]), case 'clojerl.Reduced':is_reduced(Acc1) of true -> {reduced, Acc1}; false -> reduce_loop_tuple(F, Acc1, Tuple, Current + 1, End) end. -spec to_list_loop( list(), index(), shift(), tree_node() , tree_node(), offset() ) -> list(). to_list_loop(List, Index, _Shift, _Root, _Tail, _TailOffset) when Index < 0 -> List; to_list_loop(List, Index, Shift, Root, Tail, TailOffset) when Index >= TailOffset -> Position = Index band ?MASK + 1, List1 = to_list_loop_tuple(List, Tail, Position), to_list_loop(List1, Index - Position, Shift, Root, Tail, TailOffset); to_list_loop(List, Index, Shift, Root, Tail, TailOffset) -> Node = tuple_for_loop(Index, Shift, Root), List1 = to_list_loop_tuple(List, Node, ?NODE_SIZE), to_list_loop(List1, Index - ?NODE_SIZE, Shift, Root, Tail, TailOffset). -spec to_list_loop_tuple(list(), tree_node(), index()) -> list(). to_list_loop_tuple(List, _Node, 0) -> List; to_list_loop_tuple(List, Node, Current) -> to_list_loop_tuple([element(Current, Node) | List], Node, Current - 1). -spec cons_node(any(), size(), vector()) -> vector(). cons_node(Node, NodeSize, #clj_vector{size = 0} = Vector) -> %% Vector is empty, the node is the tail Vector#clj_vector{size = NodeSize, tail = Node}; cons_node( Node , NodeSize , #clj_vector{ size = Size , shift = Shift , root = Root , tail = Tail } ) when (Size bsr ?SHIFT) > (1 bsl Shift) -> %% Root overflow NewPath = new_path(Shift, Tail), InitValues = [{1, Root}, {2, NewPath}], NewRoot = erlang:make_tuple(?NODE_SIZE, ?NIL, InitValues), #clj_vector{ size = Size + NodeSize , shift = Shift + ?SHIFT , root = NewRoot , tail = Node , tailoff = tailoff(Size + NodeSize) }; cons_node( Node , NodeSize , #clj_vector{ size = Size , shift = Shift , root = Root , tail = Tail } = Vector ) -> %% Otherwise just push the tail NewRoot = push_tail(Size, Shift, Root, Tail), Vector#clj_vector{ size = Size + NodeSize , root = NewRoot , tail = Node , tailoff = tailoff(Size + NodeSize) }.