ryo-executor 0.2.0

[experimental] Mutation execution engine for RYO - parallel execution, conflict detection, workspace management
Documentation
//! CrossCrate InlineTrait Phase 2c — caller crate scanner.
//!
//! Given a target trait `SymbolId`, walks the workspace's typeflow
//! graph for containers (functions / impl blocks / etc.) that
//! reference the trait, filters down to **cross-crate** callers (i.e.
//! containers living in a different crate from the trait), and
//! classifies each caller via the `classify_fn` helper from
//! `ryo-mutations` (Phase 2a+2b).
//!
//! Each cross-crate caller produces a [`CallerRewrite`] entry listing
//! the `CallerPattern` variants the executor must apply during the
//! caller-side AST migration in Phase 2d:
//!
//! - `Dot`         — post-inline dispatch is unchanged; no AST edit.
//! - `Ufcs`        — rewrite `<S as Trait>::m(..)` → `S::m(..)`.
//! - `GenericBound`— strip `T: Trait` generic param and specialize.
//! - `DynDispatch` — rewrite `&dyn Trait` → `&S`.
//! - `Other`       — block inline (conservatively).
//!
//! Phase 2c stops at the **scan + classify** boundary. The executor
//! does not rewrite caller crates yet; that engine arrives in Phase
//! 2d, consuming the [`CallerRewrite`] entries produced here.

use ryo_analysis::context::AnalysisContext;
use ryo_analysis::query::CodeGraphV2;
use ryo_analysis::ASTRegistry;
use ryo_mutations::basic::trait_ops::cross_crate_caller_pattern::{classify_fn, CallerPattern};
use ryo_source::pure::{PureItem, PureTraitItem, PureVis};
use ryo_symbol::{SymbolId, SymbolKind, SymbolRegistry};
use std::collections::{HashSet, VecDeque};

/// Per-caller rewrite metadata returned by [`scan_cross_crate_callers`].
#[derive(Debug, Clone)]
pub struct CallerRewrite {
    /// Caller container symbol (typically a `PureFn`).
    pub caller_id: SymbolId,
    /// Patterns detected in this caller; the executor's Phase 2d
    /// engine will run one rewrite path per entry.
    pub patterns: Vec<CallerPattern>,
    /// Phase 4-A: whether the caller is `pub` (visible outside its
    /// own crate). True only for `PureVis::Public`; `pub(crate)`,
    /// `pub(super)`, `pub(in path)`, and private all map to false.
    ///
    /// When this is true AND the caller carries `GenericBound`, the
    /// Phase 2g rewriter changes the caller's external API contract.
    /// Downstream call sites in unrelated crates would have to be
    /// updated. Phase 4-A surfaces this as a warning footnote on the
    /// `MutationResult.description`; Phase 4-B will add the transitive
    /// walk that decides whether the cascade actually leaves the
    /// current workspace.
    pub is_public: bool,
}

