%% Copyright (c) 2013-2020 Robert Virding %% %% 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. %% File : lfe_macro_include.erl %% Author : Robert Virding %% Purpose : Lisp Flavoured Erlang macro expander for include macros. %% Expand the (include-file ...) and (include-lib ...) macros handling %% if they are LFE syntax files or erlang syntax files. Erlang syntax %% files are ones which end in .hrl. We only handle basic record and %% macro definitions. -module(lfe_macro_include). -export([file/3,lib/3,format_error/1,stringify/1]). -export([read_hrl_file_1/1,read_hrl_file/2]). %For testing %%-compile([export_all]). -include("lfe.hrl"). -include("lfe_macro.hrl"). %% Test function to inspect output of parsing functions. read_hrl_file_1(Name) -> case epp:open(Name, []) of {ok,Epp} -> %% These are two undocumented functions of epp. Fs = epp:parse_file(Epp), Ms = epp:macro_defs(Epp), epp:close(Epp), %Now we close epp {ok,Fs,Ms}; {error,E} -> {error,E} end. %% Errors. format_error({bad_form,Type}) -> lfe_io:format1(<<"bad ~w form">>, [Type]); format_error({no_include,T,F}) -> io_lib:format(<<"can't find include ~w ~ts">>, [T,F]); format_error({notrans_function,F,A}) -> lfe_io:format1(<<"unable to translate function ~w/~w">>, [F,A]); format_error({notrans_record,R}) -> lfe_io:format1(<<"unable to translate record ~w">>, [R]); format_error({notrans_type,T}) -> lfe_io:format1(<<"unable to translate type ~w">>, [T]); format_error({notrans_macro,M}) -> lfe_io:format1(<<"unable to translate macro ~w">>, [M]); %% File errors are passed on. format_error({file_error,E}) -> file:format_error(E). %% add_error(Error, State) -> State. %% add_error(Line, Error, State) -> State. %% add_warning(Warning, State) -> State. %% add_warning(Line, Warning, State) -> State. %% Add errors and warnings to the state add_error(E, St) -> add_error(St#mac.line, E, St). add_error(L, E, St) -> St#mac{errors=St#mac.errors ++ [{L,?MODULE,E}]}. add_warning(W, St) -> add_warning(St#mac.line, W, St). add_warning(L, W, St) -> St#mac{warnings=St#mac.warnings ++ [{L,?MODULE,W}]}. %% file(FileName, Env, MacState) -> %% {yes,(progn ...),MacState} | {error,MacState}. %% Expand the (include-file ...) macro. This is a VERY simple %% include file macro! We just signal errors. file(IncFile, _, #mac{ipath=Path}=St0) -> case include_name(IncFile) of {ok,Name} -> case path_read_file(Path, Name, St0) of {ok,Forms,St1} -> {yes,['progn'|Forms],St1}; {error,St1} -> {error,St1}; not_found -> {error,add_error({no_include,file,Name}, St0)} end; {error,_} -> {error,add_error({bad_form,'include-file'}, St0)} end. %% lib(FileName, Env, MacState) -> %% {yes,(progn ...),MacState} | {error,MacState}. %% Expand the (include-lib ...) macro. We do the same as epp so we %% first test if we can find the file through the normal search path, %% if not we assume that the first directory name is a library name, %% find its true directory and try with that. lib(IncFile, _, #mac{ipath=Path}=St0) -> case include_name(IncFile) of {ok,Name} -> case path_read_file(Path, Name, St0) of {ok,Forms,St1} -> {yes,['progn'|Forms],St1}; {error,St1} -> {error,St1}; not_found -> case lib_read_file(Name, St0) of {ok,Forms,St1} -> {yes,['progn'|Forms],St1}; {error,St1} -> {error,St1}; not_found -> {error,add_error({no_include,lib,Name}, St0)} end end; {error,_} -> {error,add_error({bad_form,'include-lib'}, St0)} end. %% include_name(FileName) -> bool(). %% Gets the file name from the include-XXX FileName. include_name(Name) -> try {ok,lists:flatten(unicode:characters_to_list(Name, utf8))} catch _:_ -> {error,badarg} end. %% path_read_file(Path, Name, State) -> %% {ok,Forms,State} | {error,State} | not_found. %% Step down the path trying to read the file. path_read_file(Path, Name, St) -> case file:path_open(Path, Name, [read,raw]) of {ok,F,Pname} -> file:close(F), %Close it again read_file(Pname, St); %Read it {error,_} -> not_found %Not found end. %% lib_file_name(LibPath) -> {ok,LibFileName} | {error,Error}. %% Construct path to true library file. lib_file_name(Name) -> try [App|Path] = filename:split(Name), LibDir = code:lib_dir(list_to_atom(App)), {ok,filename_join([LibDir|Path])} catch _:_ -> error end. filename_join(["." | [_|_]=Rest]) -> filename_join(Rest); filename_join(Comp) -> filename:join(Comp). %% lib_read_file(FileName, State) -> %% {ok,Forms,State} | {error,State} | not_found. %% Try to read the library file. Try to open the file to make sure %% that even if we can find the lirbary the file is there. lib_read_file(Name, St) -> case lib_file_name(Name) of {ok,LibName} -> case file:open(LibName, [read,raw]) of {ok,F} -> file:close(F), read_file(LibName, St); {error,_} -> not_found end; error -> not_found end. %% read_file(FileName, State) -> {ok,Forms,State} | {error,State}. read_file(Name, St) -> case lists:suffix(".hrl", Name) of true -> read_hrl_file(Name, St); %Read file as .hrl file false -> read_lfe_file(Name, St) end. %% read_lfe_file(FileName, State) -> {ok,Forms,State} | {error,State}. read_lfe_file(Name, #mac{errors=Es}=St) -> %% Read the file as an LFE file. case lfe_io:read_file(Name) of {ok,Fs} -> {ok,Fs,St}; {error,E} -> {error,St#mac{errors=Es ++ [E]}} end. %% read_hrl_file(FileName, State) -> {ok,Forms,State} | {error,Error}. %% We use two undocumented functions of epp which allow us to get %% inside and get out the macros but it must be called after the %% whole file has been processed. read_hrl_file(Name, St) -> case epp:open(Name, []) of {ok,Epp} -> Fs = epp:parse_file(Epp), %This must be called first Ms = epp:macro_defs(Epp), % then this undocumented! epp:close(Epp), %Now we close epp parse_hrl_file(Fs, Ms, St); {error,E} -> {error,add_error({file_error,E}, St)} end. %% parse_hrl_file(Forms, Macros, State) -> {ok,Forms,State} | {error,State}. %% All the attributes go in an extend-module form. In 18 and older a %% typed record definition would result in 2 attributes, the bare %% record def and the record type def. We want just the record type %% def. -ifdef(NEW_REC_CORE). parse_hrl_file(Fs, Ms, St0) -> {As,Lfs,St1} = trans_forms(Fs, St0), %% io:format("~p\n",[Ms]), {Lms,St2} = trans_macros(Ms, St1), {ok,[['extend-module',[],As]] ++ Lfs ++ Lms,St2}. -else. parse_hrl_file(Fs0, Ms, St0) -> %% Trim away untyped record attribute when there is a typed record %% attribute as well. Trs = typed_record_attrs(Fs0), Fs1 = delete_typed_record_defs(Fs0, Trs), {As,Lfs,St1} = trans_forms(Fs1, St0), {Lms,St2} = trans_macros(Ms, St1), {ok,[['extend-module',[],As]] ++ Lfs ++ Lms,St2}. typed_record_attrs(Fs) -> [ Name || {attribute,_,type,{{record,Name},_,_}} <- Fs ]. delete_typed_record_defs(Fs, Trs) -> Dfun = fun ({attribute,_,record,{Name,_}}) -> not lists:member(Name, Trs); (_) -> true end, lists:filter(Dfun, Fs). -endif. %% trans_forms(Forms, State) -> {Attributes,LForms,State}. %% Translate the record and function defintions and attributes in the %% forms to LFE record and function definitions and %% attributes. trans_forms(Fs, St0) -> Tfun = fun (F, {As,Lfs,St}) -> trans_form(F, As, Lfs, St) end, {As,Lfs,St1} = lists:foldl(Tfun, {[],[],St0}, Fs), {lists:reverse(As),lists:reverse(Lfs),St1}. %% trans_form(Form, Attributes, LispForms, State) -> %% {Attributes,LispForms,State}. %% Note that the Attributes and LispForms are the ones that have %% preceded this form, but in reverse order. trans_form({attribute,Line,record,{Name,Fields}}, As, Lfs, St) -> case catch {ok,trans_record(Name, Line, Fields)} of {ok,Lrec} -> {As,[Lrec|Lfs],St}; {'EXIT',_E}-> %Something went wrong {As,Lfs,add_warning({notrans_record,Name}, St)} end; trans_form({attribute,Line,type,{Name,Def,E}}, As, Lfs, St) -> case catch {ok,trans_type(Name, Line, Def, E)} of {ok,Ltype} -> {As,[Ltype|Lfs],St}; {'EXIT',_E} -> %Something went wrong exit({boom,_E,{Name,Line,Def,E}}), {As,Lfs,add_warning({notrans_type,Name}, St)} end; trans_form({attribute,Line,opaque,{Name,Def,E}}, As, Lfs, St) -> case catch {ok,trans_opaque(Name, Line, Def, E)} of {ok,Ltype} -> {As,[Ltype|Lfs],St}; {'EXIT',_E} -> %Something went wrong exit({boom,_E,{Name,Line,Def,E}}), {As,Lfs,add_warning({notrans_type,Name}, St)} end; trans_form({attribute,Line,spec,{Func,Types}}, As, Lfs, St) -> case catch {ok,trans_spec(Func, Line, Types)} of {ok,Lspec} -> {As,[['define-function-spec'|Lspec]|Lfs],St}; {'EXIT',_E} -> %Something went wrong exit({boom,_E,{Func,Line,Types}}), {As,Lfs,add_warning({notrans_spec,Func}, St)} end; trans_form({attribute,_,export,Es}, As, Lfs, St) -> Les = trans_farity(Es), {[[export|Les]|As],Lfs,St}; trans_form({attribute,_,import,{Mod,Es}}, As, Lfs, St) -> Les = trans_farity(Es), {[[import,[from,Mod|Les]]|As],Lfs,St}; trans_form({attribute,_,Name,E}, As, Lfs, St) -> {[[Name,E]|As],Lfs,St}; trans_form({function,_,Name,Arity,Cls}, As, Lfs, St) -> case catch {ok,trans_function(Name, Arity, Cls)} of {ok,Lfunc} -> {As,[Lfunc|Lfs],St}; {'EXIT',_E} -> %Something went wrong {As,Lfs,add_warning({notrans_function,Name,Arity}, St)} end; trans_form({error,E}, As, Lfs, #mac{errors=Es}=St) -> %% Assume the error is in the right format, {Line,Mod,Err}. {As,Lfs,St#mac{errors=Es ++ [E]}}; trans_form(_, As, Lfs, St) -> %Ignore everything else {As,Lfs,St}. trans_farity(Es) -> lists:map(fun ({F,A}) -> [F,A] end, Es). %% trans_record(Name, Line, Fields) -> LRecDef. %% Translate an Erlang record definition to LFE. We currently ignore %% any type information. trans_record(Name, _, Fs) -> Lfs = record_fields(Fs), [defrecord,Name|Lfs]. record_fields(Fs) -> [ record_field(F) || F <- Fs ]. record_field({record_field,_,F}) -> %Just the field name lfe_translate:from_lit(F); record_field({record_field,_,F,Def}) -> %Field name and default value Fd = lfe_translate:from_lit(F), Ld = lfe_translate:from_expr(Def), [Fd,Ld]; record_field({typed_record_field,Rf,Type}) -> typed_record_field(Rf, Type). typed_record_field({record_field,_,F}, Type) -> %% Just the field name, set default value to 'undefined. Fd = lfe_translate:from_lit(F), Td = lfe_types:from_type_def(Type), [Fd,?Q(undefined),Td]; typed_record_field({record_field,_,F,Def}, Type) -> Fd = lfe_translate:from_lit(F), Ld = lfe_translate:from_expr(Def), Td = lfe_types:from_type_def(Type), [Fd,Ld,Td]. %% trans_type(Name, Line, Definition, Extra) -> TypeDef. %% Translate an Erlang type definition to LFE. In 18 and older this %% could also contain a typed record definition which we use. -ifdef(NEW_REC_CORE). trans_type(Name, _Line, Def, E) -> ['define-type',[Name|lfe_types:from_type_defs(E)], lfe_types:from_type_def(Def)]. -else. trans_type({record,Name}, Line, Def, _E) -> trans_record(Name, Line, Def); trans_type(Name, _Line, Def, E) -> ['define-type',[Name|lfe_types:from_type_defs(E)], lfe_types:from_type_def(Def)]. -endif. %% trans_opaque(Name, Line, Definition, Extra) -> TypeDef. %% Translate an Erlang opaque type definition to LFE. trans_opaque(Name, _, Def, E) -> ['define-opaque-type',[Name|lfe_types:from_type_defs(E)], lfe_types:from_type_def(Def)]. %%[type,{Name,convert_type(Def, Line),E}]. %% trans_spec(FuncArity, Line, TypeList) -> SpecDef. trans_spec({Name,Arity}, _, Tl) -> [[Name,Arity],lfe_types:from_func_spec_list(Tl)]. %% trans_function(Name, Arity, Clauses) -> LfuncDef. trans_function(Name, _, Cls) -> %% Make it a fun and then drop the match-lambda. ['match-lambda'|Lcs] = lfe_translate:from_expr({'fun',0,{clauses,Cls}}), [defun,Name|Lcs]. %% trans_macros(MacroDefs, State) -> {LMacroDefs,State}. %% Translate macro definitions to LFE macro definitions. Ignore %% undefined and predefined macros. trans_macros([{{atom,Mac},Defs}|Ms], St0) -> {Lms,St1} = trans_macros(Ms, St0), case catch trans_macro(Mac, Defs, St1) of {'EXIT',_E} -> %Something went wrong {Lms,add_warning({notrans_macro,Mac}, St1)}; {none,St2} -> {Lms,St2}; %No definition, ignore {Mdef,St2} -> {[Mdef|Lms],St2} end; trans_macros([], St) -> {[],St}. trans_macro(_, undefined, St) -> {none,St}; %Undefined macros trans_macro(_, {none,_}, St) -> {none,St}; %Predefined macros trans_macro(Mac, Defs0, St) -> Defs1 = order_macro_defs(Defs0), case trans_macro_defs(Defs1) of [] -> {none,St}; %No definitions Lcls -> {[defmacro,Mac|Lcls],St} end. order_macro_defs([{none,Ds}|Defs]) -> %Put the no arg version last Defs ++ [{none,Ds}]; order_macro_defs(Defs) -> Defs. %% trans_macro_defs(MacroDef) -> [] | [Clause]. %% Translate macro definition to a list of clauses. Put the no arg %% version last as a catch all. Clash if macro has no arg definition %% *and* function definition with no args: %% -define(foo, 42). %% -define(foo(), 17). %% %% NOTE: Don't yet generate code to macros with *only* no arg case to %% be used as functions. So -define(foo, bar) won't work for foo(42). trans_macro_defs([{none,{none,Ts}}|Defs]) -> Ld = trans_macro_body([], Ts), AnyArgs = ['_'|Ld], [AnyArgs|trans_macro_defs(Defs)]; trans_macro_defs([{N,{As,Ts}}|Defs]) when is_integer(N) -> Ld = trans_macro_body(As, Ts), ListArgs = [[list|As]|Ld], [ListArgs|trans_macro_defs(Defs)]; trans_macro_defs([]) -> []. trans_macro_body([], Ts0) -> Ts1 = trans_qm(Ts0), %% io:format("parse: ~p\n",[Ts1 ++ [{dot,0}]]), {ok,[E]} = erl_parse:parse_exprs(Ts1 ++ [{dot,0}]), %% io:format("result: ~p\n",[E]), [?BQ(lfe_translate:from_expr(E))]; trans_macro_body(As, Ts0) -> Ts1 = trans_qm(Ts0), {ok,[E]} = erl_parse:parse_exprs(Ts1 ++ [{dot,0}]), Le0 = lfe_translate:from_expr(E), %% Wrap variables in arg list with an (comma ...) call. Alist = [ [A|[comma,A]] || A <- As ], Le1 = lfe:sublis(Alist, Le0), %% Le1 = unquote_vars(Alist, Le0), [?BQ(Le1)]. %% {ok,[_]=F} = erl_parse:parse_exprs(Ts1 ++ [{dot,0}]), %% backquote_last(lfe_translate:from_body(F)). %% unquote_vars(Alist, Expr) -> Expr. %% Special version of sublis which doesn't enter quotes. Specially %% made for traversing code and unquote-ing vars. %% unquote_vars(_, ?Q(_)=E) -> E; %% unquote_vars(Alist, E) -> %% case lfe:assoc(E, Alist) of %% [_|New] -> New; %Found it %% [] -> %Not there %% case E of %% [H|T] -> %% [unquote_vars(Alist, H)|unquote_vars(Alist, T)]; %% _ -> E %% end %% end. %% Backquote the last expression in the body. %% backquote_last([E]) -> [?BQ(E)]; %% backquote_last([E|Es]) -> [E|backquote_last(Es)]. %% trans_qm(Tokens) -> Tokens. %% Translate variable argument names to atoms to get correct %% translation to LFE later on: ?Sune -> ?'Sune' -> (Sune) %% Translate ?FOO( ==> FOO( trans_qm([{'?',_},{atom,_,_}=A,{'(',_}=Lp|Ts]) -> [A,Lp|trans_qm(Ts)]; trans_qm([{'?',_},{var,L,V},{'(',_}=Lp|Ts]) -> [{atom,L,V},Lp|trans_qm(Ts)]; %% Translate ?FOO:bar ==> (FOO()):bar. trans_qm([{'?',L},{atom,_,_}=A,{':',_}=C|Ts]) -> Lp = {'(',L}, Rp = {')',L}, [Lp,A,Lp,Rp,Rp,C|trans_qm(Ts)]; trans_qm([{'?',L},{var,_,V},{':',_}=C|Ts]) -> Lp = {'(',L}, Rp = {')',L}, [Lp,{atom,L,V},Lp,Rp,Rp,C|trans_qm(Ts)]; %% Translate ?FOO ==> FOO(). trans_qm([{'?',L},{atom,_,_}=A|Ts]) -> [A,{'(',L},{')',L}|trans_qm(Ts)]; trans_qm([{'?',L},{var,_,V}|Ts]) -> [{atom,L,V},{'(',L},{')',L}|trans_qm(Ts)]; %% Translate ??FOO ==> lfe_macro_include:stringify(quote(FOO)) trans_qm([{'?',L},{'?',_},Arg|Ts]) -> Lp = {'(',L}, Rp = {')',L}, [{atom,L,?MODULE},{':',L},{atom,L,stringify},Lp, {atom,L,quote},Lp,Arg,Rp,Rp| trans_qm(Ts)]; trans_qm([T|Ts]) -> [T|trans_qm(Ts)]; trans_qm([]) -> []. %%% stringify(Sexpr) -> String. %% Returns a list of sexpr, a string which when parse would return %% the sexpr. stringify(E) -> lists:flatten(lfe_io:print1(E)).