fluidattacks-blends-domain 0.32.0

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

use crate::NodeId;

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum ScopeType {
    Block,
    Class,
    Declaration,
    File,
    Function,
    Namespace,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct StackScope {
    pub is_exported: bool,
    pub scope_type: Option<ScopeType>,
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct SymbolPush {
    pub symbol: String,
    pub precedence: i64,
    pub is_reference: bool,
}

#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct Definition {
    pub is_private: bool,
    pub visibility_guard: Option<String>,
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct SymbolPop {
    pub symbol: String,
    pub precedence: i64,
    pub definition: Option<Definition>,
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ImportBinding {
    pub module_stem: String,
    pub source_symbol: Option<String>,
    pub relative_level: u32,
    pub binds_module: bool,
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub enum StackNode {
    DropScopes,
    JumpTo,
    PopScopedSymbol {
        pop: SymbolPop,
        import: Option<ImportBinding>,
    },
    PopSymbol(SymbolPop),
    PushScopedSymbol {
        push: SymbolPush,
        scope: NodeId,
    },
    PushSymbol(SymbolPush),
    Root,
    Scope(StackScope),
}

impl StackNode {
    #[must_use]
    pub fn symbol(&self) -> Option<&str> {
        match self {
            Self::PushScopedSymbol { push, .. } | Self::PushSymbol(push) => Some(&push.symbol),
            Self::PopScopedSymbol { pop, .. } | Self::PopSymbol(pop) => Some(&pop.symbol),
            Self::DropScopes | Self::JumpTo | Self::Root | Self::Scope(_) => None,
        }
    }

    #[must_use]
    pub const fn precedence(&self) -> i64 {
        match self {
            Self::PushScopedSymbol { push, .. } | Self::PushSymbol(push) => push.precedence,
            Self::PopScopedSymbol { pop, .. } | Self::PopSymbol(pop) => pop.precedence,
            Self::DropScopes | Self::JumpTo | Self::Root | Self::Scope(_) => 0,
        }
    }

    #[must_use]
    pub const fn is_reference(&self) -> bool {
        match self {
            Self::PushScopedSymbol { push, .. } | Self::PushSymbol(push) => push.is_reference,
            Self::DropScopes
            | Self::JumpTo
            | Self::PopScopedSymbol { .. }
            | Self::PopSymbol(_)
            | Self::Root
            | Self::Scope(_) => false,
        }
    }

    #[must_use]
    pub const fn is_definition(&self) -> bool {
        match self {
            Self::PopScopedSymbol { pop, .. } | Self::PopSymbol(pop) => pop.definition.is_some(),
            Self::DropScopes
            | Self::JumpTo
            | Self::PushScopedSymbol { .. }
            | Self::PushSymbol(_)
            | Self::Root
            | Self::Scope(_) => false,
        }
    }

    #[must_use]
    pub const fn import_binding(&self) -> Option<&ImportBinding> {
        match self {
            Self::PopScopedSymbol { import, .. } => import.as_ref(),
            Self::DropScopes
            | Self::JumpTo
            | Self::PopSymbol(_)
            | Self::PushScopedSymbol { .. }
            | Self::PushSymbol(_)
            | Self::Root
            | Self::Scope(_) => None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        Definition, ImportBinding, ScopeType, StackNode, StackScope, SymbolPop, SymbolPush,
    };
    use crate::NodeId;
    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()),
        }
    }

    fn scoped_pop(symbol: &str) -> StackNode {
        StackNode::PopScopedSymbol {
            pop: pop(symbol),
            import: None,
        }
    }

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

    #[test]
    fn symbol_nodes_carry_their_symbol() {
        let scoped_push = StackNode::PushScopedSymbol {
            push: push("send"),
            scope: NodeId(4),
        };

        assert_eq!(StackNode::PushSymbol(push("send")).symbol(), Some("send"));
        assert_eq!(scoped_push.symbol(), Some("send"));
        assert_eq!(StackNode::PopSymbol(pop("utils")).symbol(), Some("utils"));
        assert_eq!(scoped_pop("send").symbol(), Some("send"));
        assert_eq!(StackNode::Root.symbol(), None);
        assert_eq!(file_scope().symbol(), None);
    }

    #[test]
    fn nodes_without_a_symbol_take_no_precedence() {
        assert_eq!(scoped_pop("send").precedence(), 1);
        assert_eq!(StackNode::PushSymbol(push("send")).precedence(), 0);
        assert_eq!(StackNode::JumpTo.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::PushScopedSymbol {
            push: push("send"),
            scope: NodeId(4),
        }
        .is_reference());
        assert!(!StackNode::PushSymbol(chain_link).is_reference());
        assert!(!scoped_pop("send").is_reference());
        assert!(!StackNode::DropScopes.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!(scoped_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_scoped_pops_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::PopScopedSymbol {
            pop: pop("send"),
            import: Some(binding.clone()),
        };

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