sim-macros 0.2.1

Proc-macro surface for authored SIM libraries.
Documentation
use super::markers::{parse_codec, parse_macro, parse_number_domain, parse_site};
use super::types::parse_type;
use super::{
    ParsedCase, ParsedClass, ParsedField, ParsedFunction, ParsedFunctionKind, ParsedModule,
};
use crate::attrs::{has_attr, lit_str_attr_from_attrs, string_attr_from_attrs, strip_attrs};
use quote::format_ident;
use syn::{Attribute, Fields, Item, ItemFn, ItemStruct, ReturnType};

impl ParsedModule {
    pub(crate) fn parse(items: &mut [Item]) -> syn::Result<Self> {
        let mut classes = Vec::new();
        let mut functions = Vec::new();
        let mut macros = Vec::new();
        let mut codecs = Vec::new();
        let mut number_domains = Vec::new();
        let mut sites = Vec::new();
        let mut cleaned_items = Vec::new();

        for item in items.iter().cloned() {
            match item {
                Item::Struct(mut item_struct) if has_attr(&item_struct.attrs, "sim_class") => {
                    classes.push(parse_class(&mut item_struct)?);
                    cleaned_items.push(Item::Struct(item_struct));
                }
                Item::Fn(mut item_fn)
                    if has_attr(&item_fn.attrs, "sim_constructor")
                        || has_attr(&item_fn.attrs, "sim_fn") =>
                {
                    functions.push(parse_function(&mut item_fn, &classes)?);
                    cleaned_items.push(Item::Fn(item_fn));
                }
                Item::Fn(mut item_fn) if has_attr(&item_fn.attrs, "sim_macro") => {
                    macros.push(parse_macro(&mut item_fn)?);
                }
                Item::Fn(mut item_fn) if has_attr(&item_fn.attrs, "sim_codec") => {
                    codecs.push(parse_codec(&mut item_fn)?);
                }
                Item::Fn(mut item_fn) if has_attr(&item_fn.attrs, "sim_number_domain") => {
                    number_domains.push(parse_number_domain(&mut item_fn)?);
                }
                Item::Fn(mut item_fn) if has_attr(&item_fn.attrs, "sim_site") => {
                    sites.push(parse_site(&mut item_fn)?);
                }
                other => cleaned_items.push(other),
            }
        }

        Ok(Self {
            classes,
            functions,
            macros,
            codecs,
            number_domains,
            sites,
            cleaned_items,
        })
    }
}

fn parse_class(item: &mut ItemStruct) -> syn::Result<ParsedClass> {
    let lisp_name = string_attr_from_attrs(&item.attrs, "sim_class", "name")?
        .unwrap_or_else(|| item.ident.to_string());
    let shape_literal = lit_str_attr_from_attrs(&item.attrs, "shape", "")?;
    strip_attrs(&mut item.attrs, &["sim_class", "shape"]);
    let Fields::Named(fields) = &item.fields else {
        return Err(syn::Error::new_spanned(
            &item.fields,
            "#[sim_class] requires named fields",
        ));
    };
    let mut parsed_fields = Vec::new();
    for field in &fields.named {
        let ident = field
            .ident
            .clone()
            .ok_or_else(|| syn::Error::new_spanned(field, "expected named field"))?;
        parsed_fields.push(ParsedField {
            ident,
            ty: parse_type(&field.ty)?,
        });
    }
    Ok(ParsedClass {
        rust_ident: item.ident.clone(),
        lisp_name,
        wrapper_ident: format_ident!("__Lisp{}Value", item.ident),
        shape_literal,
        fields: parsed_fields,
    })
}

fn parse_function(item: &mut ItemFn, classes: &[ParsedClass]) -> syn::Result<ParsedFunction> {
    let kind = if has_attr(&item.attrs, "sim_constructor") {
        let class_name = string_attr_from_attrs(&item.attrs, "sim_constructor", "class")?
            .ok_or_else(|| {
                syn::Error::new_spanned(
                    &item.sig.ident,
                    "#[sim_constructor] requires class = \"...\"",
                )
            })?;
        let class_ident = classes
            .iter()
            .find(|class| class.lisp_name == class_name || class.rust_ident == class_name.as_str())
            .map(|class| class.rust_ident.clone())
            .ok_or_else(|| {
                syn::Error::new_spanned(
                    &item.sig.ident,
                    format!("unknown constructor class {class_name}"),
                )
            })?;
        ParsedFunctionKind::Constructor { class_ident }
    } else {
        ParsedFunctionKind::Function {
            lisp_name: string_attr_from_attrs(&item.attrs, "sim_fn", "name")?
                .unwrap_or_else(|| item.sig.ident.to_string()),
        }
    };

    let cases = case_attrs(&item.attrs)?;
    if cases.is_empty() {
        return Err(syn::Error::new_spanned(
            &item.sig.ident,
            "expected at least one #[case(args = \"...\")]",
        ));
    }

    let mut inputs = Vec::new();
    for arg in &item.sig.inputs {
        let syn::FnArg::Typed(arg) = arg else {
            return Err(syn::Error::new_spanned(arg, "methods are not supported"));
        };
        let syn::Pat::Ident(pat_ident) = arg.pat.as_ref() else {
            return Err(syn::Error::new_spanned(
                &arg.pat,
                "unsupported argument pattern",
            ));
        };
        inputs.push((pat_ident.ident.clone(), parse_type(&arg.ty)?));
    }

    let output = match &item.sig.output {
        ReturnType::Default => None,
        ReturnType::Type(_, ty) => Some(parse_type(ty)?),
    };

    strip_attrs(
        &mut item.attrs,
        &["sim_constructor", "sim_fn", "case", "shape"],
    );

    Ok(ParsedFunction {
        rust_ident: item.sig.ident.clone(),
        kind,
        cases,
        inputs,
        output,
    })
}

fn case_attrs(attrs: &[Attribute]) -> syn::Result<Vec<ParsedCase>> {
    let mut cases = Vec::new();
    for attr in attrs.iter().filter(|attr| attr.path().is_ident("case")) {
        let mut args = None;
        let mut result = None;
        attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("args") {
                let value: syn::LitStr = meta.value()?.parse()?;
                args = Some(value);
                Ok(())
            } else if meta.path.is_ident("result") {
                let value: syn::LitStr = meta.value()?.parse()?;
                result = Some(value);
                Ok(())
            } else {
                Err(meta.error("unsupported #[case(...)] entry"))
            }
        })?;
        let args = args.ok_or_else(|| syn::Error::new_spanned(attr, "missing case args"))?;
        cases.push(ParsedCase { args, result });
    }
    Ok(cases)
}