1-module(neotoma). 2-author("Sean Cribbs <seancribbs@gmail.com>"). 3-export([file/1, file/2, bootstrap/0]). 4-export([main/1]). 5 6-define(ALL_COMBINATORS, [p_eof, p_optional, p_not, p_assert, p_seq, 7 p_choose, p_zero_or_more, p_one_or_more, p_label, p_scan, 8 p_string, p_anything, p_charclass, p_regexp, line, column]). 9 10-type option() :: {module, atom()} | {output, file:filename()} | {transform_module, atom()} | 11 {neotoma_priv_dir, file:filename()}. 12 13%% @doc Handler function for escript. 14-spec main(list()) -> ok | no_return(). 15main([]) -> 16 io:format("Usage: neotoma filename [-module output_module] [-output output_dir] [-transform_module transform_module]\n"); 17main([Filename | Args]) -> 18 %% code:priv_dir is unreliable when called in escript context, but 19 %% escript:script_name does what we want. 20 PrivDir = filename:join([filename:dirname(escript:script_name()), "priv"]), 21 file(Filename, [{neotoma_priv_dir, PrivDir} | parse_options(Args)]). 22 23%% @doc Generates a parser from the specified file. 24%% @equiv file(Filename, []) 25-spec file(file:filename()) -> ok | {error, atom()}. 26file(InputGrammar) -> 27 file(InputGrammar, []). 28 29%% @doc Generates a parser from the specified file with the given options. 30-spec file(file:filename(), [option()]) -> ok | {error, atom()}. 31file(InputGrammar, Options) -> 32 Basename = filename:basename(InputGrammar, ".peg"), 33 InputDir = filename:dirname(InputGrammar), 34 ModuleName = proplists:get_value(module, Options, list_to_atom(Basename)), 35 OutputDir = proplists:get_value(output, Options, InputDir), 36 OutputFilename = filename:join(OutputDir, atom_to_list(ModuleName) ++ ".erl"), 37 TransformModule = proplists:get_value(transform_module, Options, false), 38 validate_params(filename:absname(InputGrammar), 39 ModuleName, 40 TransformModule, 41 filename:absname(OutputFilename)), 42 Parsed = parse_grammar(InputGrammar), 43 Rules = proplists:get_value(rules, Parsed), 44 Root = proplists:get_value(root, Parsed), 45 Code = proplists:get_value(code, Parsed), 46 GenTransform = proplists:get_value(transform, Parsed), 47 Combinators = proplists:get_value(combinators, Parsed, ?ALL_COMBINATORS), 48 ModuleAttrs = generate_module_attrs(ModuleName, Combinators), 49 EntryFuns = generate_entry_functions(Root), 50 TransformFun = create_transform(TransformModule, OutputDir, GenTransform), 51 PrivDir = proplists:get_value(neotoma_priv_dir, Options, code:priv_dir(neotoma)), 52 {ok, PegIncludes} = file:read_file(filename:join([PrivDir, "peg_includes.hrl"])), 53 file:write_file(OutputFilename, [ModuleAttrs, "\n", Code, "\n", EntryFuns, "\n", Rules, "\n", TransformFun, "\n", PegIncludes]). 54 55-spec validate_params(file:filename(),atom(),atom(),file:filename()) -> 'ok'. 56validate_params(InputGrammar, _, _, OutputFile) when InputGrammar =:= OutputFile -> 57 throw({badarg, "Input and output file are the same!"}); 58validate_params(_,_, false, _) -> ok; 59validate_params(_,_, TransformModule, _) when not is_atom(TransformModule) -> 60 throw({badarg, "transform_module option must be an atom"}); 61validate_params(_,Basename, TransformModule, _) when Basename =:= TransformModule -> 62 throw({badarg, "Transform module named same as parser module!"}); 63validate_params(_,_, TransformModule, OutputFile) -> 64 OutMod = list_to_atom(filename:basename(OutputFile, ".erl")), 65 case OutMod of 66 TransformModule -> throw({badarg, "Transform module file same as parser output file!"}); 67 _ -> ok 68 end. 69 70-spec generate_module_attrs(atom(), [atom()]) -> iolist(). 71generate_module_attrs(ModName, Combinators) -> 72 ["-module(", atom_to_list(ModName) ,").\n", 73 "-export([parse/1,file/1]).\n", 74 [ generate_combinator_macro(C) || Combinators /= undefined, C <- Combinators ], 75 "\n" 76 ]. 77 78generate_combinator_macro(C) -> 79 ["-define(", atom_to_list(C), ",true).\n"]. 80 81-spec generate_entry_functions({iodata(),_}) -> iolist(). 82generate_entry_functions(Root) -> 83 {RootRule,_} = Root, 84 ["-spec file(file:name()) -> any().\n", 85 "file(Filename) -> case file:read_file(Filename) of {ok,Bin} -> parse(Bin); Err -> Err end.\n\n", 86 "-spec parse(binary() | list()) -> any().\n", 87 "parse(List) when is_list(List) -> parse(unicode:characters_to_binary(List));\n", 88 "parse(Input) when is_binary(Input) ->\n", 89 " _ = setup_memo(),\n", 90 " Result = case '",RootRule,"'(Input,{{line,1},{column,1}}) of\n", 91 " {AST, <<>>, _Index} -> AST;\n", 92 " Any -> Any\n" 93 " end,\n", 94 " release_memo(), Result.\n"]. 95 96-spec parse_grammar(file:filename()) -> any(). 97parse_grammar(InputFile) -> 98 case neotoma_parse:file(InputFile) of 99 {fail, Index} -> 100 throw({grammar_error, {fail, Index}}); 101 {Parsed, Remainder, Index} -> 102 io:format("WARNING: Grammar parse ended unexpectedly at ~p, generated parser may be incorrect.~nRemainder:~n~p", 103 [Index, Remainder]), 104 Parsed; 105 L when is_list(L) -> L; 106 _ -> throw({error, {unknown, grammar, InputFile}}) 107 end. 108 109-spec create_transform(atom() | boolean(),file:filename(),_) -> iolist(). 110create_transform(_,_,[]) -> []; 111create_transform(false,_,_) -> 112 "transform(_,Node,_Index) -> Node."; 113create_transform(ModName,Dir,_) when is_atom(ModName) -> 114 XfFile = filename:join(Dir, atom_to_list(ModName) ++ ".erl"), 115 case filelib:is_regular(XfFile) of 116 true -> io:format("'~s' already exists, skipping generation.~n", [XfFile]); 117 false -> generate_transform_stub(XfFile, ModName) 118 end, 119 ["transform(Symbol,Node,Index) -> ",atom_to_list(ModName),":transform(Symbol, Node, Index)."]. 120 121-spec generate_transform_stub(file:filename(), atom()) -> 'ok' | {'error',atom()}. 122generate_transform_stub(XfFile,ModName) -> 123 Data = ["-module(",atom_to_list(ModName),").\n", 124 "-export([transform/3]).\n\n", 125 "%% Add clauses to this function to transform syntax nodes\n", 126 "%% from the parser into semantic output.\n", 127 "transform(Symbol, Node, _Index) when is_atom(Symbol) ->\n Node."], 128 file:write_file(XfFile, Data). 129 130%% @doc Bootstraps the neotoma metagrammar. Intended only for internal development! 131%% @equiv file("src/neotoma_parse.peg") 132-spec bootstrap() -> 'ok'. 133bootstrap() -> 134 file("priv/neotoma_parse.peg", [{output, "src/"}, {neotoma_priv_dir, "priv"}]). 135 136%% @doc Parses arguments passed to escript 137-spec parse_options(list()) -> list(). 138parse_options(["-module", ModName | Rest]) -> 139 [{module, list_to_atom(ModName)} | parse_options(Rest)]; 140parse_options(["-output", Dir | Rest]) -> 141 [{output, Dir} | parse_options(Rest)]; 142parse_options(["-transform_module", ModName | Rest]) -> 143 [{transform_module, list_to_atom(ModName)} | parse_options(Rest)]; 144parse_options([]) -> 145 []. 146