Current section

Files

Jump to
caffeine_lang src caffeine_query_language generator.gleam
Raw

src/caffeine_query_language/generator.gleam

import caffeine_query_language/ast.{
type Exp, type Substituted, OperatorExpr, Primary, PrimaryExp, PrimaryWord,
TimeSliceExp, Word,
}
import caffeine_query_language/parser
import caffeine_query_language/printer
import caffeine_query_language/resolver
import gleam/dict
import gleam/list
import gleam/result
import gleam/set.{type Set}
import gleam/string
/// Transform an expression tree by substituting word values using a dictionary.
/// Words found in the dictionary are replaced with their corresponding values.
/// Words not found in the dictionary are left unchanged.
@internal
pub fn substitute_words(
exp: Exp(a),
substitutions: dict.Dict(String, String),
) -> Exp(Substituted) {
case exp {
Primary(PrimaryWord(Word(name))) -> {
let value = dict.get(substitutions, name) |> result.unwrap(name)
Primary(PrimaryWord(Word(value)))
}
Primary(PrimaryExp(inner)) ->
Primary(PrimaryExp(substitute_words(inner, substitutions)))
ast.TimeSliceExpr(spec) -> {
let query =
dict.get(substitutions, spec.query) |> result.unwrap(spec.query)
ast.TimeSliceExpr(TimeSliceExp(..spec, query: query))
}
OperatorExpr(left, right, op) ->
OperatorExpr(
substitute_words(left, substitutions),
substitute_words(right, substitutions),
op,
)
}
}
/// Extracts all word names from an expression AST.
/// Returns a list of unique word strings found in the expression.
@internal
pub fn extract_words(exp: Exp(a)) -> List(String) {
extract_words_loop(exp, set.new())
|> set.to_list
|> list.sort(string.compare)
}
/// Accumulates unique word names into a Set.
fn extract_words_loop(exp: Exp(a), acc: Set(String)) -> Set(String) {
case exp {
Primary(PrimaryWord(Word(name))) -> set.insert(acc, name)
Primary(PrimaryExp(inner)) -> extract_words_loop(inner, acc)
ast.TimeSliceExpr(_) -> acc
OperatorExpr(left, right, _) ->
extract_words_loop(right, extract_words_loop(left, acc))
}
}
/// Parse a value expression, resolve it, substitute indicator names,
/// and return the resulting expression as a plain string.
/// Handles identity expressions (single words, compositions) and good-over-total divisions.
/// Rejects time_slice expressions (not valid for expression-based resolution).
@internal
pub fn resolve_slo_to_expression(
value_expr: String,
substitutions: dict.Dict(String, String),
) -> Result(String, String) {
use parsed <- result.try(
parser.parse_expr(value_expr)
|> result.map_error(fn(err) { "Parse error: " <> err }),
)
let exp = case resolver.resolve_primitives(parsed) {
Ok(resolver.GoodOverTotal(num, den)) -> Ok(OperatorExpr(num, den, ast.Div))
Ok(resolver.TimeSlice(..)) ->
Error(
"time_slice expressions are not supported for expression resolution",
)
// Not a division or time_slice — treat as direct expression (identity/composition).
Error(_) -> Ok(parsed)
}
use exp <- result.try(exp)
use <- validate_words_exist(exp, substitutions)
Ok(substitute_words(exp, substitutions) |> printer.exp_to_string)
}
/// Validate that all words in an expression exist in the substitutions dict.
/// Returns an error listing any missing indicator names.
fn validate_words_exist(
exp: Exp(a),
substitutions: dict.Dict(String, String),
next: fn() -> Result(String, String),
) -> Result(String, String) {
let missing =
extract_words(exp)
|> list.filter(fn(word) {
case dict.get(substitutions, word) {
Ok(_) -> False
Error(_) -> True
}
})
case missing {
[] -> next()
_ ->
Error(
"evaluation references undefined indicators: "
<> string.join(missing, ", "),
)
}
}