cfs-synapse-codegen-cfs 0.2.10

cFS C and Rust code generator for Synapse IDL files
Documentation
use std::collections::HashMap;

use synapse_parser::ast::{
    ArraySuffix, Attribute, BaseType, EnumDef, Item, Literal, MessageDef, PacketKind,
    PrimitiveType, SynFile, TypeExpr,
};

use crate::constants::{ConstContext, resolve_ident_to_u64};

pub(crate) fn file_namespace(file: &SynFile) -> Vec<String> {
    file.items
        .iter()
        .find_map(|item| match item {
            Item::Namespace(ns) => Some(ns.name.clone()),
            _ => None,
        })
        .unwrap_or_default()
}

pub(crate) fn enum_defs(file: &SynFile) -> HashMap<String, &EnumDef> {
    file.items
        .iter()
        .filter_map(|item| match item {
            Item::Enum(e) => Some((e.name.clone(), e)),
            _ => None,
        })
        .collect()
}

/// Returns the `@mid` attribute value, if present.
pub(crate) fn find_mid_attr(attrs: &[Attribute]) -> Option<&Literal> {
    attrs.iter().find(|a| a.name == "mid").map(|a| &a.value)
}

/// Returns the `@cc` attribute value, if present.
pub(crate) fn find_cc_attr(attrs: &[Attribute]) -> Option<&Literal> {
    attrs.iter().find(|a| a.name == "cc").map(|a| &a.value)
}

pub(crate) fn packet_item(item: &Item) -> Option<&MessageDef> {
    match item {
        Item::Command(m) | Item::Telemetry(m) => Some(m),
        _ => None,
    }
}

pub(crate) fn packet_is_command(m: &MessageDef) -> bool {
    match m.kind {
        PacketKind::Command => true,
        PacketKind::Telemetry | PacketKind::Message => false,
    }
}

pub(crate) fn type_expr_display(ty: &TypeExpr) -> String {
    let mut out = base_type_display(&ty.base);
    match &ty.array {
        None => {}
        Some(ArraySuffix::Dynamic) => out.push_str("[]"),
        Some(ArraySuffix::Fixed(n)) => out.push_str(&format!("[{n}]")),
        Some(ArraySuffix::Bounded(n)) => out.push_str(&format!("[<={n}]")),
    }
    out
}

fn base_type_display(base: &BaseType) -> String {
    match base {
        BaseType::String => "string".to_string(),
        BaseType::Primitive(p) => primitive_name(*p).to_string(),
        BaseType::Ref(segments) => segments.join("::"),
    }
}

pub(crate) fn primitive_name(p: PrimitiveType) -> &'static str {
    match p {
        PrimitiveType::F32 => "f32",
        PrimitiveType::F64 => "f64",
        PrimitiveType::I8 => "i8",
        PrimitiveType::I16 => "i16",
        PrimitiveType::I32 => "i32",
        PrimitiveType::I64 => "i64",
        PrimitiveType::U8 => "u8",
        PrimitiveType::U16 => "u16",
        PrimitiveType::U32 => "u32",
        PrimitiveType::U64 => "u64",
        PrimitiveType::Bool => "bool",
        PrimitiveType::Bytes => "bytes",
    }
}

pub(crate) fn emit_doc_lines(out: &mut String, doc: &[String]) {
    for line in doc {
        if line.is_empty() {
            out.push_str("///\n");
        } else {
            out.push_str(&format!("/// {line}\n"));
        }
    }
}

pub(crate) fn emit_indented_doc_lines(out: &mut String, doc: &[String]) {
    for line in doc {
        if line.is_empty() {
            out.push_str("    ///\n");
        } else {
            out.push_str(&format!("    /// {line}\n"));
        }
    }
}

/// Format a MID literal for a C `#define` line.
pub(crate) fn literal_mid_str(lit: &Literal, constants: &ConstContext<'_>) -> String {
    match lit {
        Literal::Hex(n) => format!("0x{:04X}U", n),
        Literal::Int(n) => format!("{}U", n),
        Literal::Ident(segs) if constants.is_local_bare_ident(segs) => segs.join("::"),
        Literal::Ident(segs) => resolve_ident_to_u64(segs, constants)
            .map(|value| format!("0x{:04X}U", value))
            .unwrap_or_else(|| segs.join("::")),
        other => literal_str(other),
    }
}

/// Format a command-code literal for a C `#define` line.
pub(crate) fn literal_cc_str(lit: &Literal, constants: &ConstContext<'_>) -> String {
    match lit {
        Literal::Hex(n) => format!("0x{:X}U", n),
        Literal::Int(n) => format!("{}U", n),
        Literal::Ident(segs) if constants.is_local_bare_ident(segs) => segs.join("::"),
        Literal::Ident(segs) => resolve_ident_to_u64(segs, constants)
            .map(|value| format!("{}U", value))
            .unwrap_or_else(|| segs.join("::")),
        other => literal_str(other),
    }
}

pub(crate) fn literal_str(lit: &Literal) -> String {
    match lit {
        Literal::Float(f) => {
            let s = format!("{}", f);
            if s.contains('.') || s.contains('e') {
                s
            } else {
                format!("{}.0", s)
            }
        }
        Literal::Int(n) => n.to_string(),
        Literal::Hex(n) => format!("0x{:X}U", n),
        Literal::Bool(b) => {
            if *b {
                "1".to_string()
            } else {
                "0".to_string()
            }
        }
        Literal::Str(s) => format!("{:?}", s),
        Literal::Ident(segments) => segments.join("::"),
    }
}

pub(crate) fn typed_literal_str(lit: &Literal, ty: &TypeExpr) -> String {
    match (lit, &ty.base) {
        (Literal::Hex(n), BaseType::Primitive(p)) => c_hex_str(*n, *p),
        _ => literal_str(lit),
    }
}

fn c_hex_str(value: u64, ty: PrimitiveType) -> String {
    match ty {
        PrimitiveType::U8 | PrimitiveType::I8 => format!("0x{:02X}U", value),
        PrimitiveType::U16 | PrimitiveType::I16 => format!("0x{:04X}U", value),
        PrimitiveType::U32 | PrimitiveType::I32 => format!("0x{:08X}U", value),
        PrimitiveType::U64 | PrimitiveType::I64 => format!("0x{:016X}U", value),
        PrimitiveType::F32 | PrimitiveType::F64 | PrimitiveType::Bool | PrimitiveType::Bytes => {
            format!("0x{:X}U", value)
        }
    }
}

pub(crate) fn import_c_header(path: &str) -> String {
    replace_extension(path, "h")
}

pub(crate) fn import_rust_module(path: &str) -> String {
    let header = path.rsplit('/').next().unwrap_or(path);
    replace_extension(header, "")
}

fn replace_extension(path: &str, ext: &str) -> String {
    match path.rsplit_once('.') {
        Some((stem, _)) if ext.is_empty() => stem.to_string(),
        Some((stem, _)) => format!("{stem}.{ext}"),
        None if ext.is_empty() => path.to_string(),
        None => format!("{path}.{ext}"),
    }
}

pub(crate) fn to_screaming_snake(name: &str) -> String {
    let mut out = String::new();
    for (i, ch) in name.chars().enumerate() {
        if ch.is_uppercase() && i > 0 {
            out.push('_');
        }
        out.push(ch.to_ascii_uppercase());
    }
    out
}