Packages
caffeine_lang
4.6.5
6.3.1
6.3.0
6.2.2
6.2.1
6.2.0
6.1.2
6.1.1
6.1.0
6.0.0
5.6.0
5.5.0
5.4.4
5.4.3
5.4.2
5.4.1
5.4.0
5.3.0
5.2.0
5.1.1
5.1.0
5.0.12
5.0.11
5.0.10
5.0.8
5.0.7
5.0.6
5.0.5
5.0.4
5.0.1
5.0.0
4.10.0
4.9.0
4.8.3
4.8.2
4.8.1
4.8.0
4.7.9
4.7.8
4.7.7
4.7.6
4.7.5
4.6.7
4.6.6
4.6.5
4.6.4
4.6.3
4.6.2
4.6.0
4.5.1
4.5.0
4.4.4
4.4.3
4.4.1
4.4.0
4.3.7
4.3.6
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.0.2
1.0.1
0.1.0
0.0.24
0.0.23
0.0.22
0.0.21
0.0.20
0.0.19
0.0.18
0.0.17
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.2
0.0.1
A compiler for generating reliability artifacts from service expectation definitions.
Current section
Files
Jump to
Current section
Files
src/caffeine_query_language/parser.gleam
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: "<query> <comparator> <threshold> per <interval>"
/// 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
}
}