saja 0.1.0

Zero-configuration C build system
/*
 * Parse out a list of exports given a source file with a method for
 * generating a forward declaration.
 *
 * Copyright (C) 2026  Madeleine Choi
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

use std::path::{Path, PathBuf};

use clang::{Entity, EntityKind, EntityVisitResult, Index};

use crate::Saja;

impl Saja {
    pub fn extract(&self, source: &Path, target: &Path) -> anyhow::Result<Vec<Export>> {
        let index = Index::new(&self.clang, false, false);
        let tu = index
            .parser(source)
            .arguments(self.profiles.get("syntax").unwrap())
            .parse()?;
        let mut exports = vec![];

        tu.get_entity().visit_children(|entity, _parent| {
            let is_public = entity.get_children().iter().any(|child| {
                child.get_kind() == EntityKind::AnnotateAttr
                    && child.get_display_name().is_some_and(|n| n == "public")
            });

            if is_public && let Some(name) = entity.get_name() {
                if let Ok(decl) = Declaration::try_from(entity) {
                    exports.push(Export {
                        name,
                        module: source
                            .strip_prefix(target.join("src"))
                            .unwrap()
                            .to_path_buf(),
                        decl,
                    });
                } else {
                    return EntityVisitResult::Continue;
                }
            }

            EntityVisitResult::Continue
        });

        Ok(exports)
    }
}

#[derive(Debug)]
pub enum Declaration {
    Struct,
    Enum,
    Union,
    Function {
        return_type: String,
        params: Vec<(String, String)>,
    },
    Typedef {
        underlying: String,
        source: String,
    },
    Variable {
        ty: String,
    },
}

impl<'a> TryFrom<Entity<'a>> for Declaration {
    type Error = ();

    fn try_from(entity: Entity) -> Result<Self, Self::Error> {
        match entity.get_kind() {
            EntityKind::StructDecl => Ok(Declaration::Struct),

            EntityKind::EnumDecl => Ok(Declaration::Enum),

            EntityKind::UnionDecl => Ok(Declaration::Union),

            EntityKind::FunctionDecl => {
                let result_ty = entity
                    .get_result_type()
                    .map(|t| t.get_display_name())
                    .unwrap_or("void".into());

                let params = entity
                    .get_arguments()
                    .unwrap_or_default()
                    .into_iter()
                    .map(|arg| {
                        (
                            arg.get_type()
                                .map(|t| t.get_display_name())
                                .unwrap_or_default(),
                            arg.get_name().unwrap_or_default(),
                        )
                    })
                    .collect();

                Ok(Declaration::Function {
                    return_type: result_ty,
                    params,
                })
            }

            EntityKind::TypedefDecl => {
                let underlying = entity
                    .get_typedef_underlying_type()
                    .map(|t| t.get_display_name())
                    .unwrap_or_default();

                let source = entity
                    .get_range()
                    .map(|range| {
                        range
                            .tokenize()
                            .into_iter()
                            .map(|token| token.get_spelling())
                            .filter(|token| token != "public")
                            .collect::<Vec<_>>()
                            .join(" ")
                    })
                    .unwrap_or_default();

                Ok(Declaration::Typedef { underlying, source })
            }

            EntityKind::VarDecl => {
                let ty = entity
                    .get_type()
                    .map(|t| t.get_display_name())
                    .unwrap_or_default();

                Ok(Declaration::Variable { ty })
            }

            _ => Err(()),
        }
    }
}

#[derive(Debug)]
pub struct Export {
    pub name: String,
    pub module: PathBuf,
    pub decl: Declaration,
}

impl Export {
    fn format_param(ty: &str, name: &str) -> String {
        if let Some((base, suffix)) = ty.split_once('[') {
            return format!("{base} {name}[{suffix}");
        }

        format!("{ty} {name}")
    }

    pub fn forward(&self) -> String {
        match &self.decl {
            Declaration::Struct => format!("struct {};", self.name),

            Declaration::Enum => format!("enum {};", self.name),

            Declaration::Union => format!("union {};", self.name),

            Declaration::Function {
                return_type,
                params,
            } => {
                let params = params
                    .iter()
                    .map(|(ty, name)| Self::format_param(ty, name))
                    .collect::<Vec<_>>()
                    .join(", ");

                format!("{return_type} {}({params});", self.name)
            }

            Declaration::Typedef { underlying, source } if source.is_empty() => {
                format!("typedef {underlying} {};", self.name)
            }

            Declaration::Typedef { source, .. } => {
                format!("{source};")
            }

            Declaration::Variable { ty } => {
                format!("extern {ty} {};", self.name)
            }
        }
    }
}