aranya-capi-codegen 0.7.1

Code generation for Aranya's C API tooling
Documentation
use std::{fs::File, io::Write as _};

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{Ident, Path, parse_quote};
use tracing::{debug, info, instrument};

use super::{
    ast::Ast,
    ctx::Ctx,
    error::BuildError,
    syntax::{Fn, Item, Node, Trimmed},
};

/// Configures code generation.
#[derive(Clone, Debug)]
pub struct Config {
    /// The error type to use.
    ///
    /// Must implement `aranya_capi_core::ErrorCode`.
    pub err_ty: Path,
    /// The extended error type to use.
    ///
    /// Must implement `aranya_capi_core::ExtendedError`.
    pub ext_err_ty: Path,
    /// Function identifier prefix.
    ///
    /// E.g., `os` converts `fn foo` to `fn os_foo`.
    pub fn_prefix: Ident,
    /// Type identifier prefix.
    ///
    /// E.g., `Os` converts `struct Foo` to `struct OsFoo`.
    pub ty_prefix: Ident,
    /// Path to the module where defs live.
    pub defs: Path,
    /// The `TARGET` environment variable.
    pub target: String,
}

impl Config {
    /// Generates code.
    #[instrument(skip_all)]
    pub fn generate(self, source: &str) -> Result<TokenStream, BuildError> {
        let file = syn::parse_file(source)?;
        generate(self, file.items.into_iter().map(Item::from).collect())
    }
}

fn generate(cfg: Config, items: Vec<Item>) -> Result<TokenStream, BuildError> {
    info!(target = cfg.target, items = items.len(), "generating C API");

    let capi = format_ident!("__capi");
    let mut ctx = Ctx {
        capi: capi.clone(),
        conv: parse_quote!(#capi::internal::conv),
        util: parse_quote!(#capi::internal::util),
        error: parse_quote!(#capi::internal::error),
        err_ty: cfg.err_ty,
        ext_err_ty: cfg.ext_err_ty,
        ty_prefix: cfg.ty_prefix,
        fn_prefix: cfg.fn_prefix,
        defs: cfg.defs,
        hidden: format_ident!("__hidden"),
        imports: format_ident!("__imports"),
        errs: Default::default(),
    };
    let ast = Ast::parse(&mut ctx, items)?;
    ctx.propagate()?;

    let capi = &ctx.capi;
    let err_ty = &ctx.err_ty;
    let ext_err_ty = &ctx.ext_err_ty;

    debug!(
        capi = %Trimmed(capi),
        err_ty = %Trimmed(err_ty),
        ext_err_ty = %Trimmed(ext_err_ty),
        ty_prefix = %ctx.ty_prefix,
        fn_prefix = %ctx.fn_prefix,
        "generating code",
    );

    // `use ...` items.
    let mut imports = Vec::new();
    // Type defs.
    let mut defs = Vec::new();
    // Rust and exported FFI fns.
    let mut fns = Vec::<Fn>::new();
    // Other items from the AST.
    let mut other = Vec::new();

    for node in ast.nodes {
        match node {
            n @ (Node::Alias(_) | Node::Enum(_) | Node::Struct(_) | Node::Union(_)) => defs.push(n),
            Node::FfiFn(f) => fns.push(f.into()),
            Node::RustFn(f) => fns.push(f.into()),
            Node::Other(Item::Use(mut v)) => {
                v.vis = parse_quote!(pub(super));
                imports.push(v);
            }
            Node::Other(v) => other.push(v),
        }
    }
    let hidden = &ast.hidden;

    let mod_imports = &ctx.imports;
    let mod_hidden = &ctx.hidden;

    let code = quote! {
        /// This code is @generated by `aranya-capi-codegen`. DO NOT EDIT.
        const _: () = ();

        extern crate aranya_capi_core as #capi;

        use #capi::Builder;
        use #capi::internal::tracing;

        mod #mod_imports {
            #(#imports)*
        }

        #(#defs)*
        #(#fns)*
        #(#other)*

        #[allow(deprecated)]
        mod #mod_hidden {
            #[allow(clippy::wildcard_imports)]
            use super::*;

            #hidden
        }
    };

    dump(&code, "/tmp/expand-capi-codegen.rs");

    Ok(code)
}

/// Formats a [`TokenStream`] as a string.
pub fn format(tokens: &TokenStream) -> String {
    let mut data = tokens.to_string();
    if let Ok(file) = syn::parse_file(&data) {
        data = prettyplease::unparse(&file);
    }
    data
}

#[doc(hidden)]
#[allow(clippy::panic, reason = "This is debugging `build.rs` code")]
pub fn dump(code: &TokenStream, path: &str) {
    if !cfg!(feature = "debug") {
        return;
    }
    let data = format(code);
    File::create(path)
        .unwrap_or_else(|_| panic!("unable to create `{path}`"))
        .write_all(data.as_bytes())
        .unwrap_or_else(|_| panic!("unable to write all data to `{path}`"));
}