%%% -*- coding: utf-8 -*- %%% -*- erlang-indent-level: 2 -*- %%% ------------------------------------------------------------------- %%% Copyright 2010-2017 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-2017 Manolis Papadakis, Eirini Arvaniti and Kostis Sagonas %%% @version {@version} %%% @author Manolis Papadakis %%% @doc PropEr's main parse transform. It is automatically applied to modules %%% when including the main PropEr header, unless `PROPER_NO_TRANS' is defined. %%% Applying this transform has the following effects: %%%
    %%%
  • All 0-arity functions whose name begins with `prop_' are automatically %%% exported.
  • %%%
  • Type declarations in `?FORALL's that correspond to native types are %%% properly substituted (with some limitations, see the %%% {@link proper_typeserver} module for details).
  • %%%
-module(proper_transformer). -export([parse_transform/2]). -export_type([]). -include("proper_internal.hrl"). %%------------------------------------------------------------------------------ %% Types %%------------------------------------------------------------------------------ -record(mod_info, {name :: mod_name(), export_all = false :: boolean(), funs = sets:new() :: proper_typeserver:mod_exp_funs(), imports = sets:new() :: proper_typeserver:mod_exp_funs(), no_autos = sets:new() :: proper_typeserver:mod_exp_funs(), exp_types = sets:new() :: proper_typeserver:mod_exp_types(), exp_funs = sets:new() :: proper_typeserver:mod_exp_funs(), helper_pid :: pid() | 'undefined'}). -type mod_info() :: #mod_info{}. -type exp_mod_data () :: 'nodata' | {'data', proper_typeserver:mod_exp_types(), proper_typeserver:mod_exp_funs()}. -type exp_dict() :: dict:dict(mod_name(), exp_mod_data()). %%------------------------------------------------------------------------------ %% Top-level functions %%------------------------------------------------------------------------------ %% @private -spec parse_transform([abs_form()], [compile:option()]) -> [abs_form()]. parse_transform(Forms, Options) -> RawModInfo = collect_info(Forms, Options), #mod_info{name = ModName, export_all = ExportAll, funs = AllFuns, exp_types = ExpTypes, exp_funs = RawExpFuns} = RawModInfo, {ExpFuns,PropsToExport} = case ExportAll of true -> {AllFuns, []}; false -> AllProps = sets:filter(fun is_prop/1, AllFuns), {RawExpFuns, sets:to_list(sets:subtract(AllProps,RawExpFuns))} end, HelperPid = helper_start(ModName, ExpTypes, ExpFuns), ModInfo = RawModInfo#mod_info{helper_pid = HelperPid}, NewForms = [rewrite_form(F,ModInfo) || F <- Forms], helper_stop(HelperPid), add_exports(NewForms, PropsToExport). -spec collect_info([abs_form()], [compile:option()]) -> mod_info(). collect_info(Forms, Options) -> StartModInfo = add_options(#mod_info{}, Options), lists:foldl(fun add_info/2, StartModInfo, Forms). -spec add_options(mod_info(), compile:option() | [compile:option()]) -> mod_info(). add_options(ModInfo, []) -> ModInfo; add_options(ModInfo, [export_all | Rest]) -> add_options(ModInfo#mod_info{export_all = true}, Rest); add_options(#mod_info{no_autos = NoAutos} = ModInfo, [{no_auto_import,FunsList} | Rest]) -> NewNoAutos = sets:union(sets:from_list(FunsList), NoAutos), add_options(ModInfo#mod_info{no_autos = NewNoAutos}, Rest); add_options(ModInfo, [_OtherOption | Rest]) -> add_options(ModInfo, Rest); add_options(ModInfo, SingleOption) -> add_options(ModInfo, [SingleOption]). -spec add_info(abs_form(), mod_info()) -> mod_info(). add_info({attribute,_Line,module,ModName}, ModInfo) -> ModInfo#mod_info{name = ModName}; add_info({function,_Line,Name,Arity,_Clauses}, #mod_info{funs = Funs} = ModInfo) -> NewFuns = sets:add_element({Name,Arity}, Funs), ModInfo#mod_info{funs = NewFuns}; add_info({attribute,_Line,import,{_FromMod,MoreImports}}, #mod_info{imports = Imports} = ModInfo) -> NewImports = sets:union(sets:from_list(MoreImports), Imports), ModInfo#mod_info{imports = NewImports}; add_info({attribute,_Line,export_type,MoreExpTypes}, #mod_info{exp_types = ExpTypes} = ModInfo) -> NewExpTypes = sets:union(sets:from_list(MoreExpTypes), ExpTypes), ModInfo#mod_info{exp_types = NewExpTypes}; add_info({attribute,_Line,export,MoreExpFuns}, #mod_info{exp_funs = ExpFuns} = ModInfo) -> NewExpFuns = sets:union(sets:from_list(MoreExpFuns), ExpFuns), ModInfo#mod_info{exp_funs = NewExpFuns}; add_info({attribute,_Line,compile,Options}, ModInfo) -> add_options(ModInfo, Options); add_info(_Form, ModInfo) -> ModInfo. -spec is_prop({fun_name(),arity()}) -> boolean(). is_prop({Name,0}) -> lists:prefix(?PROPERTY_PREFIX, atom_to_list(Name)); is_prop(_) -> false. -spec add_exports([abs_form()], [{fun_name(),arity()}]) -> [abs_form()]. add_exports([], _ToExport) -> []; add_exports([{attribute,_,module,_} = ModAttr | Rest], ToExport) -> ExpAttr = {attribute,?anno(0),export,ToExport}, [ModAttr, ExpAttr | Rest]; add_exports([Form | Rest], ToExport) -> [Form | add_exports(Rest, ToExport)]. %%------------------------------------------------------------------------------ %% Helper server interface %%------------------------------------------------------------------------------ -spec helper_start(mod_name(), proper_typeserver:mod_exp_types(), proper_typeserver:mod_exp_funs()) -> pid(). helper_start(Mod, ModExpTypes, ModExpFuns) -> spawn(fun() -> helper_init(Mod,ModExpTypes,ModExpFuns) end). -spec helper_stop(pid()) -> 'ok'. helper_stop(HelperPid) -> HelperPid ! stop, ok. -spec is_exported_type(mod_name(), atom(), arity(), pid()) -> boolean(). is_exported_type(Mod, Call, Arity, HelperPid) -> HelperPid ! {is_exported_type,self(),Mod,Call,Arity}, receive Answer -> Answer end. -spec helper_init(mod_name(), proper_typeserver:mod_exp_types(), proper_typeserver:mod_exp_funs()) -> 'ok'. helper_init(Mod, ModExpTypes, ModExpFuns) -> ExpDict = dict:from_list([{Mod,{data,ModExpTypes,ModExpFuns}}]), helper_loop(ExpDict). -spec helper_loop(exp_dict()) -> 'ok'. helper_loop(ExpDict) -> receive stop -> ok; {is_exported_type,From,Mod,Call,Arity} -> NewExpDict = add_module(Mod, ExpDict), Answer = case dict:fetch(Mod, NewExpDict) of {data,ModExpTypes,ModExpFuns} -> CallRef = {Call,Arity}, not sets:is_element(CallRef, ModExpFuns) andalso sets:is_element(CallRef, ModExpTypes); nodata -> false end, From ! Answer, helper_loop(NewExpDict) end. -spec add_module(mod_name(), exp_dict()) -> exp_dict(). add_module(Mod, ExpDict) -> case dict:is_key(Mod, ExpDict) of true -> ExpDict; false -> case proper_typeserver:get_exp_info(Mod) of {ok,ModExpTypes,ModExpFuns} -> dict:store(Mod, {data,ModExpTypes,ModExpFuns}, ExpDict); {error,_Reason} -> dict:store(Mod, nodata, ExpDict) end end. %%------------------------------------------------------------------------------ %% ?FORALL detection functions %%------------------------------------------------------------------------------ -spec rewrite_form(abs_form(), mod_info()) -> abs_form(). rewrite_form({attribute,Line,record,{RecName,FieldInits}}, ModInfo) -> NewFieldInits = [rewrite_field_init(F,ModInfo) || F <- FieldInits], {attribute,Line,record,{RecName,NewFieldInits}}; rewrite_form({function,Line,Name,Arity,Clauses}, ModInfo) -> NewClauses = [rewrite_clause(C,ModInfo) || C <- Clauses], {function,Line,Name,Arity,NewClauses}; rewrite_form(Form, _ModInfo) -> Form. -spec rewrite_field_init(abs_rec_field(), mod_info()) -> abs_rec_field(). rewrite_field_init({typed_record_field,RecField,Type}, ModInfo) -> {typed_record_field,rewrite_field_init(RecField, ModInfo),Type}; rewrite_field_init({record_field,_Line,_FieldName} = FieldInit, _ModInfo) -> FieldInit; rewrite_field_init({record_field,Line,FieldName,InitExpr}, ModInfo) -> {record_field,Line,FieldName,rewrite_expr(InitExpr,ModInfo)}. -spec rewrite_clause(abs_clause(), mod_info()) -> abs_clause(). rewrite_clause({clause,Line,PatSeq,Guards,Body}, ModInfo) -> NewPatSeq = [rewrite_expr(P,ModInfo) || P <- PatSeq], NewBody = [rewrite_expr(E,ModInfo) || E <- Body], {clause,Line,NewPatSeq,Guards,NewBody}. %% This also covers some other constructs that don't clash with expressions: %% binary element specifications, list and binary comprehension generators and %% filters, remote function references. It also covers patterns. -spec rewrite_expr(abs_expr(), mod_info()) -> abs_expr(). rewrite_expr({match,Line,Pattern,Expr}, ModInfo) -> NewPattern = rewrite_expr(Pattern, ModInfo), NewExpr = rewrite_expr(Expr, ModInfo), {match,Line,NewPattern,NewExpr}; rewrite_expr({tuple,Line,FieldExprs}, ModInfo) -> NewFieldExprs = [rewrite_expr(F,ModInfo) || F <- FieldExprs], {tuple,Line,NewFieldExprs}; rewrite_expr({cons,Line,HeadExpr,TailExpr}, ModInfo) -> NewHeadExpr = rewrite_expr(HeadExpr, ModInfo), NewTailExpr = rewrite_expr(TailExpr, ModInfo), {cons,Line,NewHeadExpr,NewTailExpr}; rewrite_expr({bin,Line,BinElems}, ModInfo) -> NewBinElems = [rewrite_expr(B,ModInfo) || B <- BinElems], {bin,Line,NewBinElems}; rewrite_expr({bin_element,Line,ValueExpr,Size,TSL}, ModInfo) -> {bin_element,Line,rewrite_expr(ValueExpr,ModInfo),Size,TSL}; rewrite_expr({op,Line,Op,LeftExpr,RightExpr}, ModInfo) -> NewLeftExpr = rewrite_expr(LeftExpr, ModInfo), NewRightExpr = rewrite_expr(RightExpr, ModInfo), {op,Line,Op,NewLeftExpr,NewRightExpr}; rewrite_expr({op,Line,Op,Expr}, ModInfo) -> {op,Line,Op,rewrite_expr(Expr,ModInfo)}; rewrite_expr({record,Line,RecName,FieldInits}, ModInfo) -> NewFieldInits = [rewrite_field_init(F,ModInfo) || F <- FieldInits], {record,Line,RecName,NewFieldInits}; rewrite_expr({record,Line,RecExpr,RecName,FieldInits}, ModInfo) -> NewRecExpr = rewrite_expr(RecExpr, ModInfo), NewFieldInits = [rewrite_field_init(F,ModInfo) || F <- FieldInits], {record,Line,NewRecExpr,RecName,NewFieldInits}; rewrite_expr({record_field,Line,RecExpr,RecName,FieldName}, ModInfo) -> {record_field,Line,rewrite_expr(RecExpr,ModInfo),RecName,FieldName}; rewrite_expr({'catch',Line,Expr}, ModInfo) -> {'catch',Line,rewrite_expr(Expr,ModInfo)}; rewrite_expr({call,Line, {remote,_,{atom,_,proper},{atom,_,forall}} = FunRef, [RawType,Prop]}, ModInfo) -> NewRawType = rewrite_type(RawType, ModInfo), NewProp = rewrite_expr(Prop, ModInfo), {call,Line,FunRef,[NewRawType,NewProp]}; rewrite_expr({call, Line, {remote,_,{atom,_,proper_types}, {atom, _, bind}} = FunRef, [RawType, Gen, ShrinkToParts]}, ModInfo) -> NewRawType = rewrite_type(RawType, ModInfo), NewGen = rewrite_expr(Gen, ModInfo), {call, Line, FunRef, [NewRawType,NewGen, ShrinkToParts]}; rewrite_expr({call,Line,FunRef,Args}, ModInfo) -> NewFunRef = rewrite_expr(FunRef, ModInfo), NewArgs = [rewrite_expr(A,ModInfo) || A <- Args], {call,Line,NewFunRef,NewArgs}; rewrite_expr({remote,Line,ModExpr,FunExpr}, ModInfo) -> NewModExpr = rewrite_expr(ModExpr, ModInfo), NewFunExpr = rewrite_expr(FunExpr, ModInfo), {remote,Line,NewModExpr,NewFunExpr}; rewrite_expr({lc,Line,Expr,GensAndFilters}, ModInfo) -> NewExpr = rewrite_expr(Expr, ModInfo), NewGensAndFilters = [rewrite_expr(W,ModInfo) || W <- GensAndFilters], {lc,Line,NewExpr,NewGensAndFilters}; rewrite_expr({bc,Line,Expr,GensAndFilters}, ModInfo) -> NewExpr = rewrite_expr(Expr, ModInfo), NewGensAndFilters = [rewrite_expr(W,ModInfo) || W <- GensAndFilters], {bc,Line,NewExpr,NewGensAndFilters}; rewrite_expr({generate,Line,Pattern,Expr}, ModInfo) -> NewPattern = rewrite_expr(Pattern, ModInfo), NewExpr = rewrite_expr(Expr, ModInfo), {generate,Line,NewPattern,NewExpr}; rewrite_expr({b_generate,Line,Pattern,Expr}, ModInfo) -> NewPattern = rewrite_expr(Pattern, ModInfo), NewExpr = rewrite_expr(Expr, ModInfo), {b_generate,Line,NewPattern,NewExpr}; rewrite_expr({block,Line,Body}, ModInfo) -> NewBody = [rewrite_expr(E,ModInfo) || E <- Body], {block,Line,NewBody}; rewrite_expr({'if',Line,Clauses}, ModInfo) -> NewClauses = [rewrite_clause(C,ModInfo) || C <- Clauses], {'if',Line,NewClauses}; rewrite_expr({'case',Line,Expr,Clauses}, ModInfo) -> NewExpr = rewrite_expr(Expr, ModInfo), NewClauses = [rewrite_clause(C,ModInfo) || C <- Clauses], {'case',Line,NewExpr,NewClauses}; rewrite_expr({'try',Line,Body1,Clauses1,Clauses2,Body2}, ModInfo) -> NewBody1 = [rewrite_expr(E,ModInfo) || E <- Body1], NewClauses1 = [rewrite_clause(C,ModInfo) || C <- Clauses1], NewClauses2 = [rewrite_clause(C,ModInfo) || C <- Clauses2], NewBody2 = [rewrite_expr(E,ModInfo) || E <- Body2], {'try',Line,NewBody1,NewClauses1,NewClauses2,NewBody2}; rewrite_expr({'receive',Line,Clauses}, ModInfo) -> NewClauses = [rewrite_clause(C,ModInfo) || C <- Clauses], {'receive',Line,NewClauses}; rewrite_expr({'receive',Line,Clauses,AfterExpr,AfterBody}, ModInfo) -> NewClauses = [rewrite_clause(C,ModInfo) || C <- Clauses], NewAfterExpr = rewrite_expr(AfterExpr, ModInfo), NewAfterBody = [rewrite_expr(E,ModInfo) || E <- AfterBody], {'receive',Line,NewClauses,NewAfterExpr,NewAfterBody}; rewrite_expr({'fun',Line,{clauses,Clauses}}, ModInfo) -> NewClauses = [rewrite_clause(C,ModInfo) || C <- Clauses], {'fun',Line,{clauses,NewClauses}}; rewrite_expr({'query',Line,ListCompr}, ModInfo) -> {'query',Line,rewrite_expr(ListCompr,ModInfo)}; rewrite_expr({record_field,Line,Expr,FieldName}, ModInfo) -> {record_field,Line,rewrite_expr(Expr,ModInfo),FieldName}; rewrite_expr(Expr, _ModInfo) -> Expr. %%------------------------------------------------------------------------------ %% Type rewriting functions %%------------------------------------------------------------------------------ -spec rewrite_type(abs_expr(), mod_info()) -> abs_expr(). rewrite_type({tuple,Line,FieldExprs}, ModInfo) -> NewFieldExprs = [rewrite_type(F,ModInfo) || F <- FieldExprs], {tuple,Line,NewFieldExprs}; rewrite_type({cons,Line,HeadExpr,TailExpr}, ModInfo) -> NewHeadExpr = rewrite_type(HeadExpr, ModInfo), NewTailExpr = rewrite_type(TailExpr, ModInfo), {cons,Line,NewHeadExpr,NewTailExpr}; rewrite_type({op,Line,'++',LeftExpr,RightExpr}, ModInfo) -> NewLeftExpr = rewrite_type(LeftExpr, ModInfo), NewRightExpr = rewrite_type(RightExpr, ModInfo), {op,Line,'++',NewLeftExpr,NewRightExpr}; rewrite_type({call,Line,{remote,_,{atom,_,Mod},{atom,_,Call}} = FunRef, Args} = Expr, #mod_info{name = ModName, helper_pid = HelperPid} = ModInfo) -> case is_exported_type(Mod, Call, length(Args), HelperPid) of true -> native_type_call(ModName, Expr); false -> NewArgs = [rewrite_type(A,ModInfo) || A <- Args], {call,Line,FunRef,NewArgs} end; rewrite_type({call,Line,{atom,_,Fun} = FunRef,Args} = Expr, #mod_info{name = ModName, funs = Funs, imports = Imports, no_autos = NoAutos} = ModInfo) -> Arity = length(Args), CallRef = {Fun,Arity}, case sets:is_element(CallRef,Funs) orelse sets:is_element(CallRef,Imports) orelse erl_internal:bif(Fun,Arity) andalso not sets:is_element(CallRef,NoAutos) of true -> NewArgs = [rewrite_type(A,ModInfo) || A <- Args], {call,Line,FunRef,NewArgs}; false -> native_type_call(ModName, Expr) end; rewrite_type(Expr, _ModInfo) -> Expr. -spec native_type_call(mod_name(), abs_expr()) -> abs_expr(). native_type_call(ModName, Expr) -> L = ?anno(0), AbsModName = {atom,L,ModName}, AbsTypeStr = {string,L,lists:flatten(erl_pp:expr(Expr))}, FunRef = {remote,L,{atom,L,proper_types},{atom,L,native_type}}, {call,L,FunRef,[AbsModName,AbsTypeStr]}.