diff --git a/.gitignore b/.gitignore index 443f45b..0489cab 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ ebin/* _build erl_crash.dump *.pyc -src/ec_semver_parser.erl + +src/ec_semver_parser.peg diff --git a/Makefile b/Makefile index 0871acf..031bba4 100644 --- a/Makefile +++ b/Makefile @@ -19,15 +19,20 @@ ERLWARE_COMMONS_PLT=$(CURDIR)/.erlware_commons_plt all: compile doc test #dialyzer #fail on travis -deps: +deps: .DEV_MODE $(REBAR) get-deps compile +.DEV_MODE: + touch $@ + cp priv/ec_semver_parser.peg src + get-deps: $(REBAR) get-deps compile compile: deps $(REBAR) skip_deps=true compile + doc: compile - $(REBAR) skip_deps=true doc @@ -84,5 +89,6 @@ clean: distclean: clean rm -rf $(ERLWARE_COMMONS_PLT).$(ERL_VER) rm -rvf $(CURDIR)/deps + rm -rvf .DEV_MODE rebuild: distclean get-deps all diff --git a/src/ec_semver_parser.peg b/priv/ec_semver_parser.peg similarity index 100% rename from src/ec_semver_parser.peg rename to priv/ec_semver_parser.peg diff --git a/rebar.config b/rebar.config index 57dc49a..6bc7d6f 100644 --- a/rebar.config +++ b/rebar.config @@ -1,11 +1,9 @@ %% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- %% Dependencies ================================================================ -{deps, [{neotoma, "", - {git, "https://github.com/seancribbs/neotoma.git", {branch, master}}}, - {proper, "", {git, "https://github.com/bkearns/proper.git", {branch, master}}}, - {rebar_vsn_plugin, ".*", {git, "https://github.com/erlware/rebar_vsn_plugin.git", - {branch, "master"}}}]}. +{deps, [{rebar_vsn_plugin, ".*", + {git, "https://github.com/erlware/rebar_vsn_plugin.git", + {branch, "master"}}}]}. {erl_first_files, ["ec_dictionary"]}. diff --git a/rebar.config.script b/rebar.config.script index c19a751..bbe043f 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -1,15 +1,42 @@ +%% Merge the list values in `ToAdd' into the list found at key `Key' +%% in proplist `C'. Don't duplicate items. New Items are added to the +%% front of existing items. It is an error if the value at `Key' is +%% not a list in `C'. +MergeConfig = fun({Key, ToAdd}, C) -> + case lists:keyfind(Key, 1, C) of + false -> + lists:keystore(Key, 1, C, {Key, ToAdd}); + {Key, List} when is_list(List) -> + %% remove items in ToAdd already in List + ToAdd1 = [ I || I <- ToAdd, not lists:member(I, List) ], + lists:keystore(Key, 1, C, {Key, ToAdd1 ++ List }) + end + end. + {match, [ErtsNumber]} = re:run(erlang:system_info(otp_release), "R(\\d+).+", [{capture, [1], list}]), ErtsVsn = erlang:list_to_integer(ErtsNumber), -Opts1 = case lists:keysearch(erl_opts, 1, CONFIG) of - {value, {erl_opts, Opts0}} -> - Opts0; - false -> - [] - end, -Opts2 = if - ErtsVsn >= 15 -> - [{d, have_callback_support} | Opts1]; - true -> - Opts1 - end, -lists:keystore(erl_opts, 1, CONFIG, {erl_opts, Opts2}). +AddErlOpts = if + ErtsVsn >= 15 -> + [{d, have_callback_support}]; + true -> + [] + end, + +DevOnlyDeps = [{neotoma, "", + {git, "https://github.com/seancribbs/neotoma.git", {branch, master}}}, + {proper, "", + {git, "https://github.com/bkearns/proper.git", {branch, master}}}], + +Config1 = MergeConfig({erl_opts, AddErlOpts}, CONFIG), + +ConfigPath = filename:dirname(SCRIPT), +DevMarker = filename:join([ConfigPath, ".DEV_MODE"]), +case filelib:is_file(DevMarker) of + true -> + lists:foldl(MergeConfig, Config1, + [{deps, DevOnlyDeps}, + {erl_opts, [{d, 'DEV_ONLY'}]}]); + false -> + Config1 +end. + diff --git a/src/ec_semver_parser.erl b/src/ec_semver_parser.erl new file mode 100644 index 0000000..3829c71 --- /dev/null +++ b/src/ec_semver_parser.erl @@ -0,0 +1,231 @@ +-module(ec_semver_parser). +-export([parse/1,file/1]). +-compile({nowarn_unused_function,[p/4, p/5, p_eof/0, p_optional/1, p_not/1, p_assert/1, p_seq/1, p_and/1, p_choose/1, p_zero_or_more/1, p_one_or_more/1, p_label/2, p_string/1, p_anything/0, p_charclass/1, p_regexp/1, p_attempt/4, line/1, column/1]}). + + +-compile(export_all). +-spec file(file:name()) -> any(). +file(Filename) -> {ok, Bin} = file:read_file(Filename), parse(Bin). + +-spec parse(binary() | list()) -> any(). +parse(List) when is_list(List) -> parse(list_to_binary(List)); +parse(Input) when is_binary(Input) -> + setup_memo(), + Result = case 'semver'(Input,{{line,1},{column,1}}) of + {AST, <<>>, _Index} -> AST; + Any -> Any + end, + release_memo(), Result. + +'semver'(Input, Index) -> + p(Input, Index, 'semver', fun(I,D) -> (p_seq([fun 'major_minor_patch_min_patch'/2, p_optional(p_seq([p_string(<<"-">>), fun 'alpha_part'/2, p_zero_or_more(p_seq([p_string(<<".">>), fun 'alpha_part'/2]))])), p_optional(p_seq([p_string(<<"+">>), fun 'alpha_part'/2, p_zero_or_more(p_seq([p_string(<<".">>), fun 'alpha_part'/2]))])), p_not(p_anything())]))(I,D) end, fun(Node, _Idx) -> ec_semver:internal_parse_version(Node) end). + +'major_minor_patch_min_patch'(Input, Index) -> + p(Input, Index, 'major_minor_patch_min_patch', fun(I,D) -> (p_seq([p_choose([p_seq([p_optional(p_string(<<"v">>)), fun 'numeric_part'/2]), fun 'alpha_part'/2]), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2])), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2])), p_optional(p_seq([p_string(<<".">>), fun 'version_part'/2]))]))(I,D) end, fun(Node, Idx) ->transform('major_minor_patch_min_patch', Node, Idx) end). + +'version_part'(Input, Index) -> + p(Input, Index, 'version_part', fun(I,D) -> (p_choose([fun 'numeric_part'/2, fun 'alpha_part'/2]))(I,D) end, fun(Node, Idx) ->transform('version_part', Node, Idx) end). + +'numeric_part'(Input, Index) -> + p(Input, Index, 'numeric_part', fun(I,D) -> (p_one_or_more(p_charclass(<<"[0-9]">>)))(I,D) end, fun(Node, _Idx) ->erlang:list_to_integer(erlang:binary_to_list(erlang:iolist_to_binary(Node))) end). + +'alpha_part'(Input, Index) -> + p(Input, Index, 'alpha_part', fun(I,D) -> (p_one_or_more(p_charclass(<<"[A-Za-z0-9]">>)))(I,D) end, fun(Node, _Idx) ->erlang:iolist_to_binary(Node) end). + + +transform(_,Node,_Index) -> Node. + +p(Inp, Index, Name, ParseFun) -> + p(Inp, Index, Name, ParseFun, fun(N, _Idx) -> N end). + +p(Inp, StartIndex, Name, ParseFun, TransformFun) -> + case get_memo(StartIndex, Name) of % See if the current reduction is memoized + {ok, Memo} -> %Memo; % If it is, return the stored result + Memo; + _ -> % If not, attempt to parse + Result = case ParseFun(Inp, StartIndex) of + {fail,_} = Failure -> % If it fails, memoize the failure + Failure; + {Match, InpRem, NewIndex} -> % If it passes, transform and memoize the result. + Transformed = TransformFun(Match, StartIndex), + {Transformed, InpRem, NewIndex} + end, + memoize(StartIndex, Name, Result), + Result + end. + +setup_memo() -> + put({parse_memo_table, ?MODULE}, ets:new(?MODULE, [set])). + +release_memo() -> + ets:delete(memo_table_name()). + +memoize(Index, Name, Result) -> + Memo = case ets:lookup(memo_table_name(), Index) of + [] -> []; + [{Index, Plist}] -> Plist + end, + ets:insert(memo_table_name(), {Index, [{Name, Result}|Memo]}). + +get_memo(Index, Name) -> + case ets:lookup(memo_table_name(), Index) of + [] -> {error, not_found}; + [{Index, Plist}] -> + case proplists:lookup(Name, Plist) of + {Name, Result} -> {ok, Result}; + _ -> {error, not_found} + end + end. + +memo_table_name() -> + get({parse_memo_table, ?MODULE}). + +p_eof() -> + fun(<<>>, Index) -> {eof, [], Index}; + (_, Index) -> {fail, {expected, eof, Index}} end. + +p_optional(P) -> + fun(Input, Index) -> + case P(Input, Index) of + {fail,_} -> {[], Input, Index}; + {_, _, _} = Success -> Success + end + end. + +p_not(P) -> + fun(Input, Index)-> + case P(Input,Index) of + {fail,_} -> + {[], Input, Index}; + {Result, _, _} -> {fail, {expected, {no_match, Result},Index}} + end + end. + +p_assert(P) -> + fun(Input,Index) -> + case P(Input,Index) of + {fail,_} = Failure-> Failure; + _ -> {[], Input, Index} + end + end. + +p_and(P) -> + p_seq(P). + +p_seq(P) -> + fun(Input, Index) -> + p_all(P, Input, Index, []) + end. + +p_all([], Inp, Index, Accum ) -> {lists:reverse( Accum ), Inp, Index}; +p_all([P|Parsers], Inp, Index, Accum) -> + case P(Inp, Index) of + {fail, _} = Failure -> Failure; + {Result, InpRem, NewIndex} -> p_all(Parsers, InpRem, NewIndex, [Result|Accum]) + end. + +p_choose(Parsers) -> + fun(Input, Index) -> + p_attempt(Parsers, Input, Index, none) + end. + +p_attempt([], _Input, _Index, Failure) -> Failure; +p_attempt([P|Parsers], Input, Index, FirstFailure)-> + case P(Input, Index) of + {fail, _} = Failure -> + case FirstFailure of + none -> p_attempt(Parsers, Input, Index, Failure); + _ -> p_attempt(Parsers, Input, Index, FirstFailure) + end; + Result -> Result + end. + +p_zero_or_more(P) -> + fun(Input, Index) -> + p_scan(P, Input, Index, []) + end. + +p_one_or_more(P) -> + fun(Input, Index)-> + Result = p_scan(P, Input, Index, []), + case Result of + {[_|_], _, _} -> + Result; + _ -> + {fail, {expected, Failure, _}} = P(Input,Index), + {fail, {expected, {at_least_one, Failure}, Index}} + end + end. + +p_label(Tag, P) -> + fun(Input, Index) -> + case P(Input, Index) of + {fail,_} = Failure -> + Failure; + {Result, InpRem, NewIndex} -> + {{Tag, Result}, InpRem, NewIndex} + end + end. + +p_scan(_, [], Index, Accum) -> {lists:reverse( Accum ), [], Index}; +p_scan(P, Inp, Index, Accum) -> + case P(Inp, Index) of + {fail,_} -> {lists:reverse(Accum), Inp, Index}; + {Result, InpRem, NewIndex} -> p_scan(P, InpRem, NewIndex, [Result | Accum]) + end. + +p_string(S) when is_list(S) -> p_string(list_to_binary(S)); +p_string(S) -> + Length = erlang:byte_size(S), + fun(Input, Index) -> + try + <> = Input, + {S, Rest, p_advance_index(S, Index)} + catch + error:{badmatch,_} -> {fail, {expected, {string, S}, Index}} + end + end. + +p_anything() -> + fun(<<>>, Index) -> {fail, {expected, any_character, Index}}; + (Input, Index) when is_binary(Input) -> + <> = Input, + {<>, Rest, p_advance_index(<>, Index)} + end. + +p_charclass(Class) -> + {ok, RE} = re:compile(Class, [unicode, dotall]), + fun(Inp, Index) -> + case re:run(Inp, RE, [anchored]) of + {match, [{0, Length}|_]} -> + {Head, Tail} = erlang:split_binary(Inp, Length), + {Head, Tail, p_advance_index(Head, Index)}; + _ -> {fail, {expected, {character_class, binary_to_list(Class)}, Index}} + end + end. + +p_regexp(Regexp) -> + {ok, RE} = re:compile(Regexp, [unicode, dotall, anchored]), + fun(Inp, Index) -> + case re:run(Inp, RE) of + {match, [{0, Length}|_]} -> + {Head, Tail} = erlang:split_binary(Inp, Length), + {Head, Tail, p_advance_index(Head, Index)}; + _ -> {fail, {expected, {regexp, binary_to_list(Regexp)}, Index}} + end + end. + +line({{line,L},_}) -> L; +line(_) -> undefined. + +column({_,{column,C}}) -> C; +column(_) -> undefined. + +p_advance_index(MatchedInput, Index) when is_list(MatchedInput) orelse is_binary(MatchedInput)-> % strings + lists:foldl(fun p_advance_index/2, Index, unicode:characters_to_list(MatchedInput)); +p_advance_index(MatchedInput, Index) when is_integer(MatchedInput) -> % single characters + {{line, Line}, {column, Col}} = Index, + case MatchedInput of + $\n -> {{line, Line+1}, {column, 1}}; + _ -> {{line, Line}, {column, Col+1}} + end. diff --git a/test/ec_dictionary_proper.erl b/test/ec_dictionary_proper.erl index d30f542..a455e90 100644 --- a/test/ec_dictionary_proper.erl +++ b/test/ec_dictionary_proper.erl @@ -5,6 +5,8 @@ %% proper:module(ec_dictionary_proper). -module(ec_dictionary_proper). +-ifdef(DEV_ONLY). + -export([my_dict/0, dict/1, sym_dict/0, sym_dict/1, gb_tree/0, gb_tree/1, sym_dict2/0]). -include_lib("proper/include/proper.hrl"). @@ -221,3 +223,4 @@ gb_tree(0) -> gb_tree(N) -> gb_trees:enter(key(),value(),gb_tree(N-1)). +-endif.