Skip to content

Commit 7776c83

Browse files
committed
WIP: map support
1 parent e602076 commit 7776c83

File tree

5 files changed

+95
-16
lines changed

5 files changed

+95
-16
lines changed

include/proper.hrl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@
4747
%%------------------------------------------------------------------------------
4848

4949
-import(proper_types, [integer/2, float/2, atom/0, binary/0, binary/1,
50-
bitstring/0, bitstring/1, list/1, vector/2, union/1,
51-
weighted_union/1, tuple/1, loose_tuple/1, exactly/1,
52-
fixed_list/1, function/2, map/2, any/0]).
50+
bitstring/0, bitstring/1, list/1, map/1, map/2, map_union/2,
51+
vector/2, union/1, weighted_union/1, tuple/1, loose_tuple/1,
52+
exactly/1, fixed_list/1, fixed_map/1, function/2, any/0]).
5353

5454

5555
%%------------------------------------------------------------------------------

src/proper_types.erl

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,13 @@
142142
-export([integer/2, float/2, atom/0, binary/0, binary/1, bitstring/0,
143143
bitstring/1, list/1, vector/2, union/1, weighted_union/1, tuple/1,
144144
loose_tuple/1, exactly/1, fixed_list/1, fixed_map/1, function/2, map/0,
145-
map/2, any/0, shrink_list/1, safe_union/1, safe_weighted_union/1]).
145+
map/1, map/2, any/0, shrink_list/1, safe_union/1, safe_weighted_union/1]).
146146
-export([integer/0, non_neg_integer/0, pos_integer/0, neg_integer/0, range/2,
147147
float/0, non_neg_float/0, number/0, boolean/0, byte/0, char/0, nil/0,
148148
list/0, tuple/0, string/0, wunion/1, term/0, timeout/0, arity/0]).
149149
-export([int/0, nat/0, largeint/0, real/0, bool/0, choose/2, elements/1,
150150
oneof/1, frequency/1, return/1, default/2, orderedlist/1, function0/1,
151-
function1/1, function2/1, function3/1, function4/1,
151+
function1/1, function2/1, function3/1, function4/1, map_union/1,
152152
weighted_default/2]).
153153
-export([resize/2, non_empty/1, noshrink/1]).
154154

@@ -1120,28 +1120,49 @@ function_is_instance(Type, X) ->
11201120
map() ->
11211121
?LAZY(map(any(), any())).
11221122

