fluidattacks-blends-domain 0.41.0

Blends functional core: pure AST graph to syntax graph (no_std)
Documentation
use alloc::string::String;

/// The lexical construct a [`StackScope`] stands for.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[allow(
    clippy::exhaustive_enums,
    reason = "a fixed ontology: the lexical scope kinds the stack graph model distinguishes"
)]
pub enum ScopeType {
    /// A `{}`-delimited block (loop body, conditional branch, ...).
    Block,
    /// A class or struct body.
    Class,
    /// A function/method/variable declaration's own scope.
    Declaration,
    /// A whole source file.
    File,
    /// A function or method body.
    Function,
    /// A namespace or module body.
    Namespace,
}

/// A scope node: where name lookups stop descending further down the stack.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
#[non_exhaustive]
pub struct StackScope {
    /// The lexical construct this scope stands for, when known.
    pub scope_type: Option<ScopeType>,
}

/// A reference: a symbol pushed onto the stack to be resolved against a definition.
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct SymbolPush {
    /// The referenced symbol's name.
    pub symbol: String,
    /// Resolution order among competing pushes; lower wins.
    pub precedence: i64,
    /// Whether this push is a reference (as opposed to an internal relay).
    pub is_reference: bool,
}

/// What makes a [`SymbolPop`] a definition rather than a plain re-export.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
#[non_exhaustive]
pub struct Definition {
    /// Whether the definition is private to its declaring module.
    pub is_private: bool,
    /// The name of the symbol that must be in scope to see this definition, if any.
    pub visibility_guard: Option<String>,
}

/// A definition: a symbol popped off the stack, ending a reference's search.
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct SymbolPop {
    /// The defined symbol's name.
    pub symbol: String,
    /// Resolution order among competing pops; lower wins.
    pub precedence: i64,
    /// What this pop defines, when it is a true definition rather than a relay.
    pub definition: Option<Definition>,
    /// The import this definition binds, when it comes from an import.
    pub import: Option<ImportBinding>,
}

/// How an import's [`SymbolPop`] binds the module it imports from.
#[derive(Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub struct ImportBinding {
    /// The imported module's own name (the last path segment).
    pub module_stem: String,
    /// The symbol imported from that module, when it names one.
    pub source_symbol: Option<String>,
    /// How many parent directories a relative import climbs.
    pub relative_level: u32,
    /// Whether the whole module (not just one symbol) is bound.
    pub binds_module: bool,
}

/// One node of a file's stack graph: a step in the push/pop path name
/// resolution walks to connect a reference to its definition.
#[derive(Clone, PartialEq, Eq, Debug)]
#[allow(
    clippy::exhaustive_enums,
    reason = "a fixed ontology: the stack-graphs algorithm's node kinds"
)]
pub enum StackNode {
    /// Pops a symbol, ending the search if it is a definition.
    PopSymbol(SymbolPop),
    /// Pushes a symbol to be resolved against a later pop.
    PushSymbol(SymbolPush),
    /// The path's starting point.
    Root,
    /// A lexical scope a path may enter while searching for a definition.
    Scope(StackScope),
}

impl StackNode {
    /// The symbol this node pushes or pops, for symbol-carrying variants.
    #[must_use]
    pub fn symbol(&self) -> Option<&str> {
        match self {
            Self::PushSymbol(push) => Some(&push.symbol),
            Self::PopSymbol(pop) => Some(&pop.symbol),
            Self::Root | Self::Scope(_) => None,
        }
    }

    /// This node's resolution precedence; 0 for variants without one.
    #[must_use]
    pub const fn precedence(&self) -> i64 {
        match self {
            Self::PushSymbol(push) => push.precedence,
            Self::PopSymbol(pop) => pop.precedence,
            Self::Root | Self::Scope(_) => 0,
        }
    }

    /// Whether this node is a reference push (as opposed to an internal relay).
    #[must_use]
    pub const fn is_reference(&self) -> bool {
        match self {
            Self::PushSymbol(push) => push.is_reference,
            Self::PopSymbol(_) | Self::Root | Self::Scope(_) => false,
        }
    }

    /// Whether this node pops a true definition (as opposed to a relay or a reference).
    #[must_use]
    pub const fn is_definition(&self) -> bool {
        match self {
            Self::PopSymbol(pop) => pop.definition.is_some(),
            Self::PushSymbol(_) | Self::Root | Self::Scope(_) => false,
        }
    }

    /// The import this definition binds, when it comes from an import.
    #[must_use]
    pub const fn import_binding(&self) -> Option<&ImportBinding> {
        match self {
            Self::PopSymbol(pop) => pop.import.as_ref(),
            Self::PushSymbol(_) | Self::Root | Self::Scope(_) => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        Definition, ImportBinding, ScopeType, StackNode, StackScope, SymbolPop, SymbolPush,
    };
    use alloc::borrow::ToOwned;

    fn push(symbol: &str) -> SymbolPush {
        SymbolPush {
            symbol: symbol.to_owned(),
            precedence: 0,
            is_reference: true,
        }
    }

    fn pop(symbol: &str) -> SymbolPop {
        SymbolPop {
            symbol: symbol.to_owned(),
            precedence: 1,
            definition: Some(Definition::default()),
            import: None,
        }
    }

    fn file_scope() -> StackNode {
        StackNode::Scope(StackScope {
            scope_type: Some(ScopeType::File),
        })
    }

    #[test]
    fn symbol_nodes_carry_their_symbol() {
        assert_eq!(StackNode::PushSymbol(push("send")).symbol(), Some("send"));
        assert_eq!(StackNode::PopSymbol(pop("utils")).symbol(), Some("utils"));
        assert_eq!(StackNode::Root.symbol(), None);
        assert_eq!(file_scope().symbol(), None);
    }

    #[test]
    fn nodes_without_a_symbol_take_no_precedence() {
        assert_eq!(StackNode::PopSymbol(pop("send")).precedence(), 1);
        assert_eq!(StackNode::PushSymbol(push("send")).precedence(), 0);
        assert_eq!(StackNode::Root.precedence(), 0);
        assert_eq!(file_scope().precedence(), 0);
    }

    #[test]
    fn only_marked_push_nodes_are_references() {
        let chain_link = SymbolPush {
            is_reference: false,
            ..push("utils")
        };

        assert!(StackNode::PushSymbol(push("send")).is_reference());
        assert!(!StackNode::PushSymbol(chain_link).is_reference());
        assert!(!StackNode::PopSymbol(pop("send")).is_reference());
        assert!(!StackNode::Root.is_reference());
    }

    #[test]
    fn only_marked_pop_nodes_are_definitions() {
        let path_segment = SymbolPop {
            definition: None,
            ..pop("utils")
        };

        assert!(StackNode::PopSymbol(pop("send")).is_definition());
        assert!(!StackNode::PopSymbol(path_segment).is_definition());
        assert!(!StackNode::PushSymbol(push("send")).is_definition());
        assert!(!file_scope().is_definition());
    }

    #[test]
    fn only_pops_from_imports_bind_imports() {
        let binding = ImportBinding {
            module_stem: "utils".to_owned(),
            source_symbol: Some("send".to_owned()),
            relative_level: 1,
            binds_module: false,
        };
        let import = StackNode::PopSymbol(SymbolPop {
            import: Some(binding.clone()),
            ..pop("send")
        });

        assert_eq!(import.import_binding(), Some(&binding));
        assert!(import.is_definition());
        assert_eq!(StackNode::PopSymbol(pop("send")).import_binding(), None);
        assert_eq!(StackNode::PushSymbol(push("send")).import_binding(), None);
    }
}