%%% -*- coding: utf-8 -*- %%% -*- erlang-indent-level: 2 -*- %%% ------------------------------------------------------------------- %%% Copyright 2010-2020 Manolis Papadakis , %%% Eirini Arvaniti %%% and Kostis Sagonas %%% %%% This file is part of PropEr. %%% %%% PropEr is free software: you can redistribute it and/or modify %%% it under the terms of the GNU General Public License as published by %%% the Free Software Foundation, either version 3 of the License, or %%% (at your option) any later version. %%% %%% PropEr is distributed in the hope that it will be useful, %%% but WITHOUT ANY WARRANTY; without even the implied warranty of %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %%% GNU General Public License for more details. %%% %%% You should have received a copy of the GNU General Public License %%% along with PropEr. If not, see . %%% @copyright 2010-2020 Manolis Papadakis, Eirini Arvaniti and Kostis Sagonas %%% @version {@version} %%% @author Manolis Papadakis %%% @doc The shrinking subsystem and all predefined shrinkers are contained in %%% this module. %%% @private -module(proper_shrink). -export([shrink/3]). -export([number_shrinker/4, union_first_choice_shrinker/3, union_recursive_shrinker/3]). -export([split_shrinker/3, remove_shrinker/3]). -export_type([state/0, shrinker/0]). -include("proper_internal.hrl"). %%------------------------------------------------------------------------------ %% Types %%------------------------------------------------------------------------------ -type state() :: 'init' | 'done' | {'shrunk',position(),state()} | term(). -type shrinker() :: fun((proper_gen:imm_instance(), proper_types:type(), state()) -> {[proper_gen:imm_instance()],state()}). %%------------------------------------------------------------------------------ %% Main shrinking functions %%------------------------------------------------------------------------------ -spec shrink(proper_gen:imm_instance(), proper_types:type(), state()) -> {[proper_gen:imm_instance()],state()}. %% We reject all shrunk instances that don't satisfy all constraints. A full %% is_instance check is not necessary if we assume that generators and shrinkers %% always return valid instances of the base type. shrink(ImmInstance, Type, init) -> Shrinkers = get_shrinkers(Type), shrink(ImmInstance, Type, {shrinker,Shrinkers,dummy,init}); shrink(_ImmInstance, _Type, {shrinker,[],_Lookup,init}) -> {[], done}; shrink(ImmInstance, Type, {shrinker,[_Shrinker | Rest],_Lookup,done}) -> shrink(ImmInstance, Type, {shrinker,Rest,dummy,init}); shrink(ImmInstance, Type, {shrinker,Shrinkers,_Lookup,State}) -> [Shrinker | _Rest] = Shrinkers, {DirtyImmInstances,NewState} = Shrinker(ImmInstance, Type, State), SatisfiesAll = fun(I) -> Instance = proper_gen:clean_instance(I), proper_types:weakly(proper_types:satisfies_all(Instance, Type)) end, {NewImmInstances,NewLookup} = proper_arith:filter(SatisfiesAll, DirtyImmInstances), {NewImmInstances, {shrinker,Shrinkers,NewLookup,NewState}}; shrink(ImmInstance, Type, {shrunk,N,{shrinker,Shrinkers,Lookup,State}}) -> ActualN = lists:nth(N, Lookup), shrink(ImmInstance, Type, {shrinker,Shrinkers,dummy,{shrunk,ActualN,State}}). -spec get_shrinkers(proper_types:type()) -> [shrinker()]. get_shrinkers(Type) -> case proper_types:find_prop(noshrink, Type) of {ok, true} -> []; _ -> CustomShrinkers = case proper_types:find_prop(shrinkers, Type) of {ok, Shrinkers} -> Shrinkers; error -> [] end, StandardShrinkers = case proper_types:get_prop(kind, Type) of basic -> []; wrapper -> [fun alternate_shrinker/3, fun unwrap_shrinker/3]; constructed -> case proper_types:get_prop(shrink_to_parts, Type) of true -> [fun to_part_shrinker/3, fun parts_shrinker/3, fun in_shrinker/3]; false -> [fun parts_shrinker/3, fun in_shrinker/3] end; container -> [fun split_shrinker/3, fun remove_shrinker/3, fun elements_shrinker/3]; _Other -> [] end, CustomShrinkers ++ StandardShrinkers end. %%------------------------------------------------------------------------------ %% Wrapper type shrinkers %%------------------------------------------------------------------------------ %% Since shrinking only happens for generated values, any native types have %% already been produced by the typeserver, thus we are sure we won't get a %% typeserver exception. -spec alternate_shrinker(proper_gen:imm_instance(), proper_types:type(), state()) -> {[proper_gen:imm_instance()],state()}. %% we stop at the smaller alternative shrinker %% TODO: 'is_raw_type' check? alternate_shrinker(Instance, Type, init) -> Choices = proper_types:unwrap(Type), union_first_choice_shrinker(Instance, Choices, init); alternate_shrinker(_Instance, _Type, _State) -> {[], done}. -spec unwrap_shrinker(proper_gen:imm_instance(), proper_types:type(), state()) -> {[proper_gen:imm_instance()],state()}. unwrap_shrinker(Instance, Type, init) -> Choices = proper_types:unwrap(Type), union_recursive_shrinker(Instance, Choices, init); unwrap_shrinker(Instance, _Type, State) -> union_recursive_shrinker(Instance, [], State). %%------------------------------------------------------------------------------ %% Constructed type shrinkers %%------------------------------------------------------------------------------ -spec to_part_shrinker(proper_gen:imm_instance(), proper_types:type(), state()) -> {[proper_gen:imm_instance()],state()}. to_part_shrinker({'$used',ImmParts,_ImmInstance}, _Type, init) -> {[{'$to_part',P} || P <- ImmParts], done}; to_part_shrinker(_Instance, _Type, _State) -> {[], done}. -spec parts_shrinker(proper_gen:imm_instance(), proper_types:type(), state()) -> {[proper_gen:imm_instance()],state()}. %% TODO: move some of the generation code in the proper_gen module parts_shrinker(Instance = {'$used',_ImmParts,_ImmInstance}, Type, init) -> PartsType = proper_types:get_prop(parts_type, Type), parts_shrinker(Instance, Type, {parts,PartsType,dummy,init}); parts_shrinker(_CleanInstance, _Type, init) -> {[], done}; parts_shrinker(_Instance, _Type, {parts,_PartsType,_Lookup,done}) -> {[], done}; parts_shrinker({'$used',ImmParts,ImmInstance}, Type, {parts,PartsType,_Lookup,PartsState}) -> {NewImmParts,NewPartsState} = shrink(ImmParts, PartsType, PartsState), Combine = proper_types:get_prop(combine, Type), DirtyInstances = [try_combine(P, ImmInstance, Combine) || P <- NewImmParts], NotError = fun({ok,_}) -> true; (error) -> false end, {NewOKInstances,NewLookup} = proper_arith:filter(NotError, DirtyInstances), NewInstances = [X || {ok,X} <- NewOKInstances], {NewInstances, {parts,PartsType,NewLookup,NewPartsState}}; parts_shrinker(Instance, Type, {shrunk,N,{parts,PartsType,Lookup,PartsState}}) -> ActualN = lists:nth(N, Lookup), parts_shrinker(Instance, Type, {parts,PartsType,dummy,{shrunk,ActualN,PartsState}}). -spec try_combine(proper_gen:imm_instance(), proper_gen:imm_instance(), proper_gen:combine_fun()) -> {'ok',proper_gen:imm_instance()} | 'error'. try_combine(ImmParts, OldImmInstance, Combine) -> Parts = proper_gen:clean_instance(ImmParts), ImmInstance = Combine(Parts), case proper_types:is_raw_type(ImmInstance) of true -> InnerType = proper_types:cook_outer(ImmInstance), %% TODO: special case if the immediately internal is a LET? %% TODO: more specialized is_instance check here? %% This should never throw an exception, provided the instance %% has already been instance-checked. case proper_types:is_instance(OldImmInstance, InnerType) of true -> {ok,{'$used',ImmParts,OldImmInstance}}; false -> %% TODO: return more than one? then we must flatten case proper_gen:safe_generate(InnerType) of {ok,NewImmInstance} -> {ok,{'$used',ImmParts,NewImmInstance}}; {error,_Reason} -> error end end; false -> {ok,{'$used',ImmParts,ImmInstance}} end. -spec in_shrinker(proper_gen:imm_instance(), proper_types:type(), state()) -> {[proper_gen:imm_instance()],state()}. in_shrinker(Instance = {'$used',ImmParts,_ImmInstance}, Type, init) -> Combine = proper_types:get_prop(combine, Type), Parts = proper_gen:clean_instance(ImmParts), ImmInstance = Combine(Parts), %% TODO: more specialized raw_type check here? case proper_types:is_raw_type(ImmInstance) of true -> InnerType = proper_types:cook_outer(ImmInstance), in_shrinker(Instance, Type, {inner,InnerType,init}); false -> {[], done} end; in_shrinker(Instance = {'$to_part',ImmInstance}, Type, init) -> %% TODO: move this to proper_types PartsType = proper_types:get_prop(parts_type, Type), case {proper_types:find_prop(internal_type,PartsType), proper_types:find_prop(internal_types,PartsType)} of {{ok,EachPartType},error} -> in_shrinker(Instance, Type, {part_rec,EachPartType,init}); {error,{ok,PartTypesList}} -> IsInst = fun(T) -> proper_types:is_instance(ImmInstance,T) end, {_Pos,PartType} = proper_arith:find_first(IsInst, PartTypesList), in_shrinker(Instance, Type, {part_rec,PartType,init}) end; in_shrinker(_CleanInstance, _Type, init) -> {[], done}; in_shrinker(_Instance, _Type, {_Decl,_RecType,done}) -> {[], done}; in_shrinker({'$used',ImmParts,ImmInstance}, _Type, {inner,InnerType,InnerState}) -> {NewImmInstances,NewInnerState} = shrink(ImmInstance, InnerType, InnerState), NewInstances = [{'$used',ImmParts,I} || I <- NewImmInstances], {NewInstances, {inner,InnerType,NewInnerState}}; in_shrinker({'$to_part',ImmInstance}, _Type, {part_rec,PartType,PartState}) -> {NewImmInstances,NewPartState} = shrink(ImmInstance, PartType, PartState), NewInstances = [{'$to_part',I} || I <- NewImmInstances], {NewInstances, {part_rec,PartType,NewPartState}}; in_shrinker(Instance, Type, {shrunk,N,{Decl,RecType,InnerState}}) -> in_shrinker(Instance, Type, {Decl,RecType,{shrunk,N,InnerState}}). %%------------------------------------------------------------------------------ %% Container type shrinkers %%------------------------------------------------------------------------------ -spec split_shrinker(proper_gen:imm_instance(), proper_types:type(), state()) -> {[proper_gen:imm_instance()],state()}. split_shrinker(Instance, Type, init) -> case {proper_types:find_prop(split, Type), proper_types:find_prop(get_length, Type), proper_types:find_prop(join, Type)} of {error, _, _} -> {[], done}; {{ok,_Split}, error, _} -> split_shrinker(Instance, Type, no_pos); {{ok,_Split}, {ok,GetLength}, {ok,_Join}} -> split_shrinker(Instance, Type, {slices,2,GetLength(Instance)}) end; split_shrinker(Instance, Type, no_pos) -> Split = proper_types:get_prop(split, Type), {Split(Instance), done}; split_shrinker(Instance, Type, {shrunk,done}) -> split_shrinker(Instance, Type, no_pos); %% implementation of the ddmin algorithm, but stopping before the granularity %% reaches 1, since we run a 'remove' shrinker after this %% TODO: on success, start over with the whole testcase or keep removing slices? split_shrinker(Instance, Type, {slices,N,Len}) -> case Len < 2 * N of true -> {[], done}; false -> {SmallSlices,BigSlices} = slice(Instance, Type, N, Len), {SmallSlices ++ BigSlices, {slices,2*N,Len}} end; split_shrinker(Instance, Type, {shrunk,Pos,{slices,DoubleN,_Len}}) -> N = DoubleN div 2, GetLength = proper_types:get_prop(get_length, Type), case Pos =< N of true -> split_shrinker(Instance, Type, {slices,2,GetLength(Instance)}); false -> split_shrinker(Instance, Type, {slices,N-1,GetLength(Instance)}) end. -spec slice(proper_gen:imm_instance(), proper_types:type(), pos_integer(), proper_types:length()) -> {[proper_gen:imm_instance()],[proper_gen:imm_instance()]}. slice(Instance, Type, Slices, Len) -> BigSlices = Len rem Slices, SmallSlices = Slices - BigSlices, SmallSliceLen = Len div Slices, BigSliceLen = SmallSliceLen + 1, BigSliceTotal = BigSlices * BigSliceLen, WhereToSlice = [{1 + X * BigSliceLen, BigSliceLen} || X <- lists:seq(0, BigSlices - 1)] ++ [{BigSliceTotal + 1 + X * SmallSliceLen, SmallSliceLen} || X <- lists:seq(0, SmallSlices - 1)], lists:unzip([take_slice(Instance, Type, From, SliceLen) || {From,SliceLen} <- WhereToSlice]). -spec take_slice(proper_gen:imm_instance(), proper_types:type(), pos_integer(), proper_types:length()) -> {proper_gen:imm_instance(),proper_gen:imm_instance()}. take_slice(Instance, Type, From, SliceLen) -> Split = proper_types:get_prop(split, Type), Join = proper_types:get_prop(join, Type), {Front,ImmBack} = Split(From - 1, Instance), {Slice,Back} = Split(SliceLen, ImmBack), {Slice, Join(Front, Back)}. -spec remove_shrinker(proper_gen:imm_instance(), proper_types:type(), state()) -> {[proper_gen:imm_instance()], state()}. %% TODO: try removing more than one elemnent: 2,4,... or 2,3,... - when to stop? remove_shrinker(Instance, Type, init) -> case {proper_types:find_prop(get_indices, Type), proper_types:find_prop(remove, Type)} of {{ok,_GetIndices}, {ok,_Remove}} -> remove_shrinker(Instance, Type, {shrunk,1,{indices,ordsets:from_list([]),dummy}}); _ -> {[], done} end; remove_shrinker(_Instance, _Type, {indices,_Checked,[]}) -> {[], done}; remove_shrinker(Instance, Type, {indices,Checked,[Index | Rest]}) -> Remove = proper_types:get_prop(remove, Type), {[Remove(Index, Instance)], {indices,ordsets:add_element(Index, Checked),Rest}}; remove_shrinker(Instance, Type, {shrunk,1,{indices,Checked,_ToCheck}}) -> %% TODO: normally, indices wouldn't be expected to change for the remaining %% elements, but this happens for lists, so we'll just avoid %% re-checking any indices we have already tried (even though these %% might correspond to new elements now - at least they don't in the %% case of lists) %% TODO: ordsets are used to ensure efficiency, but the ordsets module %% compares elements with == instead of =:=, that could cause us to %% miss some elements in some cases GetIndices = proper_types:get_prop(get_indices, Type), Indices = ordsets:from_list(GetIndices(Type, Instance)), NewToCheck = ordsets:subtract(Indices, Checked), remove_shrinker(Instance, Type, {indices,Checked,NewToCheck}). -spec elements_shrinker(proper_gen:imm_instance(), proper_types:type(), state()) -> {[proper_gen:imm_instance()],state()}. %% TODO: is it safe to assume that all functions and the indices will not change %% after any update? %% TODO: shrink many elements concurrently? elements_shrinker(Instance, Type, init) -> case {proper_types:find_prop(get_indices, Type), proper_types:find_prop(retrieve, Type), proper_types:find_prop(update, Type)} of {{ok,GetIndices}, {ok,Retrieve}, {ok,_Update}} -> GetElemType = case proper_types:find_prop(internal_type, Type) of {ok,RawInnerType} -> InnerType = proper_types:cook_outer(RawInnerType), fun(_I) -> InnerType end; error -> InnerTypes = proper_types:get_prop(internal_types, Type), fun(I) -> Retrieve(I, InnerTypes) end end, Indices = GetIndices(Type, Instance), elements_shrinker(Instance, Type, {inner,Indices,GetElemType,init}); _ -> {[], done} end; elements_shrinker(_Instance, _Type, {inner,[],_GetElemType,init}) -> {[], done}; elements_shrinker(Instance, Type, {inner,[_Index | Rest],GetElemType,done}) -> elements_shrinker(Instance, Type, {inner,Rest,GetElemType,init}); elements_shrinker(Instance, Type, {inner,Indices = [Index | _Rest],GetElemType,InnerState}) -> Retrieve = proper_types:get_prop(retrieve, Type), Update = proper_types:get_prop(update, Type), ImmElem = Retrieve(Index, Instance), InnerType = GetElemType(Index), {NewImmElems,NewInnerState} = shrink(ImmElem, InnerType, InnerState), NewInstances = [Update(Index, E, Instance) || E <- NewImmElems], {NewInstances, {inner,Indices,GetElemType,NewInnerState}}; elements_shrinker(Instance, Type, {shrunk,N,{inner,Indices,GetElemType,InnerState}}) -> elements_shrinker(Instance, Type, {inner,Indices,GetElemType,{shrunk,N,InnerState}}). %%------------------------------------------------------------------------------ %% Custom shrinkers %%------------------------------------------------------------------------------ -spec number_shrinker(number(), proper_types:extnum(), proper_types:extnum(), state()) -> {[number()],state()}. number_shrinker(X, Low, High, init) -> {Target,Inc,OverLimit} = find_target(X, Low, High), case X =:= Target of true -> {[], done}; false -> {[Target], {inc,Target,Inc,OverLimit}} end; number_shrinker(_X, _Low, _High, {inc,Last,Inc,OverLimit}) -> NewLast = Inc(Last), case OverLimit(NewLast) of true -> {[], done}; false -> {[NewLast], {inc,NewLast,Inc,OverLimit}} end; number_shrinker(_X, _Low, _High, {shrunk,_Pos,_State}) -> {[], done}. -spec find_target(number(), number(), number()) -> {number(),fun((number()) -> number()),fun((number()) -> boolean())}. find_target(X, Low, High) -> case {proper_types:le(Low,0), proper_types:le(0,High)} of {false, _} -> Limit = find_limit(X, Low, High, High), {Low, fun(Y) -> Y + 1 end, fun(Y) -> Y > Limit end}; {true,false} -> Limit = find_limit(X, Low, High, Low), {High, fun(Y) -> Y - 1 end, fun(Y) -> Y < Limit end}; {true,true} -> Sign = sign(X), OverLimit = case X >= 0 of true -> Limit = find_limit(X, Low, High, High), fun(Y) -> Y > Limit end; false -> Limit = find_limit(X, Low, High, Low), fun(Y) -> Y < Limit end end, {zero(X), fun(Y) -> Y + Sign end, OverLimit} end. -spec find_limit(number(), number(), number(), number()) -> number(). find_limit(X, Low, High, FallBack) -> case proper_types:le(Low, X) andalso proper_types:le(X, High) of true -> X; false -> FallBack end. -spec sign(number()) -> number(). sign(X) -> if X > 0 -> 1; X < 0 -> -1; true -> zero(X) end. -spec zero(number()) -> number(). zero(X) when is_integer(X) -> 0; zero(X) when is_float(X) -> 0.0. -spec union_first_choice_shrinker(proper_gen:imm_instance(), [proper_types:type()], state()) -> {[proper_gen:imm_instance()],state()}. %% TODO: do this incrementally? union_first_choice_shrinker(Instance, Choices, init) -> case first_plausible_choice(Instance, Choices) of none -> {[],done}; {N,_Type} -> PriorChoices = lists:sublist(Choices, N - 1), DirtyInstances = [proper_gen:safe_generate(T) || T <- PriorChoices], NewInstances = [X || {ok,X} <- DirtyInstances], {NewInstances,done} end; union_first_choice_shrinker(_Instance, _Choices, {shrunk,_Pos,done}) -> {[], done}. -spec union_recursive_shrinker(proper_gen:imm_instance(), [proper_types:type()], state()) -> {[proper_gen:imm_instance()],state()}. union_recursive_shrinker(Instance, Choices, init) -> case first_plausible_choice(Instance, Choices) of none -> {[],done}; {N,Type} -> union_recursive_shrinker(Instance, Choices, {inner,N,Type,init}) end; union_recursive_shrinker(_Instance, _Choices, {inner,_N,_Type,done}) -> {[],done}; union_recursive_shrinker(Instance, _Choices, {inner,N,Type,InnerState}) -> {NewInstances,NewInnerState} = shrink(Instance, Type, InnerState), {NewInstances, {inner,N,Type,NewInnerState}}; union_recursive_shrinker(Instance, Choices, {shrunk,Pos,{inner,N,Type,InnerState}}) -> union_recursive_shrinker(Instance, Choices, {inner,N,Type,{shrunk,Pos,InnerState}}). -spec first_plausible_choice(proper_gen:imm_instance(), [proper_types:type()]) -> {position(),proper_types:type()} | 'none'. first_plausible_choice(Instance, Choices) -> %% This should never throw an exception, provided the instance has already %% been instance-checked. IsInstance = fun(Type) -> proper_types:is_instance(Instance, Type) end, proper_arith:find_first(IsInstance, Choices).