1123+
%% @doc A map whose keys and values are defined by the given `Map'.
1124+
%%
1125+
%% Shrinks towards the empty map. That is, all keys are assumed to be optional.
1126+
%%
1127+
%% Also written simply as a {@link maps. map}.
1128+
-spec map(#{Key::raw_type() => Value::raw_type()}) -> proper_types:type().
1129+
map(Map) when is_map(Map) ->
1130+
MapType = maps:map(fun(_Key, Value) -> cook_outer(Value) end, Map),
1131+
?CONTAINER([
1132+
{generator, {typed, fun map_gen/1}},
1133+
{is_instance, {typed, fun map_is_instance/2}},
1134+
{internal_types, MapType},
1135+
{get_length, fun maps:size/1},
1136+
{join, fun maps:merge/2},
1137+
{get_indices, fun fixed_map_get_keys/2},
1138+
{remove, fun maps:remove/2},
1139+
{retrieve, fun maps:get/2},
1140+
{update, fun maps:update/3}
1141+
]).
1142+
11231143
%% @doc A map whose keys are defined by the generator `K' and values
11241144
%% by the generator `V'.
11251145
-spec map(K::raw_type(), V::raw_type()) -> proper_types:type().
11261146
map(K, V) ->
11271147
?LET(L, list({K, V}), maps:from_list(L)).
11281148

1149+
%% @doc A map merged from the given map generators.
1150+
-spec map_union([Map::raw_type()]) -> proper_types:type().
1151+
map_union(RawMaps) when is_list(RawMaps) ->
1152+
?LET(Maps, RawMaps, lists:foldl(fun maps:merge/2, #{}, Maps)).
1153+
11291154
%% @doc A map whose keys and values are defined by the given `Map'.
11301155
%% Also written simply as a {@link maps. map}.
11311156
-spec fixed_map(#{Key::raw_type() => Value::raw_type()}) -> proper_types:type().
1132-
% fixed_map(Map) when is_map(Map) ->
1133-
% Pairs = maps:to_list(Map),
1134-
% ?LET(L, fixed_list(Pairs), maps:from_list(L)).
1135-
11361157
fixed_map(Map) when is_map(Map) ->
1158+
MapType = maps:map(fun(_Key, Value) -> cook_outer(Value) end, Map),
11371159
?CONTAINER([
11381160
{generator, {typed, fun map_gen/1}},
11391161
{is_instance, {typed, fun map_is_instance/2}},
1140-
{internal_types, Map},
1162+
{internal_types, MapType},
11411163
{get_length, fun maps:size/1},
11421164
{join, fun maps:merge/2},
1143-
{get_indices, fun maps:keys/1},
1144-
{remove, fun maps:remove/2},
1165+
{get_indices, fun fixed_map_get_keys/2},
11451166
{retrieve, fun maps:get/2},
11461167
{update, fun maps:update/3}
11471168
]).
@@ -1184,6 +1205,10 @@ map_all_internal(Fun, none, Result) when is_function(Fun, 2) andalso is_boolean(
11841205
map_all_internal(Fun, {Key, Value, NextIterator}, true) when is_function(Fun, 2) ->
11851206
map_all_internal(Fun, NextIterator, Fun(Key, Value)).
11861207

1208+
fixed_map_get_keys(Type, _X) ->
1209+
Map = get_prop(internal_types, Type),
1210+
maps:keys(Map).
1211+
11871212
%% @doc All Erlang terms (that PropEr can produce). For reasons of efficiency,
11881213
%% functions are never produced as instances of this type.<br />
11891214
%% CAUTION: Instances of this type are expensive to produce, shrink and instance-

src/proper_typeserver.erl

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,6 +1648,8 @@ convert(_Mod, {type,_,nonempty_string,[]}, State, _Stack, _VarDict) ->
16481648
{ok, {simple,proper_types:non_empty(proper_types:string())}, State};
16491649
convert(_Mod, {type,_,map,any}, State, _Stack, _VarDict) ->
16501650
{ok, {simple,proper_types:map()}, State};
1651+
convert(Mod, {type,_,map,Fields}, State, Stack, VarDict) ->
1652+
convert_map(Mod, Fields, State, Stack, VarDict);
16511653
convert(_Mod, {type,_,tuple,any}, State, _Stack, _VarDict) ->
16521654
{ok, {simple,proper_types:tuple()}, State};
16531655
convert(Mod, {type,_,tuple,ElemForms}, State, Stack, VarDict) ->
@@ -1787,6 +1789,57 @@ convert_normal_rec_list(RecFun, RecArgs, NonEmpty) ->
17871789
NewRecArgs = clean_rec_args(RecArgs),
17881790
{NewRecFun, NewRecArgs}.
17891791

1792+
-spec convert_map(mod_name(), [Field], state(), stack(), var_dict()) ->
1793+
rich_result2(ret_type(), state())
1794+
when
1795+
Field :: {type, erl_anno:anno(), map_field_assoc, [abs_type()]}
1796+
| {type, erl_anno:anno(), map_field_exact, [abs_type()]}.
1797+
convert_map(Mod, Fields, State1, Stack, VarDict) ->
1798+
{AbstractRequiredFields, AbstractOptionalFields} = lists:partition(
1799+
fun ({type, _, map_field_exact, _FieldType}) ->
1800+
true;
1801+
({type, _, map_field_assoc, _FieldType}) ->
1802+
false
1803+
end,
1804+
Fields
1805+
),
1806+
case process_map_fields(Mod, AbstractRequiredFields, State1, Stack, VarDict) of
1807+
{ok, RequiredFields, State2} ->
1808+
case process_map_fields(Mod, AbstractOptionalFields, State2, Stack, VarDict) of
1809+
{ok, OptionalFields, State3} ->
1810+
Required = proper_types:fixed_map(maps:from_list(RequiredFields)),
1811+
Optional = proper_types:map(maps:from_list(OptionalFields)),
1812+
{ok, {simple, proper_types:map_union([Required, Optional])}, State3};
1813+
{error, Reason} ->
1814+
{error, Reason}
1815+
end;
1816+
{error, Reason} ->
1817+
{error, Reason}
1818+
end.
1819+
1820+
process_map_fields(Mod, AbstractFields, State, Stack, VarDict) ->
1821+
Process =
1822+
fun ({type, _, _, RawFieldTypes}, {ok, Fields, State1}) when
1823+
length(RawFieldTypes) =:= 2
1824+
->
1825+
case process_list(
1826+
Mod, RawFieldTypes, State1, [map | Stack], VarDict
1827+
) of
1828+
{ok, FieldTypes, State2} ->
1829+
{ok, [list_to_tuple(FieldTypes) | Fields], State2};
1830+
{error, Reason} ->
1831+
{error, Reason}
1832+
end;
1833+
(_FieldTypes, {error, Reason}) ->
1834+
{error, Reason}
1835+
end,
1836+
case lists:foldl(Process, {ok, [], State}, AbstractFields) of
1837+
{ok, ReverseFields, NewState} ->
1838+
{ok, lists:reverse(ReverseFields), NewState};
1839+
{error, Reason} ->
1840+
{error, Reason}
1841+
end.
1842+
17901843
-spec convert_tuple(mod_name(), [abs_type()], boolean(), state(), stack(),
17911844
var_dict()) -> rich_result2(ret_type(),state()).
17921845
convert_tuple(Mod, ElemForms, ToList, State, Stack, VarDict) ->

test/proper_exported_types_test.erl

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@
4040
%%
4141
%% Still, the test is currently not 100% there.
4242
%% TODOs:
43-
%% - Eliminate the 12 errors that `proper_typeserver:demo_translate_type/2`
44-
%% currently returns. (Three of these errors are due to the incomplete
45-
%% handling of maps.)
4643
%% - Handle symbolic instances (the {'$call', ...} case below).
4744
%%
4845

test/proper_tests.erl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,10 @@ simple_types_with_data() ->
410410
{[], [[]], [], [[a],[1,2,3]], "[]"},
411411
{fixed_list([neg_integer(),pos_integer()]), [[-12,32],[-1,1]], [-1,1],
412412
[[0,0]], none},
413+
{map(#{key => value, pos_integer() => neg_integer()}),
414+
[#{key => value, 1 => -1}], #{}, [not_a_map], none},
415+
{fixed_map(#{key => value, pos_integer() => neg_integer()}),
416+
[#{key => value, 3 => -3}], #{key => value, 1 => -1}, [not_a_map], none},
413417
{[atom(),integer(),atom(),float()], [[forty_two,42,forty_two,42.0]],
414418
['',0,'',0.0], [[proper,is,licensed],[under,the,gpl]], none},
415419
{[42 | list(integer())], [[42],[42,44,22]], [42], [[],[11,12]], none},
@@ -773,7 +777,7 @@ cant_generate_test_() ->
773777
[?_test(assert_cant_generate(Type)) || Type <- impossible_types()].
774778

775779
proper_exported_types_test_() ->
776-
[?_assertEqual({[],12}, proper_exported_types_test:not_handled())].
780+
[?_assertEqual({[],0}, proper_exported_types_test:not_handled())].
777781

778782
%%------------------------------------------------------------------------------
779783
%% Verify that failing constraints are correctly reported

0 commit comments

Comments
 (0)