Current section

Files

Jump to
caffeine_lang src caffeine_lang@cql@parser.erl
Raw

src/caffeine_lang@cql@parser.erl

-module(caffeine_lang@cql@parser).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/caffeine_lang/cql/parser.gleam").
-export([debug_exp/1, is_balanced_parens/3, do_parse_expr/1, parse_expr/1]).
-export_type(['query'/0, exp_container/0, operator/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 exp() :: {operator_expr, exp(), exp(), operator()} | {primary, primary()}.
-type primary() :: {primary_word, word()} | {primary_exp, exp()}.
-type word() :: {word, binary()}.
-file("src/caffeine_lang/cql/parser.gleam", 39).
?DOC(
" Pretty print an expression tree for debugging.\n"
" Useful for visualizing the AST structure.\n"
).
-spec debug_exp(exp()) -> binary().
debug_exp(Exp) ->
case Exp of
{operator_expr, Left, Right, Op} ->
Op_str = case Op of
add ->
<<"+"/utf8>>;
sub ->
<<"-"/utf8>>;
mul ->
<<"*"/utf8>>;
'div' ->
<<"/"/utf8>>
end,
<<<<<<<<<<<<"("/utf8, (debug_exp(Left))/binary>>/binary, " "/utf8>>/binary,
Op_str/binary>>/binary,
" "/utf8>>/binary,
(debug_exp(Right))/binary>>/binary,
")"/utf8>>;
{primary, {primary_word, {word, Value}}} ->
Value;
{primary, {primary_exp, Inner}} ->
<<<<"("/utf8, (debug_exp(Inner))/binary>>/binary, ")"/utf8>>
end.
-file("src/caffeine_lang/cql/parser.gleam", 169).
?DOC(
" Check if parentheses are balanced from position `pos` with initial `count`.\n"
" \n"
" Args:\n"
" - input: The string to check\n"
" - pos: Starting position (0-indexed)\n"
" - count: Initial parenthesis depth (1 means we're inside one open paren)\n"
" \n"
" Returns True only if:\n"
" 1. Count reaches exactly 0 at the end of the string\n"
" 2. Count never reaches 0 before the end (no premature closing)\n"
" \n"
" This ensures that for \"(A + B)\", starting at pos=1 with count=1,\n"
" we verify the closing ')' is at the very end.\n"
).
-spec is_balanced_parens(binary(), integer(), integer()) -> boolean().
is_balanced_parens(Input, Pos, Count) ->
case Pos >= string:length(Input) of
true ->
Count =:= 0;
false ->
Char = gleam@string:slice(Input, Pos, 1),
New_count = case Char of
<<"("/utf8>> ->
Count + 1;
<<")"/utf8>> ->
Count - 1;
_ ->
Count
end,
case (New_count =:= 0) andalso (Pos < (string:length(Input) - 1)) of
true ->
false;
false ->
is_balanced_parens(Input, Pos + 1, New_count)
end
end.
-file("src/caffeine_lang/cql/parser.gleam", 105).
?DOC(
" Check if expression is fully wrapped in balanced parentheses.\n"
" \n"
" Returns True only if:\n"
" 1. Starts with '(' and ends with ')'\n"
" 2. The opening '(' matches the closing ')' (not an inner pair)\n"
" \n"
" Examples:\n"
" - \"(A + B)\" -> True\n"
" - \"(A) + (B)\" -> False (not fully wrapped)\n"
" - \"((A + B))\" -> True\n"
).
-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_lang/cql/parser.gleam", 204).
?DOC(
" Find rightmost occurrence of operator at parenthesis level 0.\n"
" \n"
" Args:\n"
" - input: Expression string to search\n"
" - operator: Operator string to find (e.g., \"+\", \"-\")\n"
" - start_pos: Current search position\n"
" - paren_level: Current parenthesis nesting depth\n"
" - rightmost_pos: Position of rightmost match found so far (-1 if none)\n"
" \n"
" Algorithm:\n"
" 1. Scan left-to-right, tracking parenthesis depth\n"
" 2. Record position whenever we find operator at level 0\n"
" 3. Return rightmost match (for left-associativity)\n"
" \n"
" Example: \"A + B + C\" finds the second '+' at level 0\n"
).
-spec find_rightmost_operator_at_level(
binary(),
binary(),
integer(),
integer(),
integer()
) -> {ok, {binary(), binary()}} | {error, binary()}.
find_rightmost_operator_at_level(
Input,
Operator,
Start_pos,
Paren_level,
Rightmost_pos
) ->
case Start_pos >= string:length(Input) of
true ->
case Rightmost_pos of
-1 ->
{error, <<"Operator not found"/utf8>>};
Pos ->
Left = gleam@string:trim(gleam@string:slice(Input, 0, Pos)),
Right_start = Pos + string:length(Operator),
Right_length = string:length(Input) - Right_start,
Right = gleam@string:trim(
gleam@string:slice(Input, Right_start, Right_length)
),
{ok, {Left, Right}}
end;
false ->
Char = gleam@string:slice(Input, Start_pos, 1),
New_paren_level = case Char of
<<"("/utf8>> ->
Paren_level + 1;
<<")"/utf8>> ->
Paren_level - 1;
_ ->
Paren_level
end,
Is_target_operator = (Paren_level =:= 0) andalso (gleam@string:slice(
Input,
Start_pos,
string:length(Operator)
)
=:= Operator),
New_rightmost_pos = case Is_target_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_lang/cql/parser.gleam", 149).
?DOC(
" Find the rightmost occurrence of operator at parenthesis level 0.\n"
" \n"
" We search for rightmost to handle left-associativity correctly.\n"
" Example: \"A + B + C\" should parse as \"(A + B) + C\"\n"
).
-spec find_operator(binary(), binary()) -> {ok, {binary(), binary()}} |
{error, binary()}.
find_operator(Input, Operator) ->
find_rightmost_operator_at_level(Input, Operator, 0, 0, -1).
-file("src/caffeine_lang/cql/parser.gleam", 119).
?DOC(
" Try to split expression by operators in order.\n"
" \n"
" Strategy:\n"
" 1. Try each operator in sequence (lowest precedence first)\n"
" 2. If operator found at top level, split and recursively parse both sides\n"
" 3. If no operator found, treat entire input as a word (leaf node)\n"
" \n"
" This builds the AST with correct precedence structure.\n"
).
-spec try_operators(binary(), list({binary(), operator()})) -> {ok, exp()} |
{error, binary()}.
try_operators(Input, Operators) ->
case Operators of
[] ->
Word = {word, Input},
{ok, {primary, {primary_word, Word}}};
[{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_lang/cql/parser.gleam", 76).
?DOC(
" Core parsing logic. Handles two cases:\n"
" 1. Fully parenthesized expressions: (expr) -> unwrap and parse inner\n"
" 2. Non-parenthesized: try to split by operators in precedence order\n"
" \n"
" Operator precedence (lowest to highest): +, -, *, /\n"
" We search for lowest precedence first to build correct AST structure.\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_lang/cql/parser.gleam", 65).
?DOC(
" Parse an expression string into an ExpContainer.\n"
" \n"
" Examples:\n"
" - \"A + B\" -> Addition of A and B\n"
" - \"(A + B) / C\" -> Division with parenthesized addition\n"
" - \"A * B + C / D - E\" -> Mixed operators with precedence\n"
).
-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
).