Skip to main content

arity_parser/ast/
tokens.rs

1//! Typed AST wrappers over CST *tokens* (leaves), mirroring rowan's `AstNode`
2//! for nodes. rowan 0.16 ships only `AstNode`; arity defines its own
3//! [`AstToken`] (as rust-analyzer does).
4//!
5//! R's atomic operands are **bare tokens**, not `LITERAL` nodes: `1 + 2` is
6//! `BINARY_EXPR { INT, PLUS, INT }`, and every special constant (`TRUE`, `NA`,
7//! `NULL`, …) is an `IDENT` classified by text. These wrappers type those
8//! leaves so consumers can navigate them without re-inspecting [`SyntaxKind`].
9
10use rowan::TextRange;
11use smol_str::SmolStr;
12
13use crate::syntax::{SyntaxKind, SyntaxToken};
14
15/// A typed, zero-cost wrapper over a single CST token of a fixed
16/// [`SyntaxKind`]. The token analogue of rowan's `AstNode`.
17pub trait AstToken {
18    fn can_cast(kind: SyntaxKind) -> bool
19    where
20        Self: Sized;
21
22    fn cast(syntax: SyntaxToken) -> Option<Self>
23    where
24        Self: Sized;
25
26    fn syntax(&self) -> &SyntaxToken;
27
28    fn text(&self) -> &str {
29        self.syntax().text()
30    }
31
32    fn text_range(&self) -> TextRange {
33        self.syntax().text_range()
34    }
35}
36
37macro_rules! ast_token {
38    ($name:ident, $kind:expr) => {
39        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
40        pub struct $name(SyntaxToken);
41
42        impl AstToken for $name {
43            fn can_cast(kind: SyntaxKind) -> bool {
44                kind == $kind
45            }
46
47            fn cast(syntax: SyntaxToken) -> Option<Self> {
48                Self::can_cast(syntax.kind()).then(|| Self(syntax))
49            }
50
51            fn syntax(&self) -> &SyntaxToken {
52                &self.0
53            }
54        }
55    };
56}
57
58ast_token!(Ident, SyntaxKind::IDENT);
59ast_token!(IntLit, SyntaxKind::INT);
60ast_token!(FloatLit, SyntaxKind::FLOAT);
61ast_token!(ComplexLit, SyntaxKind::COMPLEX);
62ast_token!(StringLit, SyntaxKind::STRING);
63ast_token!(Comment, SyntaxKind::COMMENT);
64
65/// One of R's special constant symbols. All are lexed as `IDENT` and
66/// distinguished by text.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub enum RConstant {
69    True,
70    False,
71    Na,
72    NaInteger,
73    NaReal,
74    NaComplex,
75    NaCharacter,
76    Null,
77    NaN,
78    Inf,
79    /// The rebindable boolean symbol `T`.
80    TSymbol,
81    /// The rebindable boolean symbol `F`.
82    FSymbol,
83}
84
85impl Ident {
86    /// The identifier text.
87    pub fn name(&self) -> &str {
88        self.text()
89    }
90
91    /// Classify the identifier as one of R's special constant symbols, if it is
92    /// one. `T`/`F` classify as [`RConstant::TSymbol`]/[`RConstant::FSymbol`]
93    /// even though they are rebindable base bindings — callers that must exclude
94    /// them use [`Ident::is_reserved_constant`].
95    pub fn constant(&self) -> Option<RConstant> {
96        Some(match self.text() {
97            "TRUE" => RConstant::True,
98            "FALSE" => RConstant::False,
99            "NA" => RConstant::Na,
100            "NA_integer_" => RConstant::NaInteger,
101            "NA_real_" => RConstant::NaReal,
102            "NA_complex_" => RConstant::NaComplex,
103            "NA_character_" => RConstant::NaCharacter,
104            "NULL" => RConstant::Null,
105            "NaN" => RConstant::NaN,
106            "Inf" => RConstant::Inf,
107            "T" => RConstant::TSymbol,
108            "F" => RConstant::FSymbol,
109            _ => return None,
110        })
111    }
112
113    /// `TRUE`.
114    pub fn is_true(&self) -> bool {
115        self.text() == "TRUE"
116    }
117
118    /// `FALSE`.
119    pub fn is_false(&self) -> bool {
120        self.text() == "FALSE"
121    }
122
123    /// `NA` or one of its typed variants (`NA_integer_`, …).
124    pub fn is_na(&self) -> bool {
125        matches!(
126            self.text(),
127            "NA" | "NA_integer_" | "NA_real_" | "NA_complex_" | "NA_character_"
128        )
129    }
130
131    /// `NULL`.
132    pub fn is_null(&self) -> bool {
133        self.text() == "NULL"
134    }
135
136    /// `NaN`.
137    pub fn is_nan(&self) -> bool {
138        self.text() == "NaN"
139    }
140
141    /// The rebindable boolean symbols `T` / `F`.
142    pub fn is_bool_symbol(&self) -> bool {
143        matches!(self.text(), "T" | "F")
144    }
145
146    /// An implicit method-dispatch variable (`.Generic`, `.Method`, `.Class`),
147    /// bound automatically by R inside an S3/S4 (group-)generic method body. It
148    /// is always defined there, so the semantic model treats it as never a
149    /// resolvable read rather than an undefined symbol.
150    pub fn is_implicit_method_var(&self) -> bool {
151        matches!(self.text(), ".Generic" | ".Method" | ".Class")
152    }
153
154    /// A dot-dot placeholder (`...`, `..1`), lexed as `IDENT` but not
155    /// scope-resolvable.
156    pub fn is_dots(&self) -> bool {
157        let name = self.text();
158        name.starts_with('.') && name.chars().all(|c| c == '.' || c.is_ascii_digit())
159    }
160
161    /// A reserved constant that is a *value*, not a symbol reference. Mirrors
162    /// [`crate::parser::expr::ident_is_special_constant`] (`NA*`, `NULL`,
163    /// `TRUE`, `FALSE`, `Inf`, `NaN`) and excludes the rebindable `T`/`F`; this
164    /// is the set the semantic model skips when recording identifier reads.
165    pub fn is_reserved_constant(&self) -> bool {
166        matches!(
167            self.constant(),
168            Some(
169                RConstant::Na
170                    | RConstant::NaInteger
171                    | RConstant::NaReal
172                    | RConstant::NaComplex
173                    | RConstant::NaCharacter
174                    | RConstant::Null
175                    | RConstant::True
176                    | RConstant::False
177                    | RConstant::Inf
178                    | RConstant::NaN
179            )
180        )
181    }
182}
183
184impl StringLit {
185    /// The opening quote character if this is a `"`- or `'`-quoted string
186    /// literal (not a backtick-quoted name). `None` otherwise.
187    pub fn quote(&self) -> Option<char> {
188        let bytes = self.text().as_bytes();
189        if bytes.len() < 2 {
190            return None;
191        }
192        let q = bytes[0];
193        (matches!(q, b'"' | b'\'') && bytes[bytes.len() - 1] == q).then_some(q as char)
194    }
195
196    /// Raw text between the quotes (escapes intact) for a `"`/`'`-quoted string;
197    /// `None` for a backtick-quoted name. Subsumes the inner half of the old
198    /// `matchers::string_literal`.
199    pub fn inner(&self) -> Option<&str> {
200        self.quote()?;
201        let text = self.text();
202        Some(&text[1..text.len() - 1])
203    }
204
205    /// Whether this is a backtick-quoted name (`` `x` ``).
206    pub fn is_backtick(&self) -> bool {
207        let bytes = self.text().as_bytes();
208        bytes.len() >= 2 && bytes[0] == b'`' && bytes[bytes.len() - 1] == b'`'
209    }
210
211    /// The unquoted contents for any of the `"` `'` `` ` `` delimiters — the
212    /// name a string denotes. `None` if the token is not delimiter-wrapped.
213    pub fn unquote(&self) -> Option<&str> {
214        let text = self.text();
215        let bytes = text.as_bytes();
216        if bytes.len() >= 2 {
217            let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
218            if matches!(first, b'"' | b'\'' | b'`') && first == last {
219                return Some(&text[1..text.len() - 1]);
220            }
221        }
222        None
223    }
224}
225
226/// The textual name a token denotes: the raw text for an `IDENT` (or any other
227/// token), or the unquoted contents for a backtick/quoted `STRING`.
228pub fn token_name(token: &SyntaxToken) -> SmolStr {
229    if token.kind() == SyntaxKind::STRING {
230        let text = token.text();
231        let bytes = text.as_bytes();
232        if bytes.len() >= 2 {
233            let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
234            if matches!(first, b'"' | b'\'' | b'`') && first == last {
235                return SmolStr::new(&text[1..text.len() - 1]);
236            }
237        }
238    }
239    SmolStr::new(token.text())
240}