alef 0.84.1

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Same-named function entry deduplication for single-surface binding backends.
//!
//! When a Rust crate exposes the same public function under two disjoint `cfg` gates — typically
//! a real implementation re-exported under `#[cfg(feature = "X")]` plus a stub fallback under
//! `#[cfg(all(feature = "X-presets", not(feature = "X")))]` — the extractor preserves both
//! entries in the shared [`ApiSurface`](crate::core::ir::ApiSurface). The shared surface is
//! intentionally NOT collapsed because:
//!
//! 1. The two entries usually carry distinct `rust_path` values (the crate-root stub path vs.
//!    the real-module re-export path), and the e2e call-export validator depends on both being
//!    visible in the shared surface.
//! 2. Collapsing in the extract pass would inherit `#[cfg_attr(alef, alef(skip))]` from
//!    whichever entry was selected as canonical, causing the merged result to be silently
//!    stripped by the exclusion filter and disappearing from every backend.
//!
//! Every emitting backend therefore deduplicates locally instead. Both the Rust-cfg-gated backends
//! (FFI, napi, pyo3, wasm) and the **single-surface** backends (Java, C#, Go, Kotlin, Swift, Dart,
//! PHP, Ruby, Elixir) emit ONE non-`#[cfg]`-gated wrapper per function that delegates to the core
//! crate (which resolves the cfg itself) or to a single FFI symbol. Emitting both cfg-variants
//! there produces two items with identical signatures — a duplicate-definition compile error
//! (`#[pyfunction]`/`#[napi]`/`#[wasm_bindgen]` E0428, or duplicate host methods).
//!
//! [`dedup_same_name_functions`] resolves this for every emitting backend: it groups by `name`,
//! picks the canonical entry (preferring real impls — entries whose params are not all
//! `_`-prefixed), and rewrites its `cfg` to the OR of every group member's cfg. The pass is a
//! pure transformation on the input slice and never mutates it.

use crate::core::ir::FunctionDef;
use ahash::{AHashMap, AHashSet};

/// Returns a deduplicated `Vec<FunctionDef>` derived from `functions`.
///
/// Functions whose `name` is unique in the input pass through unchanged. Functions sharing a
/// `name` with at least one other entry are collapsed into a single canonical entry whose
/// `cfg` is the OR (`any(...)`) of every group member's cfg. See the module-level docs for the
/// canonical-pick heuristic and the merge rules.
///
/// The relative order of canonical entries follows the position of each group's first member
/// in the input slice.
pub fn dedup_same_name_functions(functions: &[FunctionDef]) -> Vec<FunctionDef> {
    let groups = collect_function_groups(functions);
    let groups_to_merge = groups_to_merge(&groups, functions);
    if groups_to_merge.is_empty() {
        return functions.to_vec();
    }

    let mut canonical_by_first_index: AHashMap<usize, FunctionDef> = AHashMap::new();
    let mut skipped_indices: AHashSet<usize> = AHashSet::new();
    for indices in &groups_to_merge {
        let merged_cfg = merge_cfgs(indices.iter().map(|&i| functions[i].cfg.as_deref()));
        let canonical_idx = pick_canonical_entry(indices, functions);
        let mut canonical = functions[canonical_idx].clone();
        canonical.cfg = merged_cfg;

        let first_idx = *indices.iter().min().expect("merge group indices are non-empty");
        canonical_by_first_index.insert(first_idx, canonical);

        for &idx in indices {
            if idx != first_idx {
                skipped_indices.insert(idx);
            }
        }
    }

    let mut merged_functions = Vec::with_capacity(functions.len() - skipped_indices.len());
    for (idx, function) in functions.iter().cloned().enumerate() {
        if let Some(canonical) = canonical_by_first_index.remove(&idx) {
            merged_functions.push(canonical);
        } else if !skipped_indices.contains(&idx) {
            merged_functions.push(function);
        }
    }
    merged_functions
}

fn collect_function_groups(functions: &[FunctionDef]) -> AHashMap<String, Vec<usize>> {
    let mut name_to_indices: AHashMap<String, Vec<usize>> = AHashMap::new();
    for (idx, func) in functions.iter().enumerate() {
        name_to_indices.entry(func.name.clone()).or_default().push(idx);
    }
    name_to_indices
}

fn groups_to_merge(groups: &AHashMap<String, Vec<usize>>, functions: &[FunctionDef]) -> Vec<Vec<usize>> {
    groups
        .values()
        .filter(|indices| should_merge_cfg_group(indices, functions))
        .cloned()
        .collect()
}