/// Scan every cross-crate caller of `trait_id` and classify each by
/// caller-side rewrite pattern.
///
/// "Cross-crate" means the caller's path resolves to a different
/// crate from the trait. Same-crate callers are intentionally
/// ignored — the existing single-crate executor already handles
/// those (or the v3.1 EXTERNAL_USE filter wholesale-skips the
/// suggest, depending on the call site).
///
/// Returns an empty `Vec` when:
/// - `trait_id` is not in the registry or has no name,
/// - no cross-crate container references the trait,
/// - every cross-crate caller's AST is missing or is not a `PureFn`
///   (Phase 2c covers only function callers; struct / impl callers
///   arrive in Phase 2d alongside the rewrite engine).
///
/// Takes a `SymbolRegistry` + `ASTRegistry` directly so it can be
/// invoked from either an `AnalysisContext` (full pipeline) or an
/// `ASTMutationContext` (executor `apply_to_registry` entry point),
/// without depending on `detail_store` / `typeflow_graph` (which
/// `ASTMutationContext` does not carry).
pub fn scan_cross_crate_callers_raw(
    ast_registry: &ASTRegistry,
    symbol_registry: &SymbolRegistry,
    trait_id: SymbolId,
) -> Vec<CallerRewrite> {
    let trait_crate = match symbol_registry.path(trait_id) {
        Some(p) => p.crate_name().to_string(),
        None => return Vec::new(),
    };
    let trait_name = match symbol_registry.path(trait_id) {
        Some(p) => p.name().to_string(),
        None => return Vec::new(),
    };
    if trait_crate.is_empty() || trait_name.is_empty() {
        return Vec::new();
    }

    // Pull trait method names directly off the `PureTrait` AST so this
    // helper does not need a `DetailStore` (which `ASTMutationContext`
    // does not carry).
    let trait_methods: Vec<String> = match ast_registry.get(trait_id) {
        Some(PureItem::Trait(t)) => t
            .items
            .iter()
            .filter_map(|it| match it {
                PureTraitItem::Fn(f) => Some(f.name.clone()),
                _ => None,
            })
            .collect(),
        _ => Vec::new(),
    };

    // Brute-force scan: iterate every Function symbol in the registry,
    // keep only those whose crate differs from the trait's crate, then
    // classify via `classify_fn` against the trait name/methods.
    //
    // Rationale (Phase 0 caveat, RL060 epic): typeflow.type_users drops
    // the container for `WhereBound` / `ParamType` / `ReturnType`
    // usages, which is exactly where cross-crate callers live (generic
    // bound + `&dyn Trait` parameter + return type). Brute-force
    // registry iteration sidesteps the typeflow gap.
    let mut out = Vec::new();
    let fn_ids: Vec<SymbolId> = symbol_registry
        .iter()
        .filter(|(id, path)| {
            matches!(symbol_registry.kind(*id), Some(SymbolKind::Function))
                && path.crate_name() != trait_crate
        })
        .map(|(id, _)| id)
        .collect();

    for caller_id in fn_ids {
        if let Some(PureItem::Fn(f)) = ast_registry.get(caller_id) {
            let patterns = classify_fn(f, &trait_name, &trait_methods);
            if !patterns.is_empty() {
                let is_public = matches!(f.vis, PureVis::Public);
                out.push(CallerRewrite {
                    caller_id,
                    patterns,
                    is_public,
                });
            }
        }
    }
    out
}

/// Convenience wrapper that reads the `ASTRegistry` and
/// `SymbolRegistry` off an `AnalysisContext`. Used by the integration
/// pin and by any caller that already holds a full analysis context.
pub fn scan_cross_crate_callers(ctx: &AnalysisContext, trait_id: SymbolId) -> Vec<CallerRewrite> {
    scan_cross_crate_callers_raw(&ctx.ast_registry, &ctx.registry, trait_id)
}

/// Phase 5-B: symmetric counterpart to [`scan_cross_crate_callers_raw`]
/// that returns the callers living in the **same** crate as the trait
/// (excluding the implementor's own impl block and any of its method
/// children). Used by the suggester to detect the mixed-handling case
/// where same-crate external use coexists with cross-crate callers —
/// in that situation the Phase 3 gate must fall through to the v3.1
/// EXTERNAL_USE filter chain rather than bypassing it.
///
/// `internal_ids` is the set of symbols that should NOT count as
/// external callers (typically `impl_id` plus the `children_of`
/// the impl block). The Phase 3 gate computes this set once and
/// passes it to both this helper and the existing typeflow walk.
pub fn scan_same_crate_external_callers_raw(
    ast_registry: &ASTRegistry,
    symbol_registry: &SymbolRegistry,
    trait_id: SymbolId,
    internal_ids: &HashSet<SymbolId>,
) -> Vec<CallerRewrite> {
    let trait_crate = match symbol_registry.path(trait_id) {
        Some(p) => p.crate_name().to_string(),
        None => return Vec::new(),
    };
    let trait_name = match symbol_registry.path(trait_id) {
        Some(p) => p.name().to_string(),
        None => return Vec::new(),
    };
    if trait_crate.is_empty() || trait_name.is_empty() {
        return Vec::new();
    }

    let trait_methods: Vec<String> = match ast_registry.get(trait_id) {
        Some(PureItem::Trait(t)) => t
            .items
            .iter()
            .filter_map(|it| match it {
                PureTraitItem::Fn(f) => Some(f.name.clone()),
                _ => None,
            })
            .collect(),
        _ => Vec::new(),
    };

    let mut out = Vec::new();
    let fn_ids: Vec<SymbolId> = symbol_registry
        .iter()
        .filter(|(id, path)| {
            matches!(symbol_registry.kind(*id), Some(SymbolKind::Function))
                && path.crate_name() == trait_crate
                && !internal_ids.contains(id)
        })
        .map(|(id, _)| id)
        .collect();

    for caller_id in fn_ids {
        if let Some(PureItem::Fn(f)) = ast_registry.get(caller_id) {
            let patterns = classify_fn(f, &trait_name, &trait_methods);
            if !patterns.is_empty() {
                let is_public = matches!(f.vis, PureVis::Public);
                out.push(CallerRewrite {
                    caller_id,
                    patterns,
                    is_public,
                });
            }
        }
    }
    out
}

