sim-macros 0.1.0-rc.1

Proc-macro surface for authored SIM libraries.
Documentation
//! Native number-domain ABI dispatch generation.

use crate::parse::ParsedNumberDomain;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::quote;
use syn::{Ident, LitStr};

pub(crate) fn native_number_domain_call_dispatch(
    number_domains: &[ParsedNumberDomain],
) -> TokenStream2 {
    let arms = number_domains.iter().flat_map(number_domain_dispatch_arms);
    let result_adapter = if number_domains.is_empty() {
        TokenStream2::new()
    } else {
        quote! {
            trait NativeNumberDomainOptionalResult<T> {
                fn into_native_number_domain_optional_result(self) -> ::sim::kernel::Result<Option<T>>;
            }

            impl NativeNumberDomainOptionalResult<::sim::kernel::Expr>
                for Option<::sim::kernel::Expr>
            {
                fn into_native_number_domain_optional_result(
                    self,
                ) -> ::sim::kernel::Result<Option<::sim::kernel::Expr>> {
                    Ok(self)
                }
            }

            impl NativeNumberDomainOptionalResult<::sim::kernel::Expr>
                for ::sim::kernel::Result<Option<::sim::kernel::Expr>>
            {
                fn into_native_number_domain_optional_result(
                    self,
                ) -> ::sim::kernel::Result<Option<::sim::kernel::Expr>> {
                    self
                }
            }

            trait NativeNumberDomainExprResult {
                fn into_native_number_domain_expr_result(self) -> ::sim::kernel::Result<::sim::kernel::Expr>;
            }

            impl NativeNumberDomainExprResult for ::sim::kernel::Expr {
                fn into_native_number_domain_expr_result(self) -> ::sim::kernel::Result<::sim::kernel::Expr> {
                    Ok(self)
                }
            }

            impl NativeNumberDomainExprResult for ::sim::kernel::Result<::sim::kernel::Expr> {
                fn into_native_number_domain_expr_result(self) -> ::sim::kernel::Result<::sim::kernel::Expr> {
                    self
                }
            }
        }
    };

    quote! {
        #result_adapter

        fn native_number_domain_call(
            function: &str,
            expr: &::sim::kernel::Expr,
        ) -> ::sim::kernel::Result<Option<::sim::kernel::Expr>> {
            match function {
                #(#arms,)*
                _ => Ok(None),
            }
        }
    }
}

fn number_domain_dispatch_arms(domain: &ParsedNumberDomain) -> Vec<TokenStream2> {
    let parse_op = LitStr::new(
        &format!("{}/parse-literal", domain.symbol),
        Span::call_site(),
    );
    let encode_op = LitStr::new(
        &format!("{}/encode-literal", domain.symbol),
        Span::call_site(),
    );
    let parse_ident = &domain.parse_ident;
    let encode_ident = &domain.encode_ident;
    let mut arms = vec![
        quote! {
            #parse_op => {
                let text = match expr {
                    ::sim::kernel::Expr::String(text) => text.clone(),
                    other => {
                        return Err(::sim::kernel::Error::HostError(format!(
                            "{} expected literal text, got {:?}",
                            #parse_op,
                            other,
                        )));
                    }
                };
                let parsed = NativeNumberDomainOptionalResult::into_native_number_domain_optional_result(
                    super::#parse_ident(text),
                )?;
                Ok(Some(parsed.unwrap_or(::sim::kernel::Expr::Nil)))
            }
        },
        quote! {
            #encode_op => {
                let encoded = NativeNumberDomainOptionalResult::into_native_number_domain_optional_result(
                    super::#encode_ident(expr.clone()),
                )?;
                Ok(Some(encoded.unwrap_or(::sim::kernel::Expr::Nil)))
            }
        },
    ];

    arms.extend(binary_number_domain_arms(
        domain,
        "add",
        domain.add_ident.as_ref(),
    ));
    arms.extend(binary_number_domain_arms(
        domain,
        "sub",
        domain.sub_ident.as_ref(),
    ));
    arms.extend(binary_number_domain_arms(
        domain,
        "mul",
        domain.mul_ident.as_ref(),
    ));
    arms.extend(binary_number_domain_arms(
        domain,
        "div",
        domain.div_ident.as_ref(),
    ));
    arms.extend(unary_number_domain_arms(
        domain,
        "neg",
        domain.neg_ident.as_ref(),
    ));
    arms.extend(reduction_number_domain_arms(
        domain,
        "sum",
        domain.sum_ident.as_ref(),
    ));
    arms.extend(reduction_number_domain_arms(
        domain,
        "product",
        domain.product_ident.as_ref(),
    ));
    arms
}

fn binary_number_domain_arms(
    domain: &ParsedNumberDomain,
    op: &str,
    ident: Option<&Ident>,
) -> Option<TokenStream2> {
    let ident = ident?;
    let op_lit = LitStr::new(&format!("{}/{op}", domain.symbol), Span::call_site());
    Some(quote! {
        #op_lit => {
            let ::sim::kernel::Expr::List(items) = expr else {
                return Err(::sim::kernel::Error::HostError(format!(
                    "{} expected an argument list",
                    #op_lit,
                )));
            };
            let [left, right] = items.as_slice() else {
                return Err(::sim::kernel::Error::HostError(format!(
                    "{} expects exactly two arguments",
                    #op_lit,
                )));
            };
            let result = NativeNumberDomainExprResult::into_native_number_domain_expr_result(
                super::#ident(left.clone(), right.clone()),
            )?;
            Ok(Some(result))
        }
    })
}

fn unary_number_domain_arms(
    domain: &ParsedNumberDomain,
    op: &str,
    ident: Option<&Ident>,
) -> Option<TokenStream2> {
    let ident = ident?;
    let op_lit = LitStr::new(&format!("{}/{op}", domain.symbol), Span::call_site());
    Some(quote! {
        #op_lit => {
            let ::sim::kernel::Expr::List(items) = expr else {
                return Err(::sim::kernel::Error::HostError(format!(
                    "{} expected an argument list",
                    #op_lit,
                )));
            };
            let [operand] = items.as_slice() else {
                return Err(::sim::kernel::Error::HostError(format!(
                    "{} expects exactly one argument",
                    #op_lit,
                )));
            };
            let result = NativeNumberDomainExprResult::into_native_number_domain_expr_result(
                super::#ident(operand.clone()),
            )?;
            Ok(Some(result))
        }
    })
}

fn reduction_number_domain_arms(
    domain: &ParsedNumberDomain,
    op: &str,
    ident: Option<&Ident>,
) -> Option<TokenStream2> {
    let ident = ident?;
    let op_lit = LitStr::new(&format!("{}/{op}", domain.symbol), Span::call_site());
    Some(quote! {
        #op_lit => {
            let ::sim::kernel::Expr::List(items) = expr else {
                return Err(::sim::kernel::Error::HostError(format!(
                    "{} expected an argument list",
                    #op_lit,
                )));
            };
            let result = NativeNumberDomainExprResult::into_native_number_domain_expr_result(
                super::#ident(items.clone()),
            )?;
            Ok(Some(result))
        }
    })
}