Current section
Files
Jump to
Current section
Files
src/ramble.erl
-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(<<C/binary, Rest/binary>>)
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] ->
<<<<Head/binary, " or "/utf8>>/binary, X/binary>>;
[X@1, Y] ->
<<<<<<<<Head/binary, ", "/utf8>>/binary, X@1/binary>>/binary,
", or "/utf8>>/binary,
Y/binary>>;
[X@2 | Rest] ->
<<<<Head/binary, ", "/utf8>>/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
).