-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 ).