Sunday 22 April 2007

Erlang Macro Processor (v2), Part V

The final step for EMP2 is to expand any remote macro function calls and insert the results back into the AST.

Naively we would just follow the same pattern as the macro attribute expansion that we have just added:

node_parse(Node={call,Line,{remote,_,{atom,_,Mod},{atom,_,Fun}},Args}, Mods) ->
case lists:member(Mod, Mods) of
true ->
ast_from_results(lists:flatten([apply(Mod,Fun,Args)|" "]), Line, []);
false -> setelement(4,Node,node_parse(Args, Mods))
end;
But if we do, we find that there are three(!) problems with this approach.

Firstly, ast_from_results is currently using erl_parse:parse_form to turn the textual macro results into an AST. This only works for complete Erlang forms (function definitions) and not for, say, a set of three Erlang expressions to be inserted into a function. We can fix this by using erl_parse:parse_exprs instead, but we will also have to append a full-stop and space to the result string (instead of just a space) to get it to work properly.

Secondly, the arguments for the function call are all in AST format with tuples and line numbers everywhere. We cannot just apply the function directly to these arguments; we need to convert them back to something more usable.

Finally, we may receive more than one Erlang expression from the macro. To fit these back into the space of a single node we have to wrap them in a block expression.


To tackle the first issue we need to update ast_from_results a little:
ast_from_results(FunParse, ResultsString, LineStart, ASTResults) ->
case remove_leading_whitespace(ResultsString) of
"" -> lists:flatten(lists:reverse(ASTResults));
String ->
{done,{ok,Tokens,LineEnd},StringRest} =
erl_scan:tokens([], String, LineStart),
{ok, AST} = erl_parse:FunParse(Tokens),
ast_from_results(FunParse, StringRest, LineEnd, [AST|ASTResults])
end.

As an aside, you might like to have a closer look at that erl_parse:FunParse call.

Yes, instead of hard-coding a function call or adding an extra if statement, we are calling the erl_parse function via a variable whose value we will not know until run-time[1]. Doesn't thinking about that just make you go all tingly inside? No? Me neither. Of course.

We can now use ast_from_results for erl_parse:parse_form and erl_parse:parse_exprs situations with only a single additional "erl_parse function" argument.


For the second issue I am going to use a cheap and nasty hack. Because we are not (yet) supporting anything fancier than literal terms in the argument list, we can get away with this little bit of trickery to convert the arguments into something usable by our call to apply:
ArgsLiteral = [Value || {_Type,_Line,Value} <- Args]. 


The third issue is also very easily fixed by wrapping the call to ast_from_results in a block expression tuple. We should only do this if there is more than one node in the results list:
node_parse(Node={call,Line,{remote,_,{atom,_,Mod},{atom,_,Fun}},Args}, Mods) ->
case lists:member(Mod, Mods) of
true ->
ArgsLiteral = [Value || {_Type,_Line,Value} <- Args],
Results = lists:flatten([apply(Mod,Fun,ArgsLiteral)|". "]),
case length(Results) of
1 -> hd(Results);
_ -> {block,Line,ast_from_results(parse_exprs, Results, Line, [])}
end;
false -> setelement(4,Node,node_parse(Args, Mods))
end;

Oh, and of course we need to update the other node_parse function clause to include the new argument to ast_from_results:
node_parse({attribute,Line,macro,{Mod,Fun,Args}}, _Mods) ->
ast_from_results(parse_form, lists:flatten([apply(Mod,Fun,Args)|" "]), Line, []);


And with any luck we are done. Let's try it out on our example code.

1> CL = fun(F) -> c(F), l(F) end.
#Fun
2> CL(emp2), CL(example_macro), CL(example).
{module, example}
3> [example:lookup(N) || N <- lists:seq(0, 3)]. [0,2,4,6] 4>

Yep. EMP2 is done.


The full listing:

-module(emp2).
-author("Philip Robinson").
-vsn('1.0').
-export([parse_transform/2]).

parse_transform(AST, _Options) ->
Mods = lists:flatten([Mods || {attribute,_Line,macro_modules,Mods} <- AST]),
lists:flatten([node_parse(Node, Mods) || Node <- AST]).
node_parse({attribute,Line,macro,{Mod,Fun,Args}}, _Mods) ->
ast_from_results(parse_form, lists:flatten([apply(Mod,Fun,Args)|" "]), Line, []);
node_parse(Node={call,Line,{remote,_,{atom,_,Mod},{atom,_,Fun}},Args}, Mods) ->
case lists:member(Mod, Mods) of
true ->
ArgsLiteral = [Value || {_Type,_Line,Value} <- Args],
Results = lists:flatten([apply(Mod,Fun,ArgsLiteral)|". "]),
case length(Results) of
1 -> hd(Results);
_ -> {block,Line,ast_from_results(parse_exprs,Results,Line,[])}
end;
false -> setelement(4,Node,node_parse(Args, Mods))
end;
node_parse(Node, Mods) when is_list(Node) ->
[node_parse(Element, Mods) || Element <- Node];
node_parse(Node, Mods) when is_tuple(Node) ->
list_to_tuple([node_parse(Element, Mods) || Element <- tuple_to_list(Node)]);
node_parse(Node, _Mods) -> Node.

args_from_ast(AST) -> [Value || {_Type,_Line,Value} <- AST].

ast_from_results(FunParse, ResultsString, LineStart, ASTResults) ->
case remove_leading_whitespace(ResultsString) of
"" -> lists:flatten(lists:reverse(ASTResults));
String ->
{done,{ok,Tokens,LineEnd},StringRest} =
erl_scan:tokens([], String, LineStart),
{ok, AST} = erl_parse:FunParse(Tokens),
ast_from_results(FunParse, StringRest, LineEnd, [AST|ASTResults])
end.

remove_leading_whitespace([9 |String]) -> remove_leading_whitespace(String);
remove_leading_whitespace([10|String]) -> remove_leading_whitespace(String);
remove_leading_whitespace([32|String]) -> remove_leading_whitespace(String);
remove_leading_whitespace( String ) -> String.

EMP2: Entirely painful compile-time macros for functions and expressions, in 45 lines of obscure, uncommented, and unreadable Erlang code.
(If you think that this code is bad, wait until you see EMP3.)


[1] Run-time for EMP2 is, of course, compile-time for the module that we are using EMP2 to transform.

1 comment:

  1. Excellent job!

    I've put this on GitHub for the general public to ponder on ;c)

    ReplyDelete

Obligatory legal stuff

Unless otherwise noted, all code appearing on this blog is released into the public domain and provided "as-is", without any warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the author(s) be liable for any claim, damages, or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software.