diff --git a/src/compile/utCompileStr.erl b/src/compile/utCompileStr.erl new file mode 100644 index 0000000..15eadb0 --- /dev/null +++ b/src/compile/utCompileStr.erl @@ -0,0 +1,455 @@ +-module(utCompileStr). + +-compile([nowarn_unused_function]). + +-export([ + loadStr/1 + , loadStr/2 + , formsStr/1 + , formsStr/2 +]). + +-type option() :: atom() | {atom(), term()} | {'d', atom(), term()}. + +-type abstract_code() :: [erl_parse:abstract_form()]. +-type error_info() :: erl_lint:error_info(). +-type errors() :: [{file:filename(), [error_info()]}]. +-type warnings() :: [{file:filename(), [error_info()]}]. +-type mod_ret() :: + {'ok', module()}| + {'ok', module(), cerl:c_module()} | %% with option 'to_core' + {'ok', module() | [], abstract_code()} | %% with option 'to_pp' %% module() if 'to_exp' + {'ok', module(), warnings()}. +-type bin_ret() :: + {'ok', module(), binary()}| + {'ok', module(), binary(), warnings()}. +-type err_ret() :: + 'error' | + {'error', errors(), warnings()}. +-type comp_ret() :: + mod_ret() | + bin_ret() | + err_ret(). + +-import(lists, [reverse/1, keyreplace/4]). + +%% 从字符串编译并加载 +-spec loadStr(CodeStr :: string()) -> {module, Module :: module()} | {error, What :: term()}. +loadStr(CodeStr) -> + loadStr(CodeStr, []). + +%% 从字符串编译并加载 +-spec loadStr(CodeStr :: string(), CompileFormsOpts :: [option()] | option()) -> {module, Module :: module()} | {error, What :: term()}. +loadStr(CodeStr, CompileFormsOpts) -> + {Mod, Bin} = formsStr(CodeStr, CompileFormsOpts), + code:load_binary(Mod, [], Bin). + +%% 从字符串编译 +-spec formsStr(CodeStr :: string()) -> comp_ret(). +formsStr(CodeStr) -> + formsStr(CodeStr, []). + +%% 从字符串编译 +-spec formsStr(CodeStr :: string(), CompileFormsOpts :: [option()] | option()) -> comp_ret(). +formsStr(CodeStr, CompileFormsOpts) -> + Filename = "compiled_from_string", + + %% 根据 compile:forms 的文档:当遇到 -include 或 -include_dir 指令时,编译器会在以下目录中搜索头文件: + %% 1. ".",文件服务器的当前工作目录; + %% 2. 编译文件的基名; + %% 3. 使用编译选项 'i' 选项指定的目录。最后指定的目录首先被搜索。在这种情况下,2 没有意义。 + + IncludePath = ["." | reverse([Dir || {i, Dir} <- CompileFormsOpts])], + case scanAndParse(CodeStr, Filename, 1, [], [], #{}, IncludePath) of + {ok, RevForms, _OutMacroDict} -> + Forms = [{attribute, 0, file, {"compiled_from_string", 0}} | reverse([{eof, 0} | RevForms])], + + %% note: 'binary' is forced as an implicit option, whether it is provided or not. + case compile:forms(Forms, CompileFormsOpts) of + {ok, _ModName, CompiledCodeBinary} = Ret when is_binary(CompiledCodeBinary) -> + Ret; + {ok, _ModName, _CompiledCodeBinary, _Warnings} = Ret -> + Ret; + {error, [{_, Errors}], Warnings} -> + {error, Errors, Warnings}; + Other -> + {error, Other, []} + end; + {error, Errors} -> + {error, Errors, []} + end. + + +scanAndParse([], _CurrFilename, _CurrLine, RevForms, Errors, MacroMap, _IncludePath) -> + case Errors of + [] -> + {ok, RevForms, MacroMap}; + _ -> + {error, lists:flatten(lists:reverse(Errors))} + end; +scanAndParse(RemainingText, CurrFilename, CurrLine, RevForms, Errors, MacroMap, IncludeSearchPath) -> + case scanner(RemainingText, CurrLine, MacroMap) of + {tokens, NLine, NRemainingText, Toks} -> + case erl_parse:parse_form(Toks) of + {ok, Form0} -> + {Form, Forms} = normalizeRecord(Form0), + + scanAndParse(NRemainingText, CurrFilename, NLine, [Form | Forms ++ RevForms], Errors, MacroMap, IncludeSearchPath); + {error, E} -> + scanAndParse(NRemainingText, CurrFilename, NLine, RevForms, [E | Errors], MacroMap, IncludeSearchPath) + end; + {macro, NLine, NRemainingText, NMacroDict} -> + scanAndParse(NRemainingText, CurrFilename, NLine, RevForms, Errors, NMacroDict, IncludeSearchPath); + {Def, NLine, NRemainingText, NMacroDict} when Def =:= def; Def =:= endif; Def =:= else;Def =:= undef; Def =:= attribute; Def =:= vsn -> + scanAndParse(NRemainingText, CurrFilename, NLine, RevForms, Errors, NMacroDict, IncludeSearchPath); + {vsn, NLine, NRemainingText, NMacroDict} -> + scanAndParse(NRemainingText, CurrFilename, NLine, RevForms, Errors, NMacroDict, IncludeSearchPath); + {Include, NLine, NRemainingText, IncludeFilename} when Include =:= include; Include =:= include_lib -> + {IncludeCurrentFile, IncludeFileRemainingTextents} = + case Include of + include -> + readIncludeFile(IncludeFilename, CurrFilename); + _ -> + readIncludeLibFile(IncludeFilename, CurrFilename) + end, + + %%io:format("include file ~p contents: ~n~p~nRemainingText = ~p~n", [IncludeFilename,IncludeFileRemainingTextents, RemainingText]), + %% Modify the FILE macro to reflect the filename + %%IncludeMacroDict = dict:store('FILE', {[],IncludeFilename}, MacroMap), + IncludeMacroDict = MacroMap, + + %% Process the header file (inc. any nested header files) + {ok, RevIncludeForms, IncludedMacroDict} = scanAndParse(IncludeFileRemainingTextents, IncludeCurrentFile, 1, [], Errors, IncludeMacroDict, IncludeSearchPath), + %io:format("include file results = ~p~n", [R]), + %% Restore the FILE macro in the NEW MacroMap (so we keep any macros defined in the header file) + %%NMacroDict = dict:store('FILE', {[],CurrFilename}, IncludedMacroDict), + NMacroDict = IncludedMacroDict, + %% Continue with the original file + scanAndParse(NRemainingText, CurrFilename, NLine, RevIncludeForms ++ RevForms, Errors, NMacroDict, IncludeSearchPath); + {continue, Continuation} -> + scanAndParse([], CurrFilename, CurrLine, [Continuation | RevForms], Errors, MacroMap, IncludeSearchPath); + done -> + scanAndParse([], CurrFilename, CurrLine, RevForms, Errors, MacroMap, IncludeSearchPath) + end. + +scanner(Text, Line, MacroMap) -> + case erl_scan:tokens([], Text, Line) of + {done, {ok, Tokens, NLine}, LeftOverChars} -> + case preProc(Tokens, MacroMap) of + {tokens, NToks} -> + {tokens, NLine, LeftOverChars, NToks}; + {macro, NMacroDict} -> + {macro, NLine, LeftOverChars, NMacroDict}; + {include, Filename} -> + {include, NLine, LeftOverChars, Filename}; + {include_lib, Filename} -> + {include_lib, NLine, LeftOverChars, Filename}; + Def when Def =:= def; Def =:= endif; Def =:= else; Def =:= undef; Def =:= attribute; Def =:= vsn -> + {Def, NLine, LeftOverChars, MacroMap} + end; + {more, {erl_scan_continuation, _, _, Tokens, NLine, _, Any, _} = _Continuation} -> + + %% 这应该表示“术语尚未完成”(即“。”尚未达到 %%)。 + %% 但是,由于某些奇怪的原因,如果在最后的 '.' 之后有注释,我们也会得到这个。在一个文件中。 + %% 所以我们检查 Text 是否只包含评论。 + case isOnlyComments(Text) of + true -> + done; + {false, _} -> + Header = + case string:to_integer(Any) of + {Int, []} -> + [{dot, NLine}, {integer, NLine, Int}]; + _ when is_list(Any), Any =/= [] -> + [{dot, NLine}, {string, NLine, Any}]; + _ -> + [{dot, NLine}] + end, + + case preProc(lists:reverse(lists:concat([Header, Tokens])), MacroMap) of + {tokens, NToks} -> {tokens, NLine, [], NToks}; + {macro, NMacroDict} -> {macro, NLine, [], NMacroDict}; + {include, Filename} -> {include, NLine, [], Filename}; + {include_lib, Filename} -> {include_lib, NLine, [], Filename}; + Def when Def =:= def; Def =:= endif; Def =:= else; Def =:= undef; Def =:= attribute; Def =:= vsn -> + {Def, NLine, [], MacroMap} + + end + end + end. + +is_endif(Text) -> + is_endif(Text, not_in_endif). + +is_endif([], _) -> false; +is_endif([$ | T], not_in_endif) -> + is_endif(T, not_in_endif); +is_endif([$\t | T], not_in_endif) -> + is_endif(T, not_in_endif); +is_endif([$\n | T], not_in_endif) -> + is_endif(T, not_in_endif); +is_endif("-endif.", not_in_endif) -> + true; +is_endif(_, not_in_endif) -> false. + + +isOnlyComments(Text) -> + isOnlyComments(Text, not_in_comment). + +isOnlyComments([], _) -> + true; +isOnlyComments([$ | T], not_in_comment) -> + isOnlyComments(T, not_in_comment); % skipping whitspace outside of comment +isOnlyComments([$\t | T], not_in_comment) -> + isOnlyComments(T, not_in_comment); % skipping whitspace outside of comment +isOnlyComments([$\n | T], not_in_comment) -> + isOnlyComments(T, not_in_comment); % skipping whitspace outside of comment +isOnlyComments([$\r | T], not_in_comment) -> + isOnlyComments(T, not_in_comment); +isOnlyComments([$% | T], not_in_comment) -> + isOnlyComments(T, in_comment); % found start of a comment +isOnlyComments(Text, not_in_comment) -> + {false, Text}; +% found any significant char NOT in a comment +isOnlyComments([$\n | T], in_comment) -> + isOnlyComments(T, not_in_comment); % found end of a comment +isOnlyComments([_ | T], in_comment) -> + isOnlyComments(T, in_comment). % skipping over in-comment chars + +%% have to implement a subset of the pre-processor, since epp insists on running on a file. +%% only handles 2 cases; +%% -define(MACRO, something). +%% -define(MACRO(VAR1,VARN),{stuff,VAR1,more,stuff,VARN,extra,stuff}). +preProc([{'-', _}, {atom, _, define}, {'(', _}, {_, _, Name} | DefTokens], MacroMap) -> + case DefTokens of + [{',', _} | Macro] -> + {macro, maps:put(Name, {[], macroBodyDef(Macro, [], [])}, MacroMap)}; + [{'(', _} | Macro] -> + {macro, maps:put(Name, macroParamsBodyDef(Macro, []), MacroMap)} + end; +preProc([{'-', _}, {atom, _, vsn}, {'(', _}, _, {')', _}, {dot, _}], _MacroMap) -> + vsn; +preProc([{'-', _}, {atom, _, include}, {'(', _}, {string, _, Filename}, {')', _}, {dot, _}], _MacroMap) -> + {include, Filename}; +preProc([{'-', _}, {atom, _, include_lib}, {'(', _}, {string, _, Filename}, {')', _}, {dot, _}], _MacroMap) -> + {include_lib, Filename}; +preProc([{'-', _}, {atom, _, ifndef}, {'(', _}, _, {')', _}, {dot, _}], _MacroMap) -> + def; +preProc([{'-', _}, {atom, _, ifdef}, {'(', _}, _, {')', _}, {dot, _}], _MacroMap) -> + def; +preProc([{'-', _}, {atom, _, endif}, {dot, _}], _MacroMap) -> + endif; +preProc([{'-', _}, {atom, _, else}, {dot, _}], _MacroMap) -> + else; +preProc([{'-', _}, {atom, _, undef}, {'(', _}, _, {')', _}, {dot, _}], _MacroMap) -> + undef; +preProc([{'-', _}, {atom, _, author}, {'(', _}, {'{', _}, {_, _, _}, {',', _}, {_, _, _}, {'}', _}, {')', _}, {dot, _}], _MacroMap) -> + attribute; +preProc([{'-', _}, {atom, _, author}, {'(', _}, _, {')', _}, {dot, _}], _MacroMap) -> + attribute; +preProc([{'-', _}, {atom, _, copyright}, {'(', _}, {'{', _}, _, {',', _}, _, {'}', _}, {')', _}, {dot, _}], _MacroMap) -> + attribute; +preProc([{'-', _}, {atom, _, copyright}, {'(', _}, _, {')', _}, {dot, _}], _MacroMap) -> + attribute; +preProc([{'-', _}, {atom, _, description}, {'(', _}, _, {')', _}, {dot, _}], _MacroMap) -> + attribute; +preProc([{'-', _}, {atom, _, module}, {'(', _}, _, {')', _}, {dot, _}], _MacroMap) -> + attribute; +preProc(Tokens, MacroMap) -> + {tokens, substMacros(Tokens, MacroMap)}. + +macroParamsBodyDef([{')', _}, {',', _} | Tokens], RevParams) -> + {reverse(RevParams), macroBodyDef(Tokens, [], [])}; +macroParamsBodyDef([{var, _, Param} | Tokens], RevParams) -> + macroParamsBodyDef(Tokens, [Param | RevParams]); +macroParamsBodyDef([{',', _}, {var, _, Param} | Tokens], RevParams) -> + macroParamsBodyDef(Tokens, [Param | RevParams]). + +macroBodyDef([{')', _}, {dot, _}], _, RevMacroBodyTokens) -> + reverse(RevMacroBodyTokens); +macroBodyDef([{')', Line} | Tokens], LeftList, RevMacroBodyTokens) -> + macroBodyDef(Tokens, lists:delete('(', LeftList), [{')', Line} | RevMacroBodyTokens]); +macroBodyDef([{'(', Line} | Tokens], [], RevMacroBodyTokens) -> + macroBodyDef(Tokens, ['('], [{'(', Line} | RevMacroBodyTokens]); +macroBodyDef([{'(', Line} | Tokens], LeftList, RevMacroBodyTokens) -> + macroBodyDef(Tokens, ['(' | LeftList], [{'(', Line} | RevMacroBodyTokens]); +macroBodyDef([Tok | Tokens], LeftList, RevMacroBodyTokens) -> + macroBodyDef(Tokens, LeftList, [Tok | RevMacroBodyTokens]). + +substMacros(Tokens, MacroMap) -> + reverse(substMacrosRev(Tokens, MacroMap, [])). + +%% returns a reversed list of tokes +substMacrosRev([{'?', _}, {_, LineNum, 'MODULE'} | Tokens], MacroMap, RevOutTokens) -> + {[], MacroValue} = maps:get('MODULE', MacroMap), + substMacrosRev(Tokens, MacroMap, [{atom, LineNum, MacroValue}] ++ RevOutTokens); +substMacrosRev([{'?', _}, {_, LineNum, 'FILE'} | Tokens], MacroMap, RevOutTokens) -> + {[], MacroValue} = maps:get('FILE', MacroMap), + substMacrosRev(Tokens, MacroMap, [{string, LineNum, MacroValue}] ++ RevOutTokens); +substMacrosRev([{'?', _}, {_, LineNum, 'LINE'} | Tokens], MacroMap, RevOutTokens) -> + %% special-case for ?LINE, to avoid creating a new MacroMap for every line in the source file + substMacrosRev(Tokens, MacroMap, [{integer, LineNum, LineNum}] ++ RevOutTokens); +substMacrosRev([{'?', _}, {_, _, Name}, {'(', _} = Paren | Tokens], MacroMap, RevOutTokens) -> + case maps:is_key(Name, MacroMap) of + true -> + case maps:get(Name, MacroMap) of + {[], MacroValue} -> + %% 这个宏没有任何变量,所以忽略调用后面跟着 "(...stuff" %% 在这个宏的值内递归展开任何宏调用 + %% TODO: 避免由于循环引用(甚至是间接引用)导致的无限扩展 + RevExpandedOtherMacrosTokens = substMacrosRev(MacroValue, MacroMap, []), + substMacrosRev([Paren | Tokens], MacroMap, RevExpandedOtherMacrosTokens ++ RevOutTokens); + ParamsAndBody -> + %% 这个宏确实有变量。在有序列表中收集所有传递参数 + {NTokens, Arguments, Line} = substMacrosArgsExpress(Tokens, []), + %% Expand the varibles + %%io:format("Toks: ~p ~n ParamsAndBody: ~p ~n Arguments: ~p ~n",[Toks, ParamsAndBody, Arguments]), + + ExpandedParamsTokens = substMacrosSubstArgsForVars(ParamsAndBody, [], Arguments, Line), + %% 递归扩展此宏值内的任何宏调用 + %% TODO: 避免由于循环引用(甚至是间接引用)导致的无限扩展 + RevExpandedOtherMacrosTokens = substMacrosRev(ExpandedParamsTokens, MacroMap, []), + substMacrosRev(NTokens, MacroMap, RevExpandedOtherMacrosTokens ++ RevOutTokens) + end; + _ -> + substMacrosRev(Tokens, MacroMap, RevOutTokens) + end; + +substMacrosRev([{'?', _}, {_, _, Name} | Toks], MacroMap, RevOutToks) -> + %% This macro invocation does not have arguments. + %% Therefore the definition should not have parameters + case dict:is_key(Name, MacroMap) of + true -> + {[], MacroValue} = dict:fetch(Name, MacroMap), + + %% Recursively expand any macro calls inside this macro's value + %% TODO: avoid infinite expansion due to circular references (even indirect ones) + RevExpandedOtherMacrosToks = substMacrosRev(MacroValue, MacroMap, []), + substMacrosRev(Toks, MacroMap, RevExpandedOtherMacrosToks ++ RevOutToks); + _ -> + substMacrosRev(Toks, MacroMap, RevOutToks) + end; + +substMacrosRev([Tok | Toks], MacroMap, RevOutToks) -> + substMacrosRev(Toks, MacroMap, [Tok | RevOutToks]); +substMacrosRev([], _MacroDict, RevOutToks) -> RevOutToks. + +substMacrosArgsExpress([{var, _, ArgsName}, {')', Line} | Toks], RevArgs) -> + {Toks, reverse([ArgsName | RevArgs]), Line}; +substMacrosArgsExpress([{var, _, ArgsName}, {',', _} | Toks], RevArgs) -> + substMacrosArgsExpress(Toks, [ArgsName | RevArgs]); +substMacrosArgsExpress(Toks, RevArgs) -> + substMacrosGetExpress(Toks, [], [], RevArgs). + +substMacrosGetExpress([{')', Line} | Toks], [], RevExpress, RevArgs) -> + {Toks, reverse([reverse(RevExpress) | RevArgs]), Line}; +substMacrosGetExpress([{')', Line} | Toks], LeftList, RevExpress, RevArgs) -> + substMacrosGetExpress(Toks, lists:delete('(', LeftList), [{')', Line} | RevExpress], RevArgs); +substMacrosGetExpress([{'}', Line} | Toks], LeftList, RevExpress, RevArgs) -> + substMacrosGetExpress(Toks, lists:delete('{', LeftList), [{'}', Line} | RevExpress], RevArgs); +substMacrosGetExpress([{'end', Line} | Toks], LeftList, RevExpress, RevArgs) -> + substMacrosGetExpress(Toks, lists:delete('begin', LeftList), [{'end', Line} | RevExpress], RevArgs); +substMacrosGetExpress([{']', Line} | Toks], LeftList, RevExpress, RevArgs) -> + substMacrosGetExpress(Toks, lists:delete('[', LeftList), [{']', Line} | RevExpress], RevArgs); +substMacrosGetExpress([{',', _} | Toks], [], RevExpress, RevArgs) -> + substMacrosArgsExpress(Toks, [reverse(RevExpress) | RevArgs]); +substMacrosGetExpress([{',', Line} | Toks], LeftList, RevExpress, RevArgs) -> + substMacrosGetExpress(Toks, LeftList, [{',', Line} | RevExpress], RevArgs); +substMacrosGetExpress([{'(', Line} | Toks], LeftList, RevExpress, RevArgs) -> + substMacrosGetExpress(Toks, ['(' | LeftList], [{'(', Line} | RevExpress], RevArgs); +substMacrosGetExpress([{'begin', Line} | Toks], LeftList, RevExpress, RevArgs) -> + substMacrosGetExpress(Toks, ['begin' | LeftList], [{'begin', Line} | RevExpress], RevArgs); +substMacrosGetExpress([{'[', Line} | Toks], LeftList, RevExpress, RevArgs) -> + substMacrosGetExpress(Toks, ['[' | LeftList], [{'[', Line} | RevExpress], RevArgs); +substMacrosGetExpress([{'{', Line} | Toks], LeftList, RevExpress, RevArgs) -> + substMacrosGetExpress(Toks, ['{' | LeftList], [{'{', Line} | RevExpress], RevArgs); +substMacrosGetExpress([Tok | Toks], LeftList, RevExpress, RevArgs) -> + substMacrosGetExpress(Toks, LeftList, [Tok | RevExpress], RevArgs). + +subst_macros_get_args([{')', _} | Toks], RevArgs) -> + {Toks, reverse(RevArgs)}; +subst_macros_get_args([{',', _}, {var, _, ArgName} | Toks], RevArgs) -> + subst_macros_get_args(Toks, [ArgName | RevArgs]); +subst_macros_get_args([{var, _, ArgName} | Toks], RevArgs) -> + subst_macros_get_args(Toks, [ArgName | RevArgs]). + +substMacrosSubstArgsForVars({[], BodyToks}, NBodyToks, [], Line) -> + L = lists:map( + fun(A) when is_tuple(A) -> + setelement(2, A, Line); + (A) -> + A + end, + BodyToks), + lists:concat(lists:reverse([L | NBodyToks])); +substMacrosSubstArgsForVars({[Param | Params], BodyToks}, NBodyToks, [Arg | Args], Line) -> + case lists:splitwith(fun({_, _, Var}) when Var =:= Param -> false;(_) -> true end, BodyToks) of + {L1, []} -> + L = lists:map(fun(A) when is_tuple(A) -> setelement(2, A, Line);(A) -> A end, L1), + lists:concat(lists:reverse([L | NBodyToks])); + {L1, [_ | L2]} -> + Arg2 = + case Arg of + [_ | _] -> + lists:map(fun(A) when is_tuple(A) -> setelement(2, A, Line);(A) -> A end, Arg); + _ -> + [{var, 1, Arg}] + end, + L3 = lists:map(fun(A) when is_tuple(A) -> setelement(2, A, Line);(A) -> A end, L1), + substMacrosSubstArgsForVars({Params, L2}, [Arg2, L3 | NBodyToks], Args, Line) + end. + +readIncludeFile(Filename, CurrFilename) -> + Dir1 = filename:dirname(CurrFilename), + Dir2 = filename:dirname(Filename), + Dir = lists:concat([Dir1, "/", Dir2]), + BaseName = filename:basename(Filename), + case file:path_open([Dir], BaseName, [read, raw, binary]) of + {ok, IoDevice, FullName} -> + {ok, Data} = file:read(IoDevice, filelib:file_size(FullName)), + file:close(IoDevice), + {FullName, binary_to_list(Data)}; + {error, Reason} -> + throw({failed_to_read_include_file, Reason, Filename, CurrFilename}) + end. + +getIncludeLibPath(FileName) -> + case filename:split(FileName) of + [LibName | _] when LibName =/= "/" -> + OTPLIB = code:lib_dir(), + case z_lib:list_file(OTPLIB, 1) of + {ok, [_ | _] = FileList} -> + [FilePath] = [lists:concat([OTPLIB, "/", File, "/", "include"]) || {File, _, directory, _, _} <- FileList, string:str(File, LibName) > 0], + {ok, FilePath}; + _ -> + {error, no_path} + end; + _ -> + {error, invalid_path} + end. + +readIncludeLibFile(Filename, CurrFilename) -> + case getIncludeLibPath(Filename) of + {ok, FilePath} -> + BaseName = filename:basename(Filename), + case file:path_open([FilePath], BaseName, [read, raw, binary]) of + {ok, IoDevice, FullName} -> + {ok, Data} = file:read(IoDevice, filelib:file_size(FullName)), + file:close(IoDevice), + {FullName, binary_to_list(Data)}; + {error, Reason} -> + throw({readIncludeLibFile, Reason, Filename, CurrFilename}) + end; + {error, Error} -> + throw({getIncludeLibPath, Error, Filename, CurrFilename}) + end. + +normalizeRecord({attribute, La, record, {Record, Fields}} = Form) -> + case epp:normalize_typed_record_fields(Fields) of + {typed, NewFields} -> + {{attribute, La, record, {Record, NewFields}}, [{attribute, La, type, {{record, Record}, Fields, []}}]}; + not_typed -> + {Form, []} + end; +normalizeRecord(Form) -> + {Form, []}. +