blob: a62985dbdb4d3bb7c60be421c4f602d501fd8e66 [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(ctrace_dsl).
-include_lib("syntax_tools/include/merl.hrl").
-export([
compile/2,
% Debug
source/2
]).
-type ast() :: erl_syntax:syntaxTree().
-spec compile(OperationId :: string(), FilterDef :: string()) -> ok.
compile(OperationId, FilterDef) ->
AST = parse_filter(OperationId, FilterDef),
merl:compile_and_load(AST),
ok.
-spec source(OperationId :: string(), FilterDef :: string()) -> string().
source(OperationId, FilterDef) ->
AST = parse_filter(OperationId, FilterDef),
Options = [{paper, 160}, {ribbon, 80}],
erl_prettypr:format(erl_syntax:form_list(AST), Options).
-spec parse_filter(OperationId :: string(), FilterDef :: string()) -> [ast()].
parse_filter(OperationId, FilterDef) ->
AST = merl:quote("match" ++ FilterDef ++ "."),
case AST of
?Q("match(_@Args) when _@__@Guard -> _@Return.") when
erl_syntax:type(Args) == map_expr
->
validate_args(Args),
validate_return(Return),
generate(OperationId, Args, Guard, Return);
?Q("match(_@Args) when _@__@Guard -> _@@_.") ->
fail("The only argument of the filter should be map");
?Q("match(_@@Args) when _@__@Guard -> _@@_.") ->
fail("The arity of the filter function should be 1");
_ ->
fail("Unknown shape of a filter function")
end.
-spec validate_args(MapAST :: ast()) -> ok.
validate_args(MapAST) ->
%% Unfortunatelly merl doesn't seem to support maps
%% so we had to do it manually
lists:foldl(
fun(AST, Bindings) ->
erl_syntax:type(AST) == map_field_exact orelse
fail("Only #{field := Var} syntax is supported in the header"),
NameAST = erl_syntax:map_field_exact_name(AST),
erl_syntax:type(NameAST) == atom orelse
fail("Only atoms are supported as field names in the header"),
Name = erl_syntax:atom_value(NameAST),
VarAST = erl_syntax:map_field_exact_value(AST),
erl_syntax:type(VarAST) == variable orelse
fail("Only capitalized names are supported as matching variables in the header"),
Var = erl_syntax:variable_name(VarAST),
maps:is_key(Var, Bindings) andalso
fail("'~s' variable is already in use", [Var]),
Bindings#{Var => Name}
end,
#{},
erl_syntax:map_expr_fields(MapAST)
).
-spec validate_return(Return :: [ast()]) -> ok.
validate_return(Return) ->
case Return of
?Q("true") -> ok;
?Q("false") -> ok;
?Q("_@AST") when erl_syntax:type(AST) == float -> ok;
_ -> fail("Unsupported return value '~s'", [erl_prettypr:format(Return)])
end.
generate(OperationId, Args, Guard, Return) ->
ModuleName = ctrace_config:filter_module_name(OperationId),
Module = ?Q("-module('@ModuleName@')."),
Export = ?Q("-export([match/1])."),
Function = erl_syntax:function(merl:term(match), [
?Q("(_@Args) when _@__@Guard -> _@Return"),
?Q("(_) -> false")
]),
lists:flatten([Module, Export, Function]).
fail(Msg) ->
throw({error, Msg}).
fail(Msg, Args) ->
throw({error, lists:flatten(io_lib:format(Msg, Args))}).