inauguration 0.2.0

.in language and general compiler CLI (Core IR, hybrid SIL, staging, plugins)
Documentation
use std::fs;
use std::path::Path;

use crate::in_lang_parse::InExternBinding;
use crate::package_install::{INSTALLED_PACKAGE_METADATA, InstalledPackageMetadata};
use crate::package_lock::PackageLock;
use crate::package_manifest::{
    PackageDependency, PackageManifest, PackageSemanticImport, resolve_dependency_install_path,
};

pub fn extern_language_for_ecosystem(ecosystem: &str) -> &'static str {
    match ecosystem {
        "cargo" => "rust",
        "npm" => "js",
        "pypi" => "python",
        "go" => "go",
        "composer" => "php",
        "nuget" => "dotnet",
        "gem" => "ruby",
        "hex" => "elixir",
        "v" => "v",
        _ => "package",
    }
}

pub fn load_installed_exports(install_path: &Path) -> Vec<String> {
    let metadata_path = install_path.join(INSTALLED_PACKAGE_METADATA);
    let Ok(source) = fs::read_to_string(&metadata_path) else {
        return Vec::new();
    };
    let Ok(metadata) = serde_json::from_str::<InstalledPackageMetadata>(&source) else {
        return Vec::new();
    };
    metadata.exports
}

pub fn export_symbols_for_resolved_import(
    import: &PackageSemanticImport,
    package_root: &Path,
    manifest: &PackageManifest,
    lock: Option<&PackageLock>,
) -> Vec<String> {
    let Some(dependency_key) = import.dependency.as_ref() else {
        return Vec::new();
    };
    let Some(dependency) = manifest.dependencies.get(dependency_key) else {
        return Vec::new();
    };
    let Some(install_path) =
        resolve_dependency_install_path(package_root, dependency_key, dependency, lock)
    else {
        return Vec::new();
    };
    load_installed_exports(&install_path)
}

pub fn package_import_bindings_for_semantic_import(
    import: &str,
    package_root: &Path,
    manifest: &PackageManifest,
    lock: Option<&PackageLock>,
) -> Vec<InExternBinding> {
    let resolved =
        crate::package_manifest::resolve_semantic_imports(&[import.to_string()], Some(manifest))
            .into_iter()
            .next()
            .unwrap_or(crate::package_manifest::PackageSemanticImport {
                import: import.to_string(),
                dependency: None,
                status: "unresolved".to_string(),
                reason: "package-manifest-missing".to_string(),
            });
    if resolved.status != "resolved" {
        return Vec::new();
    }
    let dependency_key = match resolved.dependency.as_ref() {
        Some(key) => key,
        None => return Vec::new(),
    };
    let dependency = match manifest.dependencies.get(dependency_key) {
        Some(dep) => dep,
        None => return Vec::new(),
    };
    bindings_for_dependency(package_root, dependency_key, dependency, lock)
}

pub fn package_import_bindings_for_source(source_path: &Path) -> Vec<InExternBinding> {
    let Ok((root, manifest)) =
        crate::package_manifest::load_package_manifest_from_source(source_path)
    else {
        return Vec::new();
    };
    let lock = crate::package_lock::discover_package_lock(&root.root)
        .and_then(|lock_root| crate::package_lock::load_package_lock(&lock_root.lock_path).ok());
    let Ok(surface) = crate::in_lang_parse::parse_in_surface_info(
        &fs::read_to_string(source_path).unwrap_or_default(),
    ) else {
        return Vec::new();
    };
    let mut bindings = Vec::new();
    for import in &surface.semantic_imports {
        bindings.extend(package_import_bindings_for_semantic_import(
            import,
            &root.root,
            &manifest,
            lock.as_ref(),
        ));
    }
    bindings
}

fn bindings_for_dependency(
    package_root: &Path,
    dependency_key: &str,
    dependency: &PackageDependency,
    lock: Option<&PackageLock>,
) -> Vec<InExternBinding> {
    let install_path =
        match resolve_dependency_install_path(package_root, dependency_key, dependency, lock) {
            Some(path) => path,
            None => return Vec::new(),
        };
    let metadata_path = install_path.join(INSTALLED_PACKAGE_METADATA);
    let metadata = match fs::read_to_string(&metadata_path)
        .ok()
        .and_then(|source| serde_json::from_str::<InstalledPackageMetadata>(&source).ok())
    {
        Some(metadata) => metadata,
        None => {
            let package_ref =
                crate::package_ref::package_ref_for_dependency(dependency_key, dependency);
            let language = package_ref
                .as_ref()
                .map(|package_ref| {
                    extern_language_for_ecosystem(&package_ref.ecosystem).to_string()
                })
                .unwrap_or_else(|| "package".to_string());
            let exports = package_ref
                .map(|package_ref| vec![crate::package_install::export_symbol_for(&package_ref)])
                .unwrap_or_default();
            return exports
                .into_iter()
                .map(|name| InExternBinding {
                    language: language.clone(),
                    name,
                    required_capabilities: vec!["package.invoke".into()],
                    ret: None,
                })
                .collect();
        }
    };
    metadata
        .bindings
        .iter()
        .map(|binding| InExternBinding {
            language: extern_language_for_ecosystem(&metadata.ecosystem).to_string(),
            name: binding.symbol.clone(),
            required_capabilities: vec!["package.invoke".into()],
            ret: crate::package_runtime::binding_return_type(binding),
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::package_install::INSTALLED_PACKAGE_METADATA;
    use crate::package_manifest::PACKAGE_MANIFEST_FILE;

    #[test]
    fn synthesizes_bindings_from_installed_metadata() {
        let temp = std::env::temp_dir().join(format!(
            "package-extern-{}-{}",
            std::process::id(),
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .expect("clock")
                .as_nanos()
        ));
        let install_path = temp.join("vendor/pypi/flask");
        fs::create_dir_all(&install_path).expect("vendor dir");
        fs::write(
            install_path.join(INSTALLED_PACKAGE_METADATA),
            r#"{"ecosystem":"pypi","name":"flask","version":"vendor","registry":"path","install_path":"vendor/pypi/flask","exports":["flask_greet"],"bindings":[{"symbol":"flask_greet","returns":"string","invoke":{"program":"python3","args":["-c","from greet import greet; import sys; sys.stdout.write(greet())"]}}]}"#,
        )
        .expect("metadata");
        fs::write(
            install_path.join("greet.py"),
            "def greet():\n    return 'flask'\n",
        )
        .expect("greet");
        fs::write(
            temp.join(PACKAGE_MANIFEST_FILE),
            "name: demo\nversion: 0.1.0\ndependencies:\n  pypi:flask:\n    version: path:vendor/pypi/flask\n    kind: pypi\n",
        )
        .expect("manifest");
        let manifest = crate::package_manifest::load_package_manifest(&temp).expect("manifest");
        let bindings =
            package_import_bindings_for_semantic_import("pip:flask", &temp, &manifest, None);
        assert_eq!(bindings.len(), 1);
        assert_eq!(bindings[0].name, "flask_greet");
        assert_eq!(bindings[0].language, "python");
        let _ = fs::remove_dir_all(temp);
    }
}