-module(ramble). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([get_config/0, go_on_tokens/3, go/3, position/0, get_errors/1, label/2, consume_and_discard_until/1, map/2, any_token/0, recover/0, satisfy/1, extract/2, token/1, return/1, seq/2, do_and_recover/2, either/2, char_in_range/2, lowercase_letter/0, uppercase_letter/0, letter/0, an_token/1, digit/0, choice/1, exact/1, desperate_custom_parser/1, main/0]). -export_type([position/0, token/1, parse_error/1, parse_result/2, final_parse_result/2, parser_config/1, parser/2]). -type position() :: {position, integer(), integer()}. -type token(FKG) :: {token, FKG} | {labeled_token, binary()}. -type parse_error(FKH) :: {unexpected, position(), token(FKH), list(token(FKH))}. -type parse_result(FKI, FKJ) :: {parse_result, {ok, FKJ} | {error, parse_error(FKI)}, list(FKI), position(), list(parse_error(FKI))}. -type final_parse_result(FKK, FKL) :: {final_parse_result, {ok, FKL} | {error, parse_error(FKK)}, list(parse_error(FKK))}. -type parser_config(FKM) :: {parser_config, FKM, FKM, binary(), fun((FKM) -> binary())}. -type parser(FKN, FKO) :: {parser, fun((list(FKN), position(), parser_config(FKN), gleam@option:option(parser(FKN, FKN))) -> parse_result(FKN, FKO))}. -spec get_config() -> parser(FKP, parser_config(FKP)). get_config() -> {parser, fun(Source, Pos, Config, _) -> {parse_result, {ok, Config}, Source, Pos, []} end}. -spec go_on_tokens(parser(FKT, FKU), list(FKT), parser_config(FKT)) -> final_parse_result(FKT, FKU). go_on_tokens(P, Src, Config) -> Res = (erlang:element(2, P))(Src, {position, 1, 1}, Config, none), {final_parse_result, erlang:element(2, Res), erlang:element(5, Res)}. -spec go(parser(binary(), FLB), binary(), parser_config(binary())) -> final_parse_result(binary(), FLB). go(P, Src, Config) -> go_on_tokens(P, gleam@string:to_graphemes(Src), Config). -spec position() -> parser(any(), position()). position() -> {parser, fun(Source, P, _, _) -> {parse_result, {ok, P}, Source, P, []} end}. -spec get_errors(parse_result(FLK, any())) -> list(parse_error(FLK)). get_errors(Res) -> case erlang:element(2, Res) of {ok, _} -> erlang:element(5, Res); {error, E} -> [E | erlang:element(5, Res)] end. -spec label(binary(), fun(() -> parser(FLQ, FLR))) -> parser(FLQ, FLR). label(Label, P) -> {parser, fun(Source, Pos, Config, Recovery) -> Res = (erlang:element(2, P()))(Source, Pos, Config, Recovery), case erlang:element(2, Res) of {ok, _} -> Res; {error, E} -> E@1 = case E of {unexpected, Pos@1, T, _} -> {unexpected, Pos@1, T, [{labeled_token, Label}]} end, {parse_result, {error, E@1}, erlang:element(3, Res), erlang:element(4, Res), erlang:element(5, Res)} end end}. -spec consume_and_discard_until(parser(FLW, FLW)) -> parser(FLW, nil). consume_and_discard_until(P) -> {parser, fun(Source, Pos, Config, Recovery) -> Res = (erlang:element(2, P))(Source, Pos, Config, Recovery), case erlang:element(2, Res) of {ok, _} -> {parse_result, {ok, nil}, Source, Pos, []}; {error, _} -> (erlang:element(2, consume_and_discard_until(P)))( erlang:element(3, Res), erlang:element(4, Res), Config, Recovery ) end end}. -spec map(parser(FMB, FMC), fun((FMC) -> FMF)) -> parser(FMB, FMF). map(P, F) -> {parser, fun(Source, Pos, Config, Recovery) -> Res = (erlang:element(2, P))(Source, Pos, Config, Recovery), case erlang:element(2, Res) of {ok, V} -> {parse_result, {ok, F(V)}, erlang:element(3, Res), erlang:element(4, Res), erlang:element(5, Res)}; {error, E} -> {parse_result, {error, E}, Source, Pos, erlang:element(5, Res)} end end}. -spec any_token() -> parser(FMI, FMI). any_token() -> {parser, fun(Source, Pos, Config, _) -> {position, Row, Col} = case Pos of {position, _, _} -> Pos; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"ramble"/utf8>>, function => <<"any_token"/utf8>>, line => 139}) end, case Source of [Head | Tail] -> New_pos = case Head =:= erlang:element(2, Config) of true -> {position, Row + 1, 0}; false -> {position, Row, Col + 1} end, {parse_result, {ok, Head}, Tail, New_pos, []}; [] -> Err = {unexpected, Pos, {token, erlang:element(3, Config)}, []}, {parse_result, {error, Err}, Source, Pos, []} end end}. -spec recover() -> parser(any(), nil). recover() -> {parser, fun(Source, Pos, Config, Recovery) -> P = case Recovery of {some, R} -> consume_and_discard_until(R); none -> _pipe = any_token(), map(_pipe, fun(_) -> nil end) end, Res = (erlang:element(2, P))(Source, Pos, Config, none), {parse_result, {ok, nil}, erlang:element(3, Res), erlang:element(4, Res), []} end}. -spec satisfy(fun((FMO) -> boolean())) -> parser(FMO, FMO). satisfy(Pred) -> {parser, fun(Source, Pos, Config, Recovery) -> case Source of [H | T] -> case Pred(H) of true -> {position, Row, Col} = Pos, New_pos = case H =:= erlang:element(2, Config) of true -> {position, Row + 1, 0}; false -> {position, Row, Col + 1} end, {parse_result, {ok, H}, T, New_pos, []}; false -> Err = {unexpected, Pos, {token, H}, []}, Res = (erlang:element(2, recover()))( Source, Pos, Config, Recovery ), {parse_result, {error, Err}, erlang:element(3, Res), erlang:element(4, Res), []} end; [] -> Err@1 = {unexpected, Pos, {token, erlang:element(3, Config)}, []}, {parse_result, {error, Err@1}, Source, Pos, []} end end}. -spec extract(parser(FMU, FMV), fun((FMV) -> parser(FMU, FMY))) -> parser(FMU, FMY). extract(P, K) -> {parser, fun(Source, Pos, Config, Recovery) -> Res = (erlang:element(2, P))(Source, Pos, Config, Recovery), case erlang:element(2, Res) of {ok, V} -> (erlang:element(2, K(V)))( erlang:element(3, Res), erlang:element(4, Res), Config, Recovery ); {error, E} -> {parse_result, {error, E}, erlang:element(3, Res), erlang:element(4, Res), erlang:element(5, Res)} end end}. -spec token(FMR) -> parser(FMR, FMR). token(T) -> extract( get_config(), fun(Config) -> label( <<<<"a '"/utf8, ((erlang:element(5, Config))(T))/binary>>/binary, "'"/utf8>>, fun() -> satisfy(fun(X) -> X =:= T end) end ) end ). -spec return(FND) -> parser(any(), FND). return(A) -> {parser, fun(Source, Pos, _, _) -> {parse_result, {ok, A}, Source, Pos, []} end}. -spec seq(parser(FNH, any()), parser(FNH, FNL)) -> parser(FNH, FNL). seq(P1, P2) -> {parser, fun(Source, Pos, Config, Recovery) -> Res1 = (erlang:element(2, P1))(Source, Pos, Config, Recovery), Res2 = (erlang:element(2, P2))( erlang:element(3, Res1), erlang:element(4, Res1), Config, Recovery ), Errors = gleam@list:append( get_errors(Res1), erlang:element(5, Res2) ), {parse_result, erlang:element(2, Res2), erlang:element(3, Res2), erlang:element(4, Res2), Errors} end}. -spec do_and_recover(parser(FNQ, FNR), parser(FNQ, FNQ)) -> parser(FNQ, FNR). do_and_recover(P, Recovery) -> {parser, fun(Source, Pos, Config, _) -> (erlang:element(2, P))(Source, Pos, Config, {some, Recovery}) end}. -spec either(parser(FNY, FNZ), parser(FNY, FNZ)) -> parser(FNY, FNZ). either(P1, P2) -> {parser, fun(Source, Pos, Config, Recovery) -> Res1 = (erlang:element(2, P1))(Source, Pos, Config, Recovery), case erlang:element(2, Res1) of {ok, _} -> Res1; {error, E1} -> Res2 = (erlang:element(2, P2))( Source, Pos, Config, Recovery ), case erlang:element(2, Res2) of {ok, _} -> Res2; {error, E2} -> {parse_result, {error, {unexpected, Pos, erlang:element(3, E2), gleam@list:append( erlang:element(4, E1), erlang:element(4, E2) )}}, erlang:element(3, Res2), erlang:element(4, Res2), []} end end end}. -spec char_in_range(integer(), integer()) -> parser(binary(), binary()). char_in_range(From, To) -> satisfy( fun(C) -> _assert_subject = gleam@string:to_utf_codepoints(C), [Codepoint] = case _assert_subject of [_] -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"ramble"/utf8>>, function => <<"char_in_range"/utf8>>, line => 266}) end, N = gleam@string:utf_codepoint_to_int(Codepoint), (From < N) andalso (N < To) end ). -spec lowercase_letter() -> parser(binary(), binary()). lowercase_letter() -> label( <<"a lowercase letter"/utf8>>, fun() -> char_in_range(16#61, 16#7b) end ). -spec uppercase_letter() -> parser(binary(), binary()). uppercase_letter() -> label( <<"an uppercase letter"/utf8>>, fun() -> char_in_range(16#41, 16#5b) end ). -spec letter() -> parser(binary(), binary()). letter() -> label( <<"a letter"/utf8>>, fun() -> either(lowercase_letter(), uppercase_letter()) end ). -spec an_token(binary()) -> parser(binary(), binary()). an_token(C) -> label(<<<<"an '"/utf8, C/binary>>/binary, "'"/utf8>>, fun() -> token(C) end). -spec digit() -> parser(binary(), binary()). digit() -> label(<<"a digit"/utf8>>, fun() -> char_in_range(16#30, 16#3a) end). -spec choice_helper(list(parser(FOZ, FPA)), list(token(FOZ))) -> parser(FOZ, FPA). choice_helper(Ps, Expected) -> {parser, fun(Source, Pos, Config, Recovery) -> case Ps of [] -> erlang:error(#{gleam_error => panic, message => <<"`ramble.choice()` doesn't accept an empty list of parsers"/utf8>>, module => <<"ramble"/utf8>>, function => <<"choice_helper"/utf8>>, line => 321}); [P] -> Res = (erlang:element(2, P))(Source, Pos, Config, Recovery), case erlang:element(2, Res) of {ok, _} -> Res; {error, E} -> Expectations = gleam@list:append( Expected, erlang:element(4, E) ), Err = {unexpected, erlang:element(2, E), erlang:element(3, E), Expectations}, {parse_result, {error, Err}, Source, Pos, []} end; [P@1 | T] -> Res@1 = (erlang:element(2, P@1))( Source, Pos, Config, Recovery ), case erlang:element(2, Res@1) of {ok, V} -> {parse_result, {ok, V}, Source, Pos, []}; {error, E@1} -> (erlang:element( 2, choice_helper( T, gleam@list:append( erlang:element(4, E@1), Expected ) ) ))(Source, Pos, Config, Recovery) end end end}. -spec choice(list(parser(FOS, FOT))) -> parser(FOS, FOT). choice(Ps) -> {parser, fun(Source, Pos, Config, Recovery) -> Res = (erlang:element(2, choice_helper(Ps, [])))( Source, Pos, Config, Recovery ), case erlang:element(2, Res) of {ok, _} -> Res; {error, E} -> Res2 = (erlang:element(2, recover()))( Source, Pos, Config, Recovery ), {parse_result, {error, E}, erlang:element(3, Res2), erlang:element(4, Res2), erlang:element(5, Res)} end end}. -spec exact(binary()) -> parser(binary(), binary()). exact(S) -> label( <<<<"\""/utf8, S/binary>>/binary, "\""/utf8>>, fun() -> case gleam@string:pop_grapheme(S) of {ok, {H, T}} -> extract( token(H), fun(C) -> extract( exact(T), fun(Rest) -> return(<>) end ) end ); {error, _} -> return(<<""/utf8>>) end end ). -spec desperate_custom_parser( fun((list(FPK), position(), parser_config(FPK), gleam@option:option(parser(FPK, FPK))) -> parse_result(FPK, FPQ)) ) -> parser(FPK, FPQ). desperate_custom_parser(Parse) -> {parser, Parse}. -spec pretty_expectations(binary(), list(binary())) -> binary(). pretty_expectations(Head, Tail) -> case Tail of [] -> Head; [X] -> <<<>/binary, X/binary>>; [X@1, Y] -> <<<<<<<>/binary, X@1/binary>>/binary, ", or "/utf8>>/binary, Y/binary>>; [X@2 | Rest] -> <<<>/binary, (pretty_expectations(X@2, Rest))/binary>> end. -spec pretty_err(parse_error(FPW), fun((FPW) -> binary()), binary()) -> binary(). pretty_err(Err, Show_token, Source_name) -> Show = fun(Token) -> case Token of {token, T} -> Show_token(T); {labeled_token, S} -> S end end, case gleam@list:map(erlang:element(4, Err), Show) of [] -> <<<<<<<<<<<<<<<<"I didn't expect the `"/utf8, (Show(erlang:element(3, Err)))/binary>>/binary, "` at position "/utf8>>/binary, (gleam@int:to_string( erlang:element( 2, erlang:element(2, Err) ) ))/binary>>/binary, ":"/utf8>>/binary, (gleam@int:to_string( erlang:element(3, erlang:element(2, Err)) ))/binary>>/binary, " in "/utf8>>/binary, Source_name/binary>>/binary, "."/utf8>>; [Hd | Tl] -> <<<<<<<<<<<<<<<<<<<<"I didn't expect the `"/utf8, (Show( erlang:element(3, Err) ))/binary>>/binary, "` at position "/utf8>>/binary, (gleam@int:to_string( erlang:element( 2, erlang:element(2, Err) ) ))/binary>>/binary, ":"/utf8>>/binary, (gleam@int:to_string( erlang:element( 3, erlang:element(2, Err) ) ))/binary>>/binary, " in "/utf8>>/binary, Source_name/binary>>/binary, ". I expected "/utf8>>/binary, (pretty_expectations(Hd, Tl))/binary>>/binary, "."/utf8>> end. -spec main() -> nil. main() -> Config = {parser_config, <<"\n"/utf8>>, <<"EOF"/utf8>>, <<"DEBUG"/utf8>>, fun(S) -> S end}, P = seq( do_and_recover(exact(<<"def"/utf8>>), token(<<";"/utf8>>)), seq( token(<<";"/utf8>>), seq( choice( [uppercase_letter(), an_token(<<"a"/utf8>>), either(digit(), an_token(<<"r"/utf8>>))] ), token(<<"d"/utf8>>) ) ) ), gleam@io:println( <<" Parser code: seq( do_and_recover(exact(\"def\"), token(\";\")), seq( token(\";\"), seq( choice([ uppercase_letter(), an_token(\"a\"), either(digit(), an_token(\"r\")), ]), token(\"d\"), ), ), )"/utf8>> ), Code = <<"ab;cd"/utf8>>, gleam@io:println(<<"Parsing: "/utf8, Code/binary>>), {final_parse_result, V, Errs} = go(P, Code, Config), Errs@1 = case V of {error, E} -> gleam@list:append(Errs, [E]); {ok, V@1} -> gleam@io:println( <<"Parser was able to produce a final result: "/utf8, V@1/binary>> ), Errs end, gleam@list:each( Errs@1, fun(It) -> gleam@io:println( pretty_err( It, erlang:element(5, Config), erlang:element(4, Config) ) ) end ).