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}