fn should_merge_cfg_group(indices: &[usize], functions: &[FunctionDef]) -> bool {
    if indices.len() <= 1 {
        return false;
    }
    let first_cfg = &functions[indices[0]].cfg;
    if !indices.iter().any(|&idx| &functions[idx].cfg != first_cfg) {
        return false;
    }
    if real_variant_signatures_agree(indices, functions) {
        return true;
    }
    warn_on_signature_disagreement(indices, functions);
    false
}

/// True when every "real" (non-stub) entry in a same-name, differing-cfg group agrees on
/// parameter arity, parameter types, and return type.
///
/// The stub-fallback convention (all params `_`-prefixed) marks an entry's own signature as a
/// placeholder, so only a REAL entry's signature is trusted as canonical — see
/// `pick_canonical_entry`. But when a group has two or more real entries, which the sanctioned
/// real+stub pattern never produces, there is no basis for picking one signature as the one
/// that applies under every cfg branch. Refusing to merge in that case leaves every entry in
/// the output; downstream emission then declares the same symbol under multiple cfg branches
/// with disagreeing signatures, which fails loudly (duplicate-definition compile/link error)
/// instead of silently emitting a signature that is wrong under some cfg combination. ~keep
fn real_variant_signatures_agree(indices: &[usize], functions: &[FunctionDef]) -> bool {
    let mut real_signatures = indices.iter().filter_map(|&idx| {
        let func = &functions[idx];
        let all_underscore = !func.params.is_empty() && func.params.iter().all(|p| p.name.starts_with('_'));
        (!all_underscore).then_some(func)
    });

    let Some(first) = real_signatures.next() else {
        return true;
    };
    real_signatures.all(|func| signatures_match(first, func))
}

fn signatures_match(a: &FunctionDef, b: &FunctionDef) -> bool {
    a.return_type == b.return_type
        && a.params.len() == b.params.len()
        && a.params.iter().zip(&b.params).all(|(p, q)| p.ty == q.ty)
}

fn warn_on_signature_disagreement(indices: &[usize], functions: &[FunctionDef]) {
    let name = &functions[indices[0]].name;
    let variants: Vec<String> = indices
        .iter()
        .map(|&idx| {
            let func = &functions[idx];
            format!(
                "{rust_path} cfg={cfg:?} params={params:?} returns={returns:?}",
                rust_path = func.rust_path,
                cfg = func.cfg,
                params = func.params.iter().map(|p| &p.ty).collect::<Vec<_>>(),
                returns = func.return_type,
            )
        })
        .collect();
    tracing::warn!(
        function = %name,
        variants = ?variants,
        "same-name functions under disjoint cfg gates disagree on signature; skipping cfg dedup \
         for this group so every variant reaches the emitter unmerged rather than silently \
         picking one signature as canonical for cfg branches it does not describe"
    );
}

/// Compute the OR-merge of a set of cfg strings.
///
/// - If any cfg is `None` (unconditional), returns `None`.
/// - If there is exactly one distinct value, returns it unchanged.
/// - Otherwise wraps all distinct values in `any(...)`.
///
/// Shared with the `dedup_api_surface` type-dedup pass,
/// which collapses same-named real/stub type pairs the same way functions are collapsed.
pub fn merge_cfgs<'a>(cfgs: impl Iterator<Item = Option<&'a str>>) -> Option<String> {
    let mut distinct: Vec<&str> = Vec::new();
    for cfg in cfgs {
        {
            let s = cfg?;
            if !distinct.contains(&s) {
                distinct.push(s);
            }
        }
    }
    match distinct.len() {
        0 => None,
        1 => Some(distinct[0].to_string()),
        _ => Some(format!("any({})", distinct.join(", "))),
    }
}

/// Pick the index of the "canonical" (real) entry from a group.
///
/// Prefers an entry whose params are NOT all underscore-prefixed (the stub convention).
/// Falls back to the first entry in the group.
fn pick_canonical_entry(indices: &[usize], functions: &[FunctionDef]) -> usize {
    for &idx in indices {
        let func = &functions[idx];
        let all_underscore = !func.params.is_empty() && func.params.iter().all(|p| p.name.starts_with('_'));
        if !all_underscore {
            return idx;
        }
    }
    indices[0]
}

#[cfg(test)]
mod tests;