Skip to main content

cyrs_ast/
lib.rs

1//! `cyrs-ast` — typed AST wrappers over the CST (spec 0001 §5).
2//!
3//! AST nodes are zero-cost wrappers around [`cyrs_syntax::SyntaxNode`].
4//! A node's methods navigate the underlying tree; no duplication, no
5//! allocation. Per §5.2, the bulk of wrappers is generated from a
6//! grammar description by a dev-only `xtask`; the output lives in the
7//! checked-in [`generated`] module and is regenerated by
8//! `cargo xtask codegen`.
9
10// Embedders: see ../../docs/integration-depth.md before depending on this surface.
11
12#![forbid(unsafe_code)]
13#![doc(html_root_url = "https://docs.rs/cyrs-ast/0.0.1")]
14
15use cyrs_syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
16
17pub mod generated;
18pub use generated::*;
19
20// --- Hand-written extensions ---------------------------------------------
21//
22// The codegen emitter (xtask::codegen) does not yet handle ungrammar
23// alternations whose arms are *sequences* — `MapProjectionItem` falls into
24// that case (`'.' (PropertyKey | '*') | '*' | key:PropertyKey ':' value:Expr`).
25// Until cy-pbx is extended, the wrapper and its child accessors are
26// hand-written here. This block is small and self-contained so a future
27// codegen extension can lift it into `generated.rs` without churn.
28
29/// Typed wrapper for one item inside a [`MapProjection`].
30///
31/// Spec §6.1 / §19 row "Map projection" (cy-01q). Each item is one of
32/// the four [`MapProjectionItemKind`] shapes; classify with [`Self::kind`]
33/// and use the kind-specific accessors.
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub struct MapProjectionItem {
36    syntax: SyntaxNode,
37}
38
39/// The four shapes a map-projection item can take.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum MapProjectionItemKind {
42    /// `.NAME` — property selector. Pulls `subject.NAME` into the
43    /// projection under key `NAME`.
44    PropertySelector,
45    /// `key: Expr` — literal item. Inserts `Expr` under key `key`.
46    Literal,
47    /// `.*` — all-properties spread of the subject.
48    AllPropertiesSpread,
49    /// `*` — all-bound-vars spread (rare openCypher form).
50    AllBoundVarsSpread,
51}
52
53impl MapProjectionItem {
54    /// Try to cast a raw `SyntaxNode` to a `MapProjectionItem`. Returns
55    /// `None` unless `syntax.kind() == MAP_PROJECTION_ITEM`.
56    pub fn cast(syntax: SyntaxNode) -> Option<Self> {
57        (syntax.kind() == SyntaxKind::MAP_PROJECTION_ITEM).then_some(Self { syntax })
58    }
59
60    /// The underlying `SyntaxNode`.
61    pub fn syntax(&self) -> &SyntaxNode {
62        &self.syntax
63    }
64
65    /// Classify this item by inspecting its leading non-trivia tokens.
66    /// The four shapes are unambiguous once we know whether the first
67    /// significant token is `.`, `*`, or an identifier — see the parser's
68    /// `map_projection_item` for the inverse.
69    pub fn kind(&self) -> MapProjectionItemKind {
70        let mut toks = self
71            .syntax
72            .children_with_tokens()
73            .filter_map(SyntaxElement::into_token)
74            .filter(|t| !t.kind().is_trivia());
75        match toks.next().map(|t| t.kind()) {
76            Some(SyntaxKind::DOT) => match toks.next().map(|t| t.kind()) {
77                Some(SyntaxKind::STAR) => MapProjectionItemKind::AllPropertiesSpread,
78                _ => MapProjectionItemKind::PropertySelector,
79            },
80            Some(SyntaxKind::STAR) => MapProjectionItemKind::AllBoundVarsSpread,
81            _ => MapProjectionItemKind::Literal,
82        }
83    }
84
85    /// For `PropertySelector` and `Literal` items, return the bound
86    /// property-key token. Returns `None` for spread forms or malformed
87    /// recovery shapes.
88    pub fn key_token(&self) -> Option<SyntaxToken> {
89        self.syntax
90            .children_with_tokens()
91            .filter_map(SyntaxElement::into_token)
92            .find(|t| matches!(t.kind(), SyntaxKind::IDENT | SyntaxKind::QUOTED_IDENT))
93    }
94
95    /// For `Literal` items only, return the value expression.
96    /// Returns `None` for the other three kinds.
97    pub fn value(&self) -> Option<Expr> {
98        if matches!(self.kind(), MapProjectionItemKind::Literal) {
99            self.syntax.children().find_map(Expr::cast)
100        } else {
101            None
102        }
103    }
104}
105
106/// Hand-written accessor: iterate the [`MapProjectionItem`] children of a
107/// [`MapProjection`]. Lives outside `generated.rs` so codegen runs do not
108/// clobber it.
109impl MapProjection {
110    /// Iterate the projection's items in source order. The receiver
111    /// expression is reachable via the codegen-emitted [`Self::subject`]
112    /// accessor.
113    pub fn items(&self) -> impl Iterator<Item = MapProjectionItem> + '_ {
114        self.syntax().children().filter_map(MapProjectionItem::cast)
115    }
116}
117
118/// Trait every typed AST wrapper conforms to. Modelled after the
119/// rust-analyzer `AstNode` pattern. The `generated` module emits plain
120/// `cast`/`syntax` methods directly (matching the spec §5.1 shape); this
121/// trait is the portable handle hand-written or macro-based wrappers use.
122pub trait AstNode: Sized {
123    /// `true` iff a node with this `kind` can be cast to `Self`.
124    fn can_cast(kind: SyntaxKind) -> bool;
125    /// Attempt to cast a raw `SyntaxNode` to this typed wrapper.
126    /// Returns `None` when the node's `SyntaxKind` doesn't match.
127    fn cast(syntax: SyntaxNode) -> Option<Self>;
128    /// Return the underlying `SyntaxNode`.  Zero-cost — typed
129    /// wrappers are `repr(transparent)`-style views.
130    fn syntax(&self) -> &SyntaxNode;
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use cyrs_syntax::parse;
137
138    #[test]
139    fn cast_source_file_from_generated() {
140        let parse = parse("");
141        // `SourceFile` comes from `generated::*`.
142        let src = SourceFile::cast(parse.syntax()).expect("SOURCE_FILE root");
143        // Trivial sanity — confirm the syntax handle round-trips.
144        assert_eq!(src.syntax().kind(), SyntaxKind::SOURCE_FILE);
145    }
146}