etdl-compiler 0.1.3

ETDL compiler: IEC 61025 fault tree resolution, MOCUS cut sets, ECEL type-checking, semantic validation, and code generation for event-driven microservices
Documentation
use etdl_parser::ast::{EtlDocument, Node};
use etdl_parser::asyncapi::AsyncApiRegistry;
use etdl_parser::ecel::*;

use crate::validate::Diagnostic;

pub fn type_check_conditions(
    doc: &EtlDocument,
    registry: &AsyncApiRegistry,
    diagnostics: &mut Vec<Diagnostic>,
) {
    for (_tree_name, tree) in &doc.event_trees {
        let init_msg_ref = &tree.initiating_event.message;

        if !doc.asyncapi_imports.contains_key(&init_msg_ref.alias) {
            continue;
        }

        if registry.resolve(init_msg_ref).is_err() {
            continue;
        }

        for (node_id, node) in &tree.nodes {
            if let Node::Barrier(barrier) = node {
                for (i, branch) in barrier.branches.iter().enumerate() {
                    if branch.condition == Condition::Default {
                        continue;
                    }

                    if let Condition::Comparison(ref cmp) = branch.condition {
                        check_comparison_type(
                            cmp, init_msg_ref, registry, node_id, i, diagnostics,
                        );
                    }
                }
            }
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
enum EcelType {
    Number,
    String,
    Bool,
    Null,
    Array(Box<EcelType>),
    Object,
    Unknown,
}

fn check_comparison_type(
    cmp: &Comparison,
    message_ref: &etdl_parser::ast::ExternalRef,
    registry: &AsyncApiRegistry,
    node_id: &str,
    branch_idx: usize,
    diagnostics: &mut Vec<Diagnostic>,
) {
    let left_type = resolve_operand_type(&cmp.left, message_ref, registry);
    let right_type = resolve_operand_type(&cmp.right, message_ref, registry);

    match cmp.op {
        Comparator::Eq | Comparator::Neq => {
            if left_type != right_type && left_type != EcelType::Unknown && right_type != EcelType::Unknown {
                diagnostics.push(Diagnostic::error(
                    "V-204",
                    format!(
                        "barrier '{}' branch {}: type mismatch in comparison {:?} {:?} {:?}: left {:?}, right {:?}",
                        node_id, branch_idx, cmp.left, cmp.op, cmp.right, left_type, right_type
                    ),
                ));
            }
        }
        Comparator::Gt | Comparator::Gte | Comparator::Lt | Comparator::Lte => {
            if left_type != EcelType::Number && left_type != EcelType::Unknown {
                diagnostics.push(Diagnostic::error(
                    "V-204",
                    format!(
                        "barrier '{}' branch {}: ordering comparison requires number, got {:?}",
                        node_id, branch_idx, left_type
                    ),
                ));
            }
            if right_type != EcelType::Number && right_type != EcelType::Unknown {
                diagnostics.push(Diagnostic::error(
                    "V-204",
                    format!(
                        "barrier '{}' branch {}: ordering comparison requires number, got {:?}",
                        node_id, branch_idx, right_type
                    ),
                ));
            }
        }
        Comparator::In => match &right_type {
            EcelType::Array(_) | EcelType::Unknown => {}
            _ => {
                diagnostics.push(Diagnostic::error(
                    "V-204",
                    format!(
                        "barrier '{}' branch {}: 'in' right operand must be array, got {:?}",
                        node_id, branch_idx, right_type
                    ),
                ));
            }
        },
        Comparator::Matches => {
            if left_type != EcelType::String && left_type != EcelType::Unknown {
                diagnostics.push(Diagnostic::error(
                    "V-204",
                    format!(
                        "barrier '{}' branch {}: 'matches' left operand must be string, got {:?}",
                        node_id, branch_idx, left_type
                    ),
                ));
            }
        }
    }
}

fn resolve_operand_type(
    operand: &Operand,
    message_ref: &etdl_parser::ast::ExternalRef,
    registry: &AsyncApiRegistry,
) -> EcelType {
    match operand {
        Operand::Path(path_expr) => {
            let segments: Vec<&PathSegment> = path_expr
                .segments
                .iter()
                .skip(1)
                .collect();

            if segments.is_empty() {
                return EcelType::Object;
            }

            match registry.get_schema_for_path(message_ref, &path_expr.segments) {
                Ok(Some(schema)) => schema_to_ecel_type(&schema),
                Ok(None) => EcelType::Unknown,
                Err(_) => EcelType::Unknown,
            }
        }
        Operand::Literal(lit) => literal_to_ecel_type(lit),
    }
}

fn literal_to_ecel_type(lit: &Literal) -> EcelType {
    match lit {
        Literal::Number(_) => EcelType::Number,
        Literal::String(_) => EcelType::String,
        Literal::Bool(_) => EcelType::Bool,
        Literal::Null => EcelType::Null,
        Literal::Array(items) => {
            let inner = items.first().map(|i| literal_to_ecel_type(i)).unwrap_or(EcelType::Unknown);
            EcelType::Array(Box::new(inner))
        }
    }
}

fn schema_to_ecel_type(schema: &serde_json::Value) -> EcelType {
    if let Some(type_val) = schema.get("type") {
        match type_val.as_str() {
            Some("string") => return EcelType::String,
            Some("integer") | Some("number") => return EcelType::Number,
            Some("boolean") => return EcelType::Bool,
            Some("null") => return EcelType::Null,
            Some("array") => {
                if let Some(items) = schema.get("items") {
                    return EcelType::Array(Box::new(schema_to_ecel_type(items)));
                }
                return EcelType::Array(Box::new(EcelType::Unknown));
            }
            Some("object") => return EcelType::Object,
            _ => {}
        }
    }

    if let Some(properties) = schema.get("properties") {
        if properties.is_object() && !properties.as_object().unwrap().is_empty() {
            return EcelType::Object;
        }
    }

    if schema.get("items").is_some() {
        return EcelType::Array(Box::new(EcelType::Unknown));
    }

    EcelType::Unknown
}