Packages
caffeine_lang
6.1.2
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_lang/analysis/dependency_validator.gleam
import caffeine_lang/errors.{type CompilationError}
import caffeine_lang/frontend/ast
import caffeine_lang/linker/dependency.{Hard}
import caffeine_lang/linker/ir.{
type IntermediateRepresentation, ir_to_identifier,
}
import gleam/bool
import gleam/dict.{type Dict}
import gleam/float
import gleam/int
import gleam/list
import gleam/option.{type Option}
import gleam/order
import gleam/result
import gleam/set.{type Set}
import gleam/string
/// Extracts depends_on from an IR's SloFields.
fn get_depends_on(
ir: IntermediateRepresentation(phase),
) -> Option(Dict(dependency.DependencyRelationType, List(String))) {
ir.slo.depends_on
}
/// Validates that all dependency relations reference existing expectations.
///
/// Dependencies must be in the format "org.team.service.name" and must:
/// - Reference an expectation that exists in the compilation
/// - Not reference the expectation itself (no self-references)
/// - Not form circular dependency chains
/// - Satisfy composite hard dependency threshold constraints
@internal
pub fn validate_dependency_relations(
irs: List(IntermediateRepresentation(ir.Linked)),
) -> Result(
List(IntermediateRepresentation(ir.DepsValidated)),
CompilationError,
) {
// Build an index of all valid expectation paths
let expectation_index = build_expectation_index(irs)
// Validate each IR that has depends_on (accumulate all errors)
use _ <- result.try(
irs
|> list.map(fn(ir) { validate_ir_dependencies(ir, expectation_index) })
|> errors.from_results()
|> result.map(fn(_) { Nil }),
)
// Detect circular dependencies
use _ <- result.try(detect_cycles(irs))
// Validate hard dependency thresholds (accumulate all errors)
use _ <- result.try(
irs
|> list.filter(fn(ir) { option.is_some(get_depends_on(ir)) })
|> list.map(fn(ir) {
validate_single_ir_hard_thresholds(ir, expectation_index)
})
|> errors.from_results()
|> result.map(fn(_) { Nil }),
)
// E10: hard-dep expectation-type alignment.
use _ <- result.try(
irs
|> list.filter(fn(ir) { option.is_some(get_depends_on(ir)) })
|> list.map(fn(ir) {
validate_single_ir_hard_type_alignment(ir, expectation_index)
})
|> errors.from_results()
|> result.map(fn(_) { Nil }),
)
// F13: hard-dep latency monotonicity (time_slice only).
use _ <- result.try(
irs
|> list.filter(fn(ir) { option.is_some(get_depends_on(ir)) })
|> list.map(fn(ir) {
validate_single_ir_hard_latency(ir, expectation_index)
})
|> errors.from_results()
|> result.map(fn(_) { Nil }),
)
// Promote phantom type from Linked to DepsValidated
Ok(list.map(irs, ir.promote))
}
/// Builds an index of all expectation paths for quick lookup.
/// The path format is "org.team.service.name".
@internal
pub fn build_expectation_index(
irs: List(IntermediateRepresentation(phase)),
) -> Dict(String, IntermediateRepresentation(phase)) {
irs
|> list.map(fn(ir) {
let path = ir_to_identifier(ir)
#(path, ir)
})
|> dict.from_list
}
fn validate_ir_dependencies(
ir: IntermediateRepresentation(phase),
expectation_index: Dict(String, IntermediateRepresentation(phase)),
) -> Result(Nil, CompilationError) {
// Skip IRs that don't have depends_on
let depends_on = get_depends_on(ir)
use <- bool.guard(when: option.is_none(depends_on), return: Ok(Nil))
let self_path = ir_to_identifier(ir)
// Extract the relations from SloFields.depends_on.
let assert option.Some(relations) = depends_on
// Check for duplicates within each relation type (hard and soft independently)
use _ <- result.try(check_for_duplicates_per_relation(relations, self_path))
// Get all dependency targets (from both hard and soft) for further validation
let all_targets = get_all_dependency_targets(relations)
// Validate each target
all_targets
|> list.try_each(fn(target) {
validate_dependency_target(target, self_path, expectation_index)
})
}
fn get_all_dependency_targets(
relations: Dict(dependency.DependencyRelationType, List(String)),
) -> List(String) {
relations
|> dict.values
|> list.flatten
}
fn check_for_duplicates_per_relation(
relations: Dict(dependency.DependencyRelationType, List(String)),
self_path: String,
) -> Result(Nil, CompilationError) {
relations
|> dict.to_list
|> list.try_each(fn(pair) {
let #(_, targets) = pair
do_check_for_duplicates(targets, set.new(), self_path)
})
}
fn do_check_for_duplicates(
targets: List(String),
seen: set.Set(String),
self_path: String,
) -> Result(Nil, CompilationError) {
case targets {
[] -> Ok(Nil)
[target, ..rest] -> {
use <- bool.guard(
when: set.contains(seen, target),
return: Error(errors.semantic_analysis_dependency_validation_error(
msg: "Duplicate dependency reference '"
<> target
<> "' in '"
<> self_path
<> "'",
)),
)
do_check_for_duplicates(rest, set.insert(seen, target), self_path)
}
}
}
fn validate_dependency_target(
target: String,
self_path: String,
expectation_index: Dict(String, IntermediateRepresentation(phase)),
) -> Result(Nil, CompilationError) {
// First, validate the format
case parse_dependency_path(target) {
Error(Nil) ->
Error(dependency_ref_error(
target,
self_path,
"expected format 'org.team.service.name'",
))
Ok(_) -> {
// Check for self-reference
use <- bool.guard(
when: target == self_path,
return: Error(dependency_ref_error(
target,
self_path,
"self-reference not allowed",
)),
)
// Check if target exists
case dict.get(expectation_index, target) {
Ok(_) -> Ok(Nil)
Error(Nil) ->
Error(dependency_ref_error(target, self_path, "target does not exist"))
}
}
}
}
/// Parses a dependency path into its components (org, team, service, name).
/// Returns Error if the path doesn't have exactly 4 non-empty parts.
@internal
pub fn parse_dependency_path(
path: String,
) -> Result(#(String, String, String, String), Nil) {
case string.split(path, ".") {
[org, team, service, name]
if org != "" && team != "" && service != "" && name != ""
-> Ok(#(org, team, service, name))
_ -> Error(Nil)
}
}
/// Build a dependency reference error with a consistent message format.
fn dependency_ref_error(
target: String,
self_path: String,
reason: String,
) -> CompilationError {
errors.semantic_analysis_dependency_validation_error(
msg: "Invalid dependency reference '"
<> target
<> "' in '"
<> self_path
<> "': "
<> reason,
)
}
// ==== Circular dependency detection ====
/// Builds a directed adjacency list from all IRs with depends_on.
fn build_adjacency_list(
irs: List(IntermediateRepresentation(phase)),
) -> Dict(String, List(String)) {
irs
|> list.filter_map(fn(ir) {
case get_depends_on(ir) {
option.Some(relations) -> {
let path = ir_to_identifier(ir)
let targets = get_all_dependency_targets(relations)
Ok(#(path, targets))
}
option.None -> Error(Nil)
}
})
|> dict.from_list
}
/// Detects circular dependencies in the dependency graph.
fn detect_cycles(
irs: List(IntermediateRepresentation(phase)),
) -> Result(Nil, CompilationError) {
let adjacency = build_adjacency_list(irs)
let nodes =
adjacency
|> dict.keys
|> list.sort(string.compare)
detect_cycles_loop(nodes, adjacency, set.new(), set.new())
|> result.map(fn(_) { Nil })
}
fn detect_cycles_loop(
nodes: List(String),
adjacency: Dict(String, List(String)),
visited: Set(String),
in_progress: Set(String),
) -> Result(Set(String), CompilationError) {
case nodes {
[] -> Ok(visited)
[node, ..rest] -> {
// Skip already fully visited nodes
use <- bool.guard(
when: set.contains(visited, node),
return: detect_cycles_loop(rest, adjacency, visited, in_progress),
)
// Explore this node via DFS
use #(visited, in_progress) <- result.try(
explore_node(node, adjacency, visited, in_progress, [node]),
)
detect_cycles_loop(rest, adjacency, visited, in_progress)
}
}
}
fn explore_node(
node: String,
adjacency: Dict(String, List(String)),
visited: Set(String),
in_progress: Set(String),
path: List(String),
) -> Result(#(Set(String), Set(String)), CompilationError) {
let in_progress = set.insert(in_progress, node)
let neighbors = dict.get(adjacency, node) |> result.unwrap([])
use #(visited, in_progress) <- result.try(explore_neighbors(
neighbors,
adjacency,
visited,
in_progress,
path,
))
// Mark node as fully visited, remove from in-progress
let visited = set.insert(visited, node)
let in_progress = set.delete(in_progress, node)
Ok(#(visited, in_progress))
}
fn explore_neighbors(
neighbors: List(String),
adjacency: Dict(String, List(String)),
visited: Set(String),
in_progress: Set(String),
path: List(String),
) -> Result(#(Set(String), Set(String)), CompilationError) {
case neighbors {
[] -> Ok(#(visited, in_progress))
[neighbor, ..rest] -> {
// Cycle detected: neighbor is on the current DFS path
use <- bool.guard(
when: set.contains(in_progress, neighbor),
return: Error(errors.semantic_analysis_dependency_validation_error(
msg: "Circular dependency detected: "
<> string.join(list.reverse(path), " -> ")
<> " -> "
<> neighbor,
)),
)
// Skip already fully visited nodes, otherwise recurse
use <- bool.guard(
when: set.contains(visited, neighbor),
return: explore_neighbors(rest, adjacency, visited, in_progress, path),
)
use #(visited, in_progress) <- result.try(
explore_node(neighbor, adjacency, visited, in_progress, [
neighbor,
..path
]),
)
explore_neighbors(rest, adjacency, visited, in_progress, path)
}
}
}
// ==== Hard dependency threshold validation ====
/// Validates hard dependency thresholds for a single IR using composite ceiling.
/// The composite ceiling is the product of all hard dependency thresholds,
/// representing the maximum achievable availability given those dependencies.
fn validate_single_ir_hard_thresholds(
ir: IntermediateRepresentation(phase),
expectation_index: Dict(String, IntermediateRepresentation(phase)),
) -> Result(Nil, CompilationError) {
let self_path = ir_to_identifier(ir)
let slo = ir.slo
let source_threshold = slo.threshold
let hard_targets = case slo.depends_on {
option.Some(relations) -> dict.get(relations, Hard) |> result.unwrap([])
option.None -> []
}
let dep_thresholds =
collect_hard_dep_thresholds(hard_targets, expectation_index)
// Nothing to validate if no deps have SLO thresholds
use <- bool.guard(when: list.is_empty(dep_thresholds), return: Ok(Nil))
let composite_ceiling =
compute_composite_ceiling(list.map(dep_thresholds, fn(pair) { pair.1 }))
// Round ceiling to 4 decimal places for cleaner error messages
let display_ceiling = round_to_4(composite_ceiling)
case float.compare(source_threshold, composite_ceiling) {
order.Gt -> {
let deps_description =
dep_thresholds
|> list.map(fn(pair) {
"'" <> pair.0 <> "' (" <> float.to_string(pair.1) <> ")"
})
|> string.join(", ")
Error(errors.semantic_analysis_dependency_validation_error(
msg: "Composite hard dependency threshold violation: '"
<> self_path
<> "' (threshold: "
<> float.to_string(source_threshold)
<> ") exceeds the composite availability ceiling of "
<> float.to_string(display_ceiling)
<> " from its hard dependencies: "
<> deps_description,
))
}
_ -> Ok(Nil)
}
}
/// Collects thresholds from hard dependency targets.
/// Skips targets that don't exist in the index.
fn collect_hard_dep_thresholds(
targets: List(String),
expectation_index: Dict(String, IntermediateRepresentation(phase)),
) -> List(#(String, Float)) {
targets
|> list.filter_map(fn(target_path) {
case dict.get(expectation_index, target_path) {
Error(Nil) -> Error(Nil)
Ok(target_ir) -> Ok(#(target_path, target_ir.slo.threshold))
}
})
}
/// Computes the composite availability ceiling from a list of thresholds.
/// Each threshold is a percentage (e.g. 99.99). The composite ceiling is
/// the product of individual availabilities.
fn compute_composite_ceiling(thresholds: List(Float)) -> Float {
list.fold(thresholds, 1.0, fn(acc, t) { acc *. { t /. 100.0 } }) *. 100.0
}
/// Rounds a float to 4 decimal places for cleaner display.
fn round_to_4(f: Float) -> Float {
int.to_float(float.round(f *. 10_000.0)) /. 10_000.0
}
// ==== E10: hard-dep expectation-type alignment ====
/// For each hard dep with a declared expectation type, error if it doesn't
/// match the dependent's declared type. Skips pairs where either side is
/// unmeasured or undeclared.
fn validate_single_ir_hard_type_alignment(
ir: IntermediateRepresentation(phase),
expectation_index: Dict(String, IntermediateRepresentation(phase)),
) -> Result(Nil, CompilationError) {
let self_path = ir_to_identifier(ir)
let parent_type = ir.slo.expectation_type
let hard_targets = case ir.slo.depends_on {
option.Some(relations) -> dict.get(relations, Hard) |> result.unwrap([])
option.None -> []
}
hard_targets
|> list.try_each(fn(target) {
case dict.get(expectation_index, target) {
Error(Nil) -> Ok(Nil)
Ok(target_ir) -> {
case parent_type, target_ir.slo.expectation_type {
option.Some(p), option.Some(d) if p != d ->
Error(errors.semantic_analysis_dependency_validation_error(
msg: "Expectation-type mismatch on hard dependency: '"
<> self_path
<> "' ("
<> expectation_type_to_string(p)
<> ") hard-depends on '"
<> target
<> "' ("
<> expectation_type_to_string(d)
<> "); hard dependencies must share an expectation type",
))
_, _ -> Ok(Nil)
}
}
}
})
}
fn expectation_type_to_string(t: ast.ExpectationType) -> String {
case t {
ast.SuccessRateType -> "success_rate"
ast.TimeSliceType -> "time_slice"
}
}
// ==== F13: hard-dep latency monotonicity ====
/// For each hard dep of a `time_slice` expectation with a `below` bound,
/// error if the dep's `below_ms` exceeds the dependent's. Skips deps that
/// aren't time_slice or have no `below_ms`.
fn validate_single_ir_hard_latency(
ir: IntermediateRepresentation(phase),
expectation_index: Dict(String, IntermediateRepresentation(phase)),
) -> Result(Nil, CompilationError) {
let self_path = ir_to_identifier(ir)
let parent_below = ir.slo.below_ms
// Only relevant when the parent declares a `below` bound.
use <- bool.guard(when: option.is_none(parent_below), return: Ok(Nil))
let assert option.Some(parent_ms) = parent_below
let hard_targets = case ir.slo.depends_on {
option.Some(relations) -> dict.get(relations, Hard) |> result.unwrap([])
option.None -> []
}
hard_targets
|> list.try_each(fn(target) {
case dict.get(expectation_index, target) {
Error(Nil) -> Ok(Nil)
Ok(target_ir) -> {
case target_ir.slo.below_ms {
option.None -> Ok(Nil)
option.Some(dep_ms) ->
case float.compare(dep_ms, parent_ms) {
order.Gt ->
Error(errors.semantic_analysis_dependency_validation_error(
msg: "Hard-dep latency monotonicity violation: '"
<> self_path
<> "' guarantees below "
<> float.to_string(parent_ms)
<> "ms but hard-depends on '"
<> target
<> "' which guarantees below "
<> float.to_string(dep_ms)
<> "ms; child must be no slower than its hard dependencies",
))
_ -> Ok(Nil)
}
}
}
}
})
}