fluidattacks-blends-domain 0.6.0

Blends functional core: pure AST graph to syntax graph (no_std)
Documentation
//! Counterpart of `blends/query/scope.py`.

use alloc::collections::BTreeSet;
use alloc::vec::Vec;

use crate::syntax::SyntaxGraph;
use crate::NodeId;

fn is_self_ref(graph: &SyntaxGraph, def_nid: NodeId, lookup_nid: NodeId) -> bool {
    graph.nodes.get(&def_nid).is_some_and(|node| {
        node.value_id() == Some(lookup_nid) || node.variable_id() == Some(lookup_nid)
    })
}

fn is_valid_def(graph: &SyntaxGraph, def_nid: NodeId, lookup_nid: NodeId) -> bool {
    !is_self_ref(graph, def_nid, lookup_nid) && def_nid < lookup_nid
}

fn dedup_by_subpath(graph: &SyntaxGraph, definitions: Vec<NodeId>) -> Vec<NodeId> {
    let mut seen_subpaths: BTreeSet<Option<NodeId>> = BTreeSet::new();
    definitions
        .into_iter()
        .filter(|def_nid| {
            let subpath = graph.subpath_index.get(def_nid).copied().flatten();
            seen_subpaths.insert(subpath)
        })
        .collect()
}

fn valid_defs_in_scope(
    graph: &SyntaxGraph,
    def_nids: &[NodeId],
    lookup_nid: NodeId,
) -> Option<Vec<NodeId>> {
    let valid_defs: Vec<NodeId> = def_nids
        .iter()
        .copied()
        .filter(|&def_nid| is_valid_def(graph, def_nid, lookup_nid))
        .collect();
    (!valid_defs.is_empty()).then(|| dedup_by_subpath(graph, valid_defs))
}

fn find_defs_in_scope(
    graph: &SyntaxGraph,
    symbol: &str,
    scope: NodeId,
    lookup_nid: NodeId,
) -> Option<Vec<NodeId>> {
    let direct = graph
        .symbol_index
        .get(symbol)
        .and_then(|scope_defs| scope_defs.get(&scope))
        .and_then(|def_nids| valid_defs_in_scope(graph, def_nids, lookup_nid));
    if direct.is_some() {
        return direct;
    }

    graph
        .symbol_index
        .iter()
        .filter(|(compound_key, _)| {
            compound_key.contains(',') && compound_key.split(',').any(|part| part == symbol)
        })
        .find_map(|(_, scope_defs)| {
            scope_defs
                .get(&scope)
                .and_then(|def_nids| valid_defs_in_scope(graph, def_nids, lookup_nid))
        })
}

#[must_use]
pub fn get_all_scope_definitions(graph: &SyntaxGraph, lookup_nid: NodeId) -> Vec<NodeId> {
    let Some(node) = graph.nodes.get(&lookup_nid) else {
        return Vec::new();
    };
    let (Some(symbol), Some(scope)) = (
        node.symbol().filter(|symbol| !symbol.is_empty()),
        node.symbol_scope(),
    ) else {
        return Vec::new();
    };

    let mut current_scope = scope;
    loop {
        if let Some(definitions) = find_defs_in_scope(graph, symbol, current_scope, lookup_nid) {
            return definitions;
        }
        match graph.scope_parent.get(&current_scope) {
            Some(parent_scope) => current_scope = *parent_scope,
            None => return Vec::new(),
        }
    }
}

#[cfg(test)]
mod tests {
    use alloc::borrow::ToOwned;
    use alloc::vec;
    use alloc::vec::Vec;

    use super::get_all_scope_definitions;
    use crate::syntax::{SyntaxGraph, SyntaxNode};
    use crate::NodeId;

    const SCOPE: NodeId = NodeId(1);
    const PARENT_SCOPE: NodeId = NodeId(0);
    const LOOKUP: NodeId = NodeId(50);

    fn declaration(value_id: Option<NodeId>, variable_id: Option<NodeId>) -> SyntaxNode {
        SyntaxNode::VariableDeclaration {
            variable: "x".to_owned(),
            variable_type: None,
            value_id,
            variable_id,
            access_modifier: None,
        }
    }

    fn graph_with_lookup(symbol: &str, symbol_scope: Option<NodeId>) -> SyntaxGraph {
        let mut graph = SyntaxGraph::new();
        graph.add_node(
            LOOKUP,
            SyntaxNode::SymbolLookup {
                symbol: symbol.to_owned(),
                symbol_scope,
                value: None,
            },
        );
        graph
    }

