import caffeine_lang/errors.{type CompilationError} import caffeine_query_language/ast.{ type Comparator, type CqlParsed, type Exp, type Operator, type TimeSliceExp, Add, Div, GreaterThan, GreaterThanOrEqualTo, LessThan, LessThanOrEqualTo, Mul, OperatorExpr, Primary, PrimaryExp, PrimaryWord, Sub, TimeSliceExp, TimeSliceExpr, Word, } import gleam/bool import gleam/float import gleam/option.{type Option} import gleam/result import gleam/string /// Parses a CQL expression string into an Exp AST node. /// Returns an error if the input cannot be parsed. @internal pub fn parse_expr(input: String) -> Result(Exp(CqlParsed), String) { let trimmed = string.trim(input) case is_fully_parenthesized(trimmed) { True -> { let inner = string.slice(trimmed, 1, string.length(trimmed) - 2) use inner_exp <- result.try(parse_expr(inner)) Ok(Primary(PrimaryExp(inner_exp))) } False -> { let operators = [#("+", Add), #("-", Sub), #("*", Mul), #("/", Div)] try_operators(trimmed, operators) } } } fn is_fully_parenthesized(input: String) -> Bool { string.starts_with(input, "(") && string.ends_with(input, ")") && { string.length(input) >= 2 && is_balanced_parens(input, 1, 1) } } fn try_operators( input: String, operators: List(#(String, Operator)), ) -> Result(Exp(CqlParsed), String) { case operators { [] -> { case try_parse_keyword_expr(input) { Ok(exp) -> Ok(exp) Error(err) -> { use <- bool.guard( when: string.starts_with(input, "time_slice(") && string.ends_with(input, ")"), return: Error(err), ) let word = Word(input) Ok(Primary(PrimaryWord(word))) } } } [#(op_str, op), ..rest] -> { case find_operator(input, op_str) { Ok(#(left, right)) -> { use left_exp <- result.try(parse_expr(left)) use right_exp <- result.try(parse_expr(right)) Ok(OperatorExpr(left_exp, right_exp, op)) } Error(_) -> try_operators(input, rest) } } } } /// Attempts to parse a keyword expression like "time_slice(...)". /// Returns Error if the input is not a keyword expression. fn try_parse_keyword_expr(input: String) -> Result(Exp(CqlParsed), String) { use <- bool.guard( when: !{ string.starts_with(input, "time_slice(") && string.ends_with(input, ")") }, return: Error("Not a keyword expression"), ) let prefix_len = string.length("time_slice(") let inner_len = string.length(input) - prefix_len - 1 let inner = string.slice(input, prefix_len, inner_len) use spec <- result.try(parse_time_slice_spec(inner)) Ok(TimeSliceExpr(spec)) } /// Parses the inner content of a time_slice expression. /// Format: " per " /// Example: "avg:system.cpu > 80 per 300s" fn parse_time_slice_spec(input: String) -> Result(TimeSliceExp, String) { let trimmed = string.trim(input) case trimmed { "" -> Error("Empty time_slice expression") _ -> { use #(query, comparator, rest) <- result.try(find_comparator(trimmed)) let query_trimmed = string.trim(query) case query_trimmed { "" -> Error("Missing query in time_slice expression") _ -> { use #(threshold_str, interval_str) <- result.try(split_on_per(rest)) use threshold <- result.try(parse_threshold(threshold_str)) use interval_seconds <- result.try(parse_interval(interval_str)) Ok(TimeSliceExp( query: query_trimmed, comparator: comparator, threshold: threshold, interval_seconds: interval_seconds, )) } } } } } /// Finds a comparator in the input and splits into (query, comparator, rest). fn find_comparator( input: String, ) -> Result(#(String, Comparator, String), String) { let comparators = [ #(">=", GreaterThanOrEqualTo), #("<=", LessThanOrEqualTo), #(">", GreaterThan), #("<", LessThan), ] find_comparator_loop(input, comparators) } fn find_comparator_loop( input: String, comparators: List(#(String, Comparator)), ) -> Result(#(String, Comparator, String), String) { case comparators { [] -> Error("No comparator found in time_slice expression") [#(comp_str, comp), ..rest] -> { case find_substring_position(input, comp_str) { option.Some(pos) -> { let query = string.slice(input, 0, pos) let rest_start = pos + string.length(comp_str) let rest_len = string.length(input) - rest_start let rest_str = string.slice(input, rest_start, rest_len) Ok(#(query, comp, rest_str)) } option.None -> find_comparator_loop(input, rest) } } } } /// Finds the position of a substring in a string. fn find_substring_position(haystack: String, needle: String) -> Option(Int) { find_substring_position_loop(haystack, needle, 0) } fn find_substring_position_loop( haystack: String, needle: String, pos: Int, ) -> Option(Int) { let needle_len = string.length(needle) let haystack_len = string.length(haystack) use <- bool.guard(when: pos + needle_len > haystack_len, return: option.None) use <- bool.guard( when: string.slice(haystack, pos, needle_len) == needle, return: option.Some(pos), ) find_substring_position_loop(haystack, needle, pos + 1) } /// Splits on "per" keyword, returning (threshold_str, interval_str). fn split_on_per(input: String) -> Result(#(String, String), String) { case find_substring_position(input, "per") { option.Some(pos) -> { let threshold_str = string.trim(string.slice(input, 0, pos)) let rest_start = pos + 3 let rest_len = string.length(input) - rest_start let interval_str = string.trim(string.slice(input, rest_start, rest_len)) Ok(#(threshold_str, interval_str)) } option.None -> Error("Missing 'per' keyword in time_slice expression") } } /// Parses a threshold value as a float. fn parse_threshold(input: String) -> Result(Float, String) { let trimmed = string.trim(input) case trimmed { "" -> Error("Missing threshold in time_slice expression") _ -> case float.parse(trimmed) { Ok(f) -> Ok(f) Error(_) -> case parse_int_as_float(trimmed) { Ok(f) -> Ok(f) Error(_) -> Error( "Invalid threshold '" <> trimmed <> "' in time_slice expression", ) } } } } /// Parses an integer string as a float. fn parse_int_as_float(input: String) -> Result(Float, String) { use <- bool.guard( when: string.contains(input, "."), return: Error("Not an integer"), ) float.parse(input <> ".0") |> result.map_error(fn(_) { "Invalid number" }) } /// Parses an interval like "10s", "5m", "1h", "1.5h" into seconds. fn parse_interval(input: String) -> Result(Float, String) { let trimmed = string.trim(input) case trimmed { "" -> Error("Missing interval in time_slice expression") _ -> { let len = string.length(trimmed) let unit = string.slice(trimmed, len - 1, 1) let number_part = string.slice(trimmed, 0, len - 1) use multiplier <- result.try(case unit { "s" -> Ok(1.0) "m" -> Ok(60.0) "h" -> Ok(3600.0) _ -> Error("Invalid interval unit '" <> unit <> "' (expected s, m, or h)") }) use number <- result.try(case float.parse(number_part) { Ok(f) -> Ok(f) Error(_) -> case parse_int_as_float(number_part) { Ok(f) -> Ok(f) Error(_) -> Error("Invalid interval number '" <> number_part <> "'") } }) Ok(number *. multiplier) } } } fn find_operator( input: String, operator: String, ) -> Result(#(String, String), CompilationError) { find_rightmost_operator_at_level(input, operator, 0, 0, -1) } /// Checks if parentheses are balanced in the input string starting from a position. /// Used to validate parenthesized expressions during parsing. @internal pub fn is_balanced_parens(input: String, pos: Int, count: Int) -> Bool { use <- bool.guard(when: pos >= string.length(input), return: count == 0) let new_count = count_parens(count, input, pos) let does_not_close_too_early = !{ { new_count == 0 } && !is_last_char(input, pos) } does_not_close_too_early && is_balanced_parens(input, pos + 1, new_count) } /// Returns true if the position is at the last character of the input string. @internal pub fn is_last_char(input: String, pos: Int) -> Bool { let is_empty = string.is_empty(input) let is_last = pos == string.length(input) - 1 is_empty || is_last } /// Finds the rightmost occurrence of an operator at parenthesis level 0. /// Returns the left and right parts of the expression split at the operator. @internal pub fn find_rightmost_operator_at_level( input: String, operator: String, start_pos: Int, paren_level: Int, rightmost_pos: Int, ) -> Result(#(String, String), CompilationError) { let operator_length = string.length(operator) case start_pos >= string.length(input) { True -> case rightmost_pos { -1 -> Error(errors.cql_parser_error(msg: "Operator not found")) pos -> { let left = string.trim(string.slice(input, 0, pos)) let right_start = pos + operator_length let right_length = string.length(input) - right_start let right = string.trim(string.slice(input, right_start, right_length)) Ok(#(left, right)) } } False -> { let new_paren_level = count_parens(paren_level, input, start_pos) let new_rightmost_pos = case new_paren_level == 0 && string.slice(input, start_pos, operator_length) == operator { True -> start_pos False -> rightmost_pos } find_rightmost_operator_at_level( input, operator, start_pos + 1, new_paren_level, new_rightmost_pos, ) } } } fn count_parens(cur_count: Int, input: String, pos: Int) -> Int { let char = string.slice(input, pos, 1) case char { "(" -> cur_count + 1 ")" -> cur_count - 1 _ -> cur_count } }