Current section

Files

Jump to
caffeine_lang src caffeine_query_language@parser.erl
Raw

src/caffeine_query_language@parser.erl

-module(caffeine_query_language@parser).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/caffeine_query_language/parser.gleam").
-export([is_last_char/2, is_balanced_parens/3, find_rightmost_operator_at_level/5, parse_expr/1]).
-export_type(['query'/0, exp_container/0, operator/0, comparator/0, time_slice_exp/0, exp/0, primary/0, word/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type 'query'() :: {'query', exp()}.
-type exp_container() :: {exp_container, exp()}.
-type operator() :: add | sub | mul | 'div'.
-type comparator() :: less_than |
less_than_or_equal_to |
greater_than |
greater_than_or_equal_to.
-type time_slice_exp() :: {time_slice_exp,
binary(),
comparator(),
float(),
float()}.
-type exp() :: {time_slice_expr, time_slice_exp()} |
{operator_expr, exp(), exp(), operator()} |
{primary, primary()}.
-type primary() :: {primary_word, word()} | {primary_exp, exp()}.
-type word() :: {word, binary()}.
-file("src/caffeine_query_language/parser.gleam", 233).
-spec find_substring_position_loop(binary(), binary(), integer()) -> gleam@option:option(integer()).
find_substring_position_loop(Haystack, Needle, Pos) ->
Needle_len = string:length(Needle),
Haystack_len = string:length(Haystack),
case (Pos + Needle_len) > Haystack_len of
true ->
none;
false ->
case gleam@string:slice(Haystack, Pos, Needle_len) =:= Needle of
true ->
{some, Pos};
false ->
find_substring_position_loop(Haystack, Needle, Pos + 1)
end
end.
-file("src/caffeine_query_language/parser.gleam", 229).
?DOC(" Finds the position of a substring in a string.\n").
-spec find_substring_position(binary(), binary()) -> gleam@option:option(integer()).
find_substring_position(Haystack, Needle) ->
find_substring_position_loop(Haystack, Needle, 0).
-file("src/caffeine_query_language/parser.gleam", 207).
-spec find_comparator_loop(binary(), list({binary(), comparator()})) -> {ok,
{binary(), comparator(), binary()}} |
{error, binary()}.
find_comparator_loop(Input, Comparators) ->
case Comparators of
[] ->
{error, <<"No comparator found in time_slice expression"/utf8>>};
[{Comp_str, Comp} | Rest] ->
case find_substring_position(Input, Comp_str) of
{some, Pos} ->
Query = gleam@string:slice(Input, 0, Pos),
Rest_start = Pos + string:length(Comp_str),
Rest_len = string:length(Input) - Rest_start,
Rest_str = gleam@string:slice(Input, Rest_start, Rest_len),
{ok, {Query, Comp, Rest_str}};
none ->
find_comparator_loop(Input, Rest)
end
end.
-file("src/caffeine_query_language/parser.gleam", 193).
?DOC(" Finds a comparator in the input and splits into (query, comparator, rest).\n").
-spec find_comparator(binary()) -> {ok, {binary(), comparator(), binary()}} |
{error, binary()}.
find_comparator(Input) ->
Comparators = [{<<">="/utf8>>, greater_than_or_equal_to},
{<<"<="/utf8>>, less_than_or_equal_to},
{<<">"/utf8>>, greater_than},
{<<"<"/utf8>>, less_than}],
find_comparator_loop(Input, Comparators).
-file("src/caffeine_query_language/parser.gleam", 253).
?DOC(" Splits on \"per\" keyword, returning (threshold_str, interval_str).\n").
-spec split_on_per(binary()) -> {ok, {binary(), binary()}} | {error, binary()}.
split_on_per(Input) ->
case find_substring_position(Input, <<"per"/utf8>>) of
{some, Pos} ->
Threshold_str = gleam@string:trim(gleam@string:slice(Input, 0, Pos)),
Rest_start = Pos + 3,
Rest_len = string:length(Input) - Rest_start,
Interval_str = gleam@string:trim(
gleam@string:slice(Input, Rest_start, Rest_len)
),
{ok, {Threshold_str, Interval_str}};
none ->
{error, <<"Missing 'per' keyword in time_slice expression"/utf8>>}
end.
-file("src/caffeine_query_language/parser.gleam", 288).
?DOC(" Parses an integer string as a float.\n").
-spec parse_int_as_float(binary()) -> {ok, float()} | {error, binary()}.
parse_int_as_float(Input) ->
case gleam_stdlib:contains_string(Input, <<"."/utf8>>) of
true ->
{error, <<"Not an integer"/utf8>>};
false ->
_pipe = gleam_stdlib:parse_float(<<Input/binary, ".0"/utf8>>),
gleam@result:map_error(
_pipe,
fun(_) -> <<"Invalid number"/utf8>> end
)
end.
-file("src/caffeine_query_language/parser.gleam", 268).
?DOC(" Parses a threshold value as a float.\n").
-spec parse_threshold(binary()) -> {ok, float()} | {error, binary()}.
parse_threshold(Input) ->
Trimmed = gleam@string:trim(Input),
case Trimmed of
<<""/utf8>> ->
{error, <<"Missing threshold in time_slice expression"/utf8>>};
_ ->
case gleam_stdlib:parse_float(Trimmed) of
{ok, F} ->
{ok, F};
{error, _} ->
case parse_int_as_float(Trimmed) of
{ok, F@1} ->
{ok, F@1};
{error, _} ->
{error,
<<<<"Invalid threshold '"/utf8, Trimmed/binary>>/binary,
"' in time_slice expression"/utf8>>}
end
end
end.
-file("src/caffeine_query_language/parser.gleam", 298).
?DOC(" Parses an interval like \"10s\", \"5m\", \"1h\", \"1.5h\" into seconds.\n").
-spec parse_interval(binary()) -> {ok, float()} | {error, binary()}.
parse_interval(Input) ->
Trimmed = gleam@string:trim(Input),
case Trimmed of
<<""/utf8>> ->
{error, <<"Missing interval in time_slice expression"/utf8>>};
_ ->
Len = string:length(Trimmed),
Unit = gleam@string:slice(Trimmed, Len - 1, 1),
Number_part = gleam@string:slice(Trimmed, 0, Len - 1),
gleam@result:'try'(case Unit of
<<"s"/utf8>> ->
{ok, 1.0};
<<"m"/utf8>> ->
{ok, 60.0};
<<"h"/utf8>> ->
{ok, 3600.0};
_ ->
{error,
<<<<"Invalid interval unit '"/utf8, Unit/binary>>/binary,
"' (expected s, m, or h)"/utf8>>}
end, fun(Multiplier) ->
gleam@result:'try'(
case gleam_stdlib:parse_float(Number_part) of
{ok, F} ->
{ok, F};
{error, _} ->
case parse_int_as_float(Number_part) of
{ok, F@1} ->
{ok, F@1};
{error, _} ->
{error,
<<<<"Invalid interval number '"/utf8,
Number_part/binary>>/binary,
"'"/utf8>>}
end
end,
fun(Number) -> {ok, Number * Multiplier} end
)
end)
end.
-file("src/caffeine_query_language/parser.gleam", 156).
?DOC(
" Parses the inner content of a time_slice expression.\n"
" Format: \"<query> <comparator> <threshold> per <interval>\"\n"
" Example: \"avg:system.cpu > 80 per 300s\"\n"
).
-spec parse_time_slice_spec(binary()) -> {ok, time_slice_exp()} |
{error, binary()}.
parse_time_slice_spec(Input) ->
Trimmed = gleam@string:trim(Input),
case Trimmed of
<<""/utf8>> ->
{error, <<"Empty time_slice expression"/utf8>>};
_ ->
gleam@result:'try'(
find_comparator(Trimmed),
fun(_use0) ->
{Query, Comparator, Rest} = _use0,
Query_trimmed = gleam@string:trim(Query),
case Query_trimmed of
<<""/utf8>> ->
{error,
<<"Missing query in time_slice expression"/utf8>>};
_ ->
gleam@result:'try'(
split_on_per(Rest),
fun(_use0@1) ->
{Threshold_str, Interval_str} = _use0@1,
gleam@result:'try'(
parse_threshold(Threshold_str),
fun(Threshold) ->
gleam@result:'try'(
parse_interval(Interval_str),
fun(Interval_seconds) ->
{ok,
{time_slice_exp,
Query_trimmed,
Comparator,
Threshold,
Interval_seconds}}
end
)
end
)
end
)
end
end
)
end.
-file("src/caffeine_query_language/parser.gleam", 136).
?DOC(
" Attempts to parse a keyword expression like \"time_slice(...)\".\n"
" Returns Error if the input is not a keyword expression.\n"
).
-spec try_parse_keyword_expr(binary()) -> {ok, exp()} | {error, binary()}.
try_parse_keyword_expr(Input) ->
case gleam_stdlib:string_starts_with(Input, <<"time_slice("/utf8>>) andalso gleam_stdlib:string_ends_with(
Input,
<<")"/utf8>>
) of
true ->
Prefix_len = string:length(<<"time_slice("/utf8>>),
Inner_len = (string:length(Input) - Prefix_len) - 1,
Inner = gleam@string:slice(Input, Prefix_len, Inner_len),
gleam@result:'try'(
parse_time_slice_spec(Inner),
fun(Spec) -> {ok, {time_slice_expr, Spec}} end
);
false ->
{error, <<"Not a keyword expression"/utf8>>}
end.
-file("src/caffeine_query_language/parser.gleam", 355).
?DOC(false).
-spec is_last_char(binary(), integer()) -> boolean().
is_last_char(Input, Pos) ->
Is_empty = gleam@string:is_empty(Input),
Is_last = Pos =:= (string:length(Input) - 1),
Is_empty orelse Is_last.
-file("src/caffeine_query_language/parser.gleam", 409).
-spec count_parens(integer(), binary(), integer()) -> integer().
count_parens(Cur_count, Input, Pos) ->
Char = gleam@string:slice(Input, Pos, 1),
case Char of
<<"("/utf8>> ->
Cur_count + 1;
<<")"/utf8>> ->
Cur_count - 1;
_ ->
Cur_count
end.
-file("src/caffeine_query_language/parser.gleam", 340).
?DOC(false).
-spec is_balanced_parens(binary(), integer(), integer()) -> boolean().
is_balanced_parens(Input, Pos, Count) ->
case Pos >= string:length(Input) of
true ->
Count =:= 0;
false ->
New_count = count_parens(Count, Input, Pos),
Does_not_close_too_early = not ((New_count =:= 0) andalso not is_last_char(
Input,
Pos
)),
Does_not_close_too_early andalso is_balanced_parens(
Input,
Pos + 1,
New_count
)
end.
-file("src/caffeine_query_language/parser.gleam", 91).
-spec is_fully_parenthesized(binary()) -> boolean().
is_fully_parenthesized(Input) ->
(gleam_stdlib:string_starts_with(Input, <<"("/utf8>>) andalso gleam_stdlib:string_ends_with(
Input,
<<")"/utf8>>
))
andalso ((string:length(Input) >= 2) andalso is_balanced_parens(Input, 1, 1)).
-file("src/caffeine_query_language/parser.gleam", 365).
?DOC(false).
-spec find_rightmost_operator_at_level(
binary(),
binary(),
integer(),
integer(),
integer()
) -> {ok, {binary(), binary()}} |
{error, caffeine_lang@common@errors:compilation_error()}.
find_rightmost_operator_at_level(
Input,
Operator,
Start_pos,
Paren_level,
Rightmost_pos
) ->
Operator_length = string:length(Operator),
case Start_pos >= string:length(Input) of
true ->
case Rightmost_pos of
-1 ->
{error, {c_q_l_parser_error, <<"Operator not found"/utf8>>}};
Pos ->
Left = gleam@string:trim(gleam@string:slice(Input, 0, Pos)),
Right_start = Pos + Operator_length,
Right_length = string:length(Input) - Right_start,
Right = gleam@string:trim(
gleam@string:slice(Input, Right_start, Right_length)
),
{ok, {Left, Right}}
end;
false ->
New_paren_level = count_parens(Paren_level, Input, Start_pos),
New_rightmost_pos = case (New_paren_level =:= 0) andalso (gleam@string:slice(
Input,
Start_pos,
Operator_length
)
=:= Operator) of
true ->
Start_pos;
false ->
Rightmost_pos
end,
find_rightmost_operator_at_level(
Input,
Operator,
Start_pos + 1,
New_paren_level,
New_rightmost_pos
)
end.
-file("src/caffeine_query_language/parser.gleam", 330).
-spec find_operator(binary(), binary()) -> {ok, {binary(), binary()}} |
{error, caffeine_lang@common@errors:compilation_error()}.
find_operator(Input, Operator) ->
find_rightmost_operator_at_level(Input, Operator, 0, 0, -1).
-file("src/caffeine_query_language/parser.gleam", 97).
-spec try_operators(binary(), list({binary(), operator()})) -> {ok, exp()} |
{error, binary()}.
try_operators(Input, Operators) ->
case Operators of
[] ->
case try_parse_keyword_expr(Input) of
{ok, Exp} ->
{ok, Exp};
{error, Err} ->
case gleam_stdlib:string_starts_with(
Input,
<<"time_slice("/utf8>>
)
andalso gleam_stdlib:string_ends_with(Input, <<")"/utf8>>) of
true ->
{error, Err};
false ->
Word = {word, Input},
{ok, {primary, {primary_word, Word}}}
end
end;
[{Op_str, Op} | Rest] ->
case find_operator(Input, Op_str) of
{ok, {Left, Right}} ->
gleam@result:'try'(
do_parse_expr(Left),
fun(Left_exp) ->
gleam@result:'try'(
do_parse_expr(Right),
fun(Right_exp) ->
{ok,
{operator_expr, Left_exp, Right_exp, Op}}
end
)
end
);
{error, _} ->
try_operators(Input, Rest)
end
end.
-file("src/caffeine_query_language/parser.gleam", 75).
?DOC(
" Parses a CQL expression string into an Exp AST node.\n"
" Handles parenthesized expressions and operator precedence.\n"
).
-spec do_parse_expr(binary()) -> {ok, exp()} | {error, binary()}.
do_parse_expr(Input) ->
Trimmed = gleam@string:trim(Input),
case is_fully_parenthesized(Trimmed) of
true ->
Inner = gleam@string:slice(Trimmed, 1, string:length(Trimmed) - 2),
gleam@result:'try'(
do_parse_expr(Inner),
fun(Inner_exp) -> {ok, {primary, {primary_exp, Inner_exp}}} end
);
false ->
Operators = [{<<"+"/utf8>>, add},
{<<"-"/utf8>>, sub},
{<<"*"/utf8>>, mul},
{<<"/"/utf8>>, 'div'}],
try_operators(Trimmed, Operators)
end.
-file("src/caffeine_query_language/parser.gleam", 68).
?DOC(false).
-spec parse_expr(binary()) -> {ok, exp_container()} | {error, binary()}.
parse_expr(Input) ->
gleam@result:'try'(
do_parse_expr(Input),
fun(Exp) -> {ok, {exp_container, Exp}} end
).