sim-macros 0.1.1

Proc-macro surface for authored SIM libraries.
Documentation
use crate::attrs::{camel_case, string_attr_from_attrs, string_attr_value};
use crate::parse::ParsedModule;
use crate::shape::{Sexp, parse_sexp};
use quote::format_ident;
use syn::{Item, Token, parse::Parser, parse_quote, punctuated::Punctuated};

#[test]
fn duplicate_lisp_lib_keys_are_rejected() {
    let parser = Punctuated::<syn::MetaNameValue, Token![,]>::parse_terminated;
    let attrs = parser
        .parse2(parse_quote! { id = "one", id = "two" })
        .unwrap();
    assert!(string_attr_value(&attrs, "id").is_err());
}

#[test]
fn duplicate_nested_attribute_keys_are_rejected() {
    let item: Item = parse_quote! {
        #[sim_class(name = "A", name = "B")]
        struct Point { x: f64 }
    };
    let Item::Struct(item) = item else {
        panic!("expected struct");
    };
    let err = string_attr_from_attrs(&item.attrs, "sim_class", "name").unwrap_err();
    assert!(err.to_string().contains("duplicate name entry"));
}

#[test]
fn tuple_struct_lisp_class_is_rejected() {
    let mut items = vec![parse_quote! {
        #[sim_class(name = "Point")]
        struct Point(f64, f64);
    }];
    let err = ParsedModule::parse(&mut items).err().unwrap();
    assert!(err.to_string().contains("named fields"));
}

#[test]
fn unknown_constructor_class_is_rejected() {
    let mut items = vec![
        parse_quote! {
            #[sim_class(name = "Point")]
            struct Point { x: f64, y: f64 }
        },
        parse_quote! {
            #[sim_constructor(class = "Missing")]
            #[case(args = "((capture x Number) (capture y Number))", result = "Missing")]
            fn make_missing(x: f64, y: f64) -> Point { Point { x, y } }
        },
    ];
    let err = ParsedModule::parse(&mut items).err().unwrap();
    assert!(err.to_string().contains("unknown constructor class"));
}

#[test]
fn borrowed_builtin_type_is_rejected() {
    let mut items = vec![parse_quote! {
        #[sim_fn(name = "echo")]
        #[case(args = "((capture value String))", result = "String")]
        fn echo(value: &str) -> String { value.to_owned() }
    }];
    let err = ParsedModule::parse(&mut items).err().unwrap();
    assert!(err.to_string().contains("borrowed builtin types"));
}

#[test]
fn codec_marker_is_parsed_and_consumed() {
    let mut items = vec![parse_quote! {
        #[sim_codec(symbol = "codec/mock", decode = "decode_mock", encode = "encode_mock")]
        fn mock_codec() {}
    }];
    let parsed = ParsedModule::parse(&mut items).unwrap();
    assert_eq!(parsed.codecs.len(), 1);
    assert_eq!(parsed.codecs[0].symbol, "codec/mock");
    assert_eq!(parsed.codecs[0].decode_ident, "decode_mock");
    assert_eq!(parsed.codecs[0].encode_ident, "encode_mock");
    assert!(parsed.cleaned_items.is_empty());
}

#[test]
fn macro_marker_is_parsed_and_consumed() {
    let mut items = vec![parse_quote! {
        #[sim_macro(symbol = "standard/proof-quote", expand = "expand_proof_quote")]
        fn proof_quote_macro() {}
    }];
    let parsed = ParsedModule::parse(&mut items).unwrap();
    assert_eq!(parsed.macros.len(), 1);
    assert_eq!(parsed.macros[0].symbol, "standard/proof-quote");
    assert_eq!(parsed.macros[0].expand_ident, "expand_proof_quote");
    assert!(parsed.cleaned_items.is_empty());
}

#[test]
fn number_domain_marker_is_parsed_and_consumed() {
    let mut items = vec![parse_quote! {
        #[sim_number_domain(
            symbol = "numbers/f64",
            parse = "parse_f64",
            encode = "encode_f64",
            add = "add_f64"
        )]
        fn f64_domain() {}
    }];
    let parsed = ParsedModule::parse(&mut items).unwrap();
    assert_eq!(parsed.number_domains.len(), 1);
    assert_eq!(parsed.number_domains[0].symbol, "numbers/f64");
    assert_eq!(parsed.number_domains[0].parse_ident, "parse_f64");
    assert_eq!(parsed.number_domains[0].encode_ident, "encode_f64");
    assert_eq!(
        parsed.number_domains[0]
            .add_ident
            .as_ref()
            .unwrap()
            .to_string(),
        "add_f64"
    );
    assert!(parsed.cleaned_items.is_empty());
}

#[test]
fn site_marker_is_parsed_and_consumed() {
    let mut items = vec![parse_quote! {
        #[sim_site(symbol = "model/local", realize = "realize_local")]
        fn local_site() {}
    }];
    let parsed = ParsedModule::parse(&mut items).unwrap();
    assert_eq!(parsed.sites.len(), 1);
    assert_eq!(parsed.sites[0].symbol, "model/local");
    assert_eq!(parsed.sites[0].realize_ident, "realize_local");
    assert!(parsed.cleaned_items.is_empty());
}

#[test]
fn site_marker_emits_site_manifest_export() {
    let mut items = vec![parse_quote! {
        #[sim_site(symbol = "model/local", realize = "realize_local")]
        fn local_site() {}
    }];
    let parsed = ParsedModule::parse(&mut items).unwrap();
    let vis = parse_quote!(pub);
    let generated = parsed
        .expand(&vis, &format_ident!("SiteLib"), "site-lib", "0.1.0", false)
        .unwrap()
        .to_string();

    assert!(generated.contains("Export :: Site"));
    assert!(generated.contains("Symbol :: qualified (\"model\" , \"local\")"));
}

#[test]
fn sexp_parser_handles_capture_shapes() {
    let parsed = parse_sexp("((capture x Number) (capture y String))").unwrap();
    assert!(matches!(parsed, Sexp::List(items) if items.len() == 2));
}

#[test]
fn camel_case_normalizes_module_names() {
    assert_eq!(camel_case("utility_tools"), "UtilityTools");
    assert_eq!(camel_case("m12"), "M12");
}