Packages
caffeine_lang
1.0.1
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/common/errors.{type CompilationError, CQLParserError}
import gleam/float
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
// ===== Types =====
/// A complete query containing a single expression.
pub type Query {
Query(exp: Exp)
}
/// Container for a parsed expression, used as the top-level parse result.
pub type ExpContainer {
ExpContainer(exp: Exp)
}
/// Arithmetic operators supported in CQL expressions.
pub type Operator {
Add
Sub
Mul
Div
}
/// Comparators for time slice expressions.
pub type Comparator {
LessThan
LessThanOrEqualTo
GreaterThan
GreaterThanOrEqualTo
}
/// Time slice specification containing query, comparator, threshold, and interval.
pub type TimeSliceExp {
TimeSliceExp(
query: String,
comparator: Comparator,
threshold: Float,
interval_seconds: Float,
)
}
/// An expression in the CQL AST, either an operator expression or a primary.
pub type Exp {
TimeSliceExpr(spec: TimeSliceExp)
OperatorExpr(numerator: Exp, denominator: Exp, operator: Operator)
Primary(primary: Primary)
}
/// A primary expression, either a word (identifier) or a parenthesized expression.
pub type Primary {
PrimaryWord(word: Word)
PrimaryExp(exp: Exp)
}
/// A word (identifier) in the expression.
pub type Word {
Word(value: String)
}
//==========================================
/// Parses a CQL expression string into an ExpContainer.
/// Returns an error if the input cannot be parsed.
pub fn parse_expr(input: String) -> Result(ExpContainer, String) {
use exp <- result.try(do_parse_expr(input))
Ok(ExpContainer(exp))
}
/// Parses a CQL expression string into an Exp AST node.
/// Handles parenthesized expressions and operator precedence.
fn do_parse_expr(input: String) -> Result(Exp, 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(do_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, String) {
case operators {
[] -> {
// No operators found, check if this is a keyword expression
case try_parse_keyword_expr(input) {
Ok(exp) -> Ok(exp)
Error(err) -> {
// If it looks like a time_slice but has invalid syntax, propagate error
case
string.starts_with(input, "time_slice(")
&& string.ends_with(input, ")")
{
True -> Error(err)
False -> {
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(do_parse_expr(left))
use right_exp <- result.try(do_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, String) {
// Check for time_slice keyword
case string.starts_with(input, "time_slice(") && string.ends_with(input, ")")
{
True -> {
// Extract the inner content (everything between "time_slice(" and ")")
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))
}
False -> Error("Not a keyword expression")
}
}
/// 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)
// Check for empty input
case trimmed {
"" -> Error("Empty time_slice expression")
_ -> {
// Find the comparator and split on it
use #(query, comparator, rest) <- result.try(find_comparator(trimmed))
// Validate query is not empty
let query_trimmed = string.trim(query)
case query_trimmed {
"" -> Error("Missing query in time_slice expression")
_ -> {
// Find "per" keyword and split threshold from interval
use #(threshold_str, interval_str) <- result.try(split_on_per(rest))
// Parse threshold as float
use threshold <- result.try(parse_threshold(threshold_str))
// Parse interval (e.g., "10s", "5m", "1h", "1.5h")
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) {
// Try comparators in order (longer ones first to avoid partial matches)
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) {
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))
}
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)
case pos + needle_len > haystack_len {
True -> None
False -> {
case string.slice(haystack, pos, needle_len) == needle {
True -> Some(pos)
False -> 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") {
Some(pos) -> {
let threshold_str = string.trim(string.slice(input, 0, pos))
let rest_start = pos + 3
// "per" is 3 chars
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))
}
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) {
case string.contains(input, ".") {
True -> Error("Not an integer")
False ->
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")
_ -> {
// Get the unit (last character)
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 {
case pos >= string.length(input) {
True -> count == 0
False -> {
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(CQLParserError(msg: "Operator not found"))
pos -> {
// Split at the rightmost operator position
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
}
}