/// Convenience wrapper for [`scan_same_crate_external_callers_raw`]
/// that takes an `AnalysisContext`.
pub fn scan_same_crate_external_callers(
    ctx: &AnalysisContext,
    trait_id: SymbolId,
    internal_ids: &HashSet<SymbolId>,
) -> Vec<CallerRewrite> {
    scan_same_crate_external_callers_raw(&ctx.ast_registry, &ctx.registry, trait_id, internal_ids)
}

// ============================================================================
// Phase 4-B — transitive caller cascade verdict
// ============================================================================

/// Result of [`walk_transitive_callers`] — whether the cascade from a
/// pub GenericBound caller can be safely rewritten given the in-
/// workspace call graph.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CascadeVerdict {
    /// Every transitive caller in the workspace eventually terminates
    /// at a non-pub leaf (or has no callers and is itself non-pub).
    /// The GenericBound rewrite can safely commit.
    Safe,
    /// At least one transitive caller is a pub fn whose own callers
    /// are not visible in the workspace registry — i.e. an external
    /// crate may reach it. The GenericBound rewrite must abort for
    /// that caller chain to avoid silently breaking the downstream
    /// API contract.
    Escapes,
}

/// BFS over `code_graph.callers_of` starting at `root_caller_id` to
/// decide whether the transitive caller cascade ever surfaces a pub
/// API leaf with no in-workspace caller.
///
/// Definition: a *leaf* is a symbol whose own callers_of iterator
/// returns nothing. If the leaf's AST visibility is `PureVis::Public`,
/// that means an external crate could call into the cascade and would
/// be broken by the GenericBound rewrite — verdict is `Escapes`.
/// Otherwise the leaf is private (or non-`Public` like `pub(crate)` /
/// `pub(super)`) and the cascade stays inside the workspace — verdict
/// is `Safe` for that branch. The walk continues until either an
/// `Escapes` is found (early-return) or the queue empties (`Safe`).
///
/// Visited set guards against cycles. The root itself is included in
/// the leaf check, so a pub fn with zero callers immediately returns
/// `Escapes`.
pub fn walk_transitive_callers(
    code_graph: &CodeGraphV2,
    ast_registry: &ASTRegistry,
    root_caller_id: SymbolId,
) -> CascadeVerdict {
    let mut visited: HashSet<SymbolId> = HashSet::new();
    let mut queue: VecDeque<SymbolId> = VecDeque::new();
    queue.push_back(root_caller_id);
    visited.insert(root_caller_id);

    while let Some(id) = queue.pop_front() {
        let callers: Vec<SymbolId> = code_graph.callers_of(id).collect();

        if callers.is_empty() {
            // Leaf: if its AST is a `pub fn`, it may be reached from
            // an external crate. Otherwise the cascade is workspace-
            // internal for this branch.
            if let Some(PureItem::Fn(f)) = ast_registry.get(id) {
                if matches!(f.vis, PureVis::Public) {
                    return CascadeVerdict::Escapes;
                }
            }
            continue;
        }

        for caller in callers {
            if visited.insert(caller) {
                queue.push_back(caller);
            }
        }
    }

    CascadeVerdict::Safe
}