Current section

Files

Jump to
caffeine_lang src caffeine_query_language printer.gleam
Raw

src/caffeine_query_language/printer.gleam

import caffeine_query_language/ast.{
type Comparator, type Exp, type Operator, type Primary,
}
import gleam/float
import gleam/int
import gleam/option.{type Option}
import gleam/string
/// Converts an expression AST node to its string representation.
@internal
pub fn exp_to_string(exp: Exp(a)) -> String {
case exp {
ast.Primary(primary:) -> primary_to_string(primary, option.None)
ast.TimeSliceExpr(spec) ->
"time_slice("
<> spec.query
<> " "
<> comparator_to_string(spec.comparator)
<> " "
<> float_to_string(spec.threshold)
<> " per "
<> float_to_string(spec.interval_seconds)
<> "s)"
ast.OperatorExpr(numerator:, denominator:, operator:) -> {
case operator, is_path_expression(exp) {
ast.Div, True -> {
exp_to_string_no_spaces(exp)
}
_, _ -> {
let left =
exp_to_string_with_context(numerator, option.Some(operator), True)
let right =
exp_to_string_with_context(
denominator,
option.Some(operator),
False,
)
let op = operator_to_string(operator)
left <> " " <> op <> " " <> right
}
}
}
}
}
/// Converts a CQL comparator to its string representation.
fn comparator_to_string(comparator: Comparator) -> String {
case comparator {
ast.LessThan -> "<"
ast.LessThanOrEqualTo -> "<="
ast.GreaterThan -> ">"
ast.GreaterThanOrEqualTo -> ">="
}
}
/// Converts a CQL operator to its string representation.
@internal
pub fn operator_to_string(operator: Operator) -> String {
case operator {
ast.Add -> "+"
ast.Sub -> "-"
ast.Mul -> "*"
ast.Div -> "/"
}
}
fn float_to_string(f: Float) -> String {
let truncated = float.truncate(f)
let is_whole = int.to_float(truncated) == f
case is_whole {
True -> int.to_string(truncated)
False -> float.to_string(f)
}
}
fn is_path_expression(exp: Exp(a)) -> Bool {
case get_leftmost_word(exp) {
option.Some(w) -> {
case string.ends_with(w, ":") {
True -> all_divisions(exp)
False -> False
}
}
option.None -> False
}
}
fn get_leftmost_word(exp: Exp(a)) -> Option(String) {
case exp {
ast.Primary(ast.PrimaryWord(ast.Word(w))) -> option.Some(w)
ast.Primary(ast.PrimaryExp(inner_exp)) -> get_leftmost_word(inner_exp)
ast.TimeSliceExpr(_) -> option.None
ast.OperatorExpr(left, _, _) -> get_leftmost_word(left)
}
}
fn all_divisions(exp: Exp(a)) -> Bool {
case exp {
ast.Primary(_) -> True
ast.OperatorExpr(left, right, ast.Div) ->
all_divisions(left) && all_divisions(right)
_ -> False
}
}
fn exp_to_string_no_spaces(exp: Exp(a)) -> String {
case exp {
ast.Primary(ast.PrimaryWord(ast.Word(w))) -> w
ast.OperatorExpr(left, right, ast.Div) ->
exp_to_string_no_spaces(left) <> "/" <> exp_to_string_no_spaces(right)
_ -> exp_to_string(exp)
}
}
fn exp_to_string_with_context(
exp: Exp(a),
parent_op: Option(Operator),
_is_left: Bool,
) -> String {
case exp {
ast.Primary(primary:) -> primary_to_string(primary, parent_op)
ast.TimeSliceExpr(_) -> exp_to_string(exp)
ast.OperatorExpr(numerator:, denominator:, operator:) -> {
case operator, is_path_expression(exp) {
ast.Div, True -> exp_to_string_no_spaces(exp)
_, _ -> {
let left =
exp_to_string_with_context(numerator, option.Some(operator), True)
let right =
exp_to_string_with_context(
denominator,
option.Some(operator),
False,
)
let op = operator_to_string(operator)
left <> " " <> op <> " " <> right
}
}
}
}
}
fn primary_to_string(
primary: Primary(a),
_parent_op: Option(Operator),
) -> String {
case primary {
ast.PrimaryWord(word:) -> word.value
ast.PrimaryExp(exp:) -> {
"(" <> exp_to_string(exp) <> ")"
}
}
}
/// Strips outer parentheses from a string if they wrap the entire expression.
/// E.g., "(a + b)" -> "a + b", but "(a + b) * c" stays unchanged.
@internal
pub fn strip_outer_parens(s: String) -> String {
let trimmed = string.trim(s)
case string.starts_with(trimmed, "(") && string.ends_with(trimmed, ")") {
True -> {
let inner = string.slice(trimmed, 1, string.length(trimmed) - 2)
case is_balanced(inner, 0) {
True -> inner
False -> trimmed
}
}
False -> trimmed
}
}
fn is_balanced(s: String, depth: Int) -> Bool {
case string.pop_grapheme(s) {
Error(_) -> depth == 0
Ok(#("(", rest)) -> is_balanced(rest, depth + 1)
Ok(#(")", rest)) ->
case depth {
0 -> False
_ -> is_balanced(rest, depth - 1)
}
Ok(#(_, rest)) -> is_balanced(rest, depth)
}
}