    fn index(graph: &mut SyntaxGraph, symbol: &str, scope: NodeId, defs: Vec<NodeId>) {
        for def_nid in &defs {
            if !graph.nodes.contains_key(def_nid) {
                graph.add_node(*def_nid, declaration(None, None));
            }
        }
        graph
            .symbol_index
            .entry(symbol.to_owned())
            .or_default()
            .insert(scope, defs);
    }

    #[test]
    fn resolves_a_definition_in_the_lookup_scope() {
        let mut graph = graph_with_lookup("x", Some(SCOPE));
        index(&mut graph, "x", SCOPE, vec![NodeId(10)]);

        assert_eq!(get_all_scope_definitions(&graph, LOOKUP), vec![NodeId(10)]);
    }

    #[test]
    fn climbs_the_scope_chain_to_a_parent_definition() {
        let mut graph = graph_with_lookup("x", Some(SCOPE));
        graph.scope_parent.insert(SCOPE, PARENT_SCOPE);
        index(&mut graph, "x", PARENT_SCOPE, vec![NodeId(10)]);

        assert_eq!(get_all_scope_definitions(&graph, LOOKUP), vec![NodeId(10)]);
    }

    #[test]
    fn the_lookup_scope_shadows_the_parent_definition() {
        let mut graph = graph_with_lookup("x", Some(SCOPE));
        graph.scope_parent.insert(SCOPE, PARENT_SCOPE);
        index(&mut graph, "x", PARENT_SCOPE, vec![NodeId(10)]);
        index(&mut graph, "x", SCOPE, vec![NodeId(20)]);

        assert_eq!(get_all_scope_definitions(&graph, LOOKUP), vec![NodeId(20)]);
    }

    #[test]
    fn ignores_definitions_declared_after_the_lookup() {
        let mut graph = graph_with_lookup("x", Some(SCOPE));
        index(&mut graph, "x", SCOPE, vec![NodeId(60)]);

        assert_eq!(get_all_scope_definitions(&graph, LOOKUP), Vec::new());
    }

    #[test]
    fn skips_definitions_referencing_the_lookup_itself() {
        let mut graph = graph_with_lookup("x", Some(SCOPE));
        graph.add_node(NodeId(10), declaration(Some(LOOKUP), None));
        graph.add_node(NodeId(20), declaration(None, Some(LOOKUP)));
        index(
            &mut graph,
            "x",
            SCOPE,
            vec![NodeId(10), NodeId(20), NodeId(30)],
        );

        assert_eq!(get_all_scope_definitions(&graph, LOOKUP), vec![NodeId(30)]);
    }

    #[test]
    fn keeps_the_first_definition_per_subpath() {
        let mut graph = graph_with_lookup("x", Some(SCOPE));
        graph.subpath_index.insert(NodeId(10), Some(NodeId(9)));
        graph.subpath_index.insert(NodeId(20), Some(NodeId(9)));
        index(&mut graph, "x", SCOPE, vec![NodeId(10), NodeId(20)]);

        assert_eq!(get_all_scope_definitions(&graph, LOOKUP), vec![NodeId(10)]);
    }

    #[test]
    fn resolves_a_symbol_inside_a_compound_key() {
        let mut graph = graph_with_lookup("value", Some(SCOPE));
        index(&mut graph, "done,value", SCOPE, vec![NodeId(10)]);

        assert_eq!(get_all_scope_definitions(&graph, LOOKUP), vec![NodeId(10)]);
    }

    #[test]
    fn a_compound_fragment_match_does_not_count() {
        let mut graph = graph_with_lookup("val", Some(SCOPE));
        index(&mut graph, "done,value", SCOPE, vec![NodeId(10)]);

        assert_eq!(get_all_scope_definitions(&graph, LOOKUP), Vec::new());
    }

    #[test]
    fn returns_empty_without_a_symbol_scope() {
        let mut graph = graph_with_lookup("x", None);
        index(&mut graph, "x", SCOPE, vec![NodeId(10)]);

        assert_eq!(get_all_scope_definitions(&graph, LOOKUP), Vec::new());
    }

    #[test]
    fn returns_empty_for_an_empty_symbol() {
        let mut graph = graph_with_lookup("", Some(SCOPE));
        index(&mut graph, "", SCOPE, vec![NodeId(10)]);

        assert_eq!(get_all_scope_definitions(&graph, LOOKUP), Vec::new());
    }

    #[test]
    fn returns_empty_for_a_missing_lookup_node() {
        let graph = SyntaxGraph::new();

        assert_eq!(get_all_scope_definitions(&graph, LOOKUP), Vec::new());
    }
}