Skip to main content

arity_parser/dcf/
syntax.rs

1//! The DCF grammar's node kinds and its rowan language definition.
2//!
3//! This is a **second** [`rowan::Language`] alongside the R grammar's
4//! [`RLanguage`](crate::syntax::RLanguage). The two `SyntaxKind` enums are
5//! distinct types that happen to share a name: the module path is the
6//! disambiguator, so never glob-import both.
7
8use rowan::Language;
9
10/// A DCF node or token kind.
11///
12/// Nodes come first, tokens second, and the discriminants are load-bearing —
13/// [`DcfLanguage::kind_from_raw`] maps them back by number, and
14/// [`SyntaxKind::ALL`] plus `syntax_kind_round_trips` guard that table.
15#[allow(non_camel_case_types)]
16#[derive(
17    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
18)]
19#[repr(u16)]
20pub enum SyntaxKind {
21    // --- nodes ---
22    /// The whole document.
23    ROOT,
24    /// One blank-line-delimited record. A `DESCRIPTION` has exactly one.
25    RECORD,
26    /// `Name: value` plus every continuation line that follows it.
27    FIELD,
28    /// One physical line of a field's value. The field's own line (everything
29    /// after the colon) is a `VALUE_LINE` too, which is what makes folding
30    /// uniform.
31    VALUE_LINE,
32    /// A `#`-at-column-zero line. Legal inside a [`FIELD`](Self::FIELD): R's
33    /// `read.dcf` skips such a line and *resumes* the field's continuation.
34    COMMENT_LINE,
35    /// An empty or whitespace-only line — a record separator.
36    BLANK_LINE,
37    /// A line that is neither a field, a continuation, a comment, nor blank.
38    /// `read.dcf` errors here; arity keeps the bytes and reports a diagnostic.
39    MALFORMED_LINE,
40
41    // --- tokens ---
42    /// A field's name, excluding any whitespace before the colon.
43    FIELD_NAME,
44    /// The `:` separating a field's name from its value.
45    COLON,
46    /// A value line's content run, from its first to its last non-space byte.
47    VALUE_TEXT,
48    /// A comment line's `#...`, up to but excluding the newline.
49    COMMENT,
50    /// A malformed line's raw content.
51    TEXT,
52    /// Indentation, the space(s) after a colon, and trailing spaces.
53    WHITESPACE,
54    /// `\n` or `\r\n` — always one token, never split.
55    NEWLINE,
56    /// The `_` arm of [`DcfLanguage::kind_from_raw`]. Never emitted by the
57    /// parser.
58    ERROR,
59}
60
61impl SyntaxKind {
62    /// Every kind, in discriminant order.
63    pub const ALL: [SyntaxKind; Self::COUNT] = [
64        SyntaxKind::ROOT,
65        SyntaxKind::RECORD,
66        SyntaxKind::FIELD,
67        SyntaxKind::VALUE_LINE,
68        SyntaxKind::COMMENT_LINE,
69        SyntaxKind::BLANK_LINE,
70        SyntaxKind::MALFORMED_LINE,
71        SyntaxKind::FIELD_NAME,
72        SyntaxKind::COLON,
73        SyntaxKind::VALUE_TEXT,
74        SyntaxKind::COMMENT,
75        SyntaxKind::TEXT,
76        SyntaxKind::WHITESPACE,
77        SyntaxKind::NEWLINE,
78        SyntaxKind::ERROR,
79    ];
80
81    /// How many kinds there are.
82    pub const COUNT: usize = 15;
83
84    /// Whether this kind is a node (rather than a token).
85    pub fn is_node(self) -> bool {
86        matches!(
87            self,
88            SyntaxKind::ROOT
89                | SyntaxKind::RECORD
90                | SyntaxKind::FIELD
91                | SyntaxKind::VALUE_LINE
92                | SyntaxKind::COMMENT_LINE
93                | SyntaxKind::BLANK_LINE
94                | SyntaxKind::MALFORMED_LINE
95        )
96    }
97
98    /// Whether this kind is one of the four line nodes. Every physical line of
99    /// the source belongs to exactly one of these, or is a
100    /// [`FIELD`](Self::FIELD)'s own first line.
101    pub fn is_line(self) -> bool {
102        matches!(
103            self,
104            SyntaxKind::VALUE_LINE
105                | SyntaxKind::COMMENT_LINE
106                | SyntaxKind::BLANK_LINE
107                | SyntaxKind::MALFORMED_LINE
108        )
109    }
110}
111
112impl From<SyntaxKind> for rowan::SyntaxKind {
113    fn from(kind: SyntaxKind) -> Self {
114        Self(kind as u16)
115    }
116}
117
118/// The DCF language, for rowan's generic tree types.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
120pub enum DcfLanguage {}
121
122impl Language for DcfLanguage {
123    type Kind = SyntaxKind;
124
125    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
126        match raw.0 {
127            0 => SyntaxKind::ROOT,
128            1 => SyntaxKind::RECORD,
129            2 => SyntaxKind::FIELD,
130            3 => SyntaxKind::VALUE_LINE,
131            4 => SyntaxKind::COMMENT_LINE,
132            5 => SyntaxKind::BLANK_LINE,
133            6 => SyntaxKind::MALFORMED_LINE,
134            7 => SyntaxKind::FIELD_NAME,
135            8 => SyntaxKind::COLON,
136            9 => SyntaxKind::VALUE_TEXT,
137            10 => SyntaxKind::COMMENT,
138            11 => SyntaxKind::TEXT,
139            12 => SyntaxKind::WHITESPACE,
140            13 => SyntaxKind::NEWLINE,
141            _ => SyntaxKind::ERROR,
142        }
143    }
144
145    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
146        kind.into()
147    }
148}
149
150pub type SyntaxNode = rowan::SyntaxNode<DcfLanguage>;
151pub type SyntaxToken = rowan::SyntaxToken<DcfLanguage>;
152pub type SyntaxElement = rowan::SyntaxElement<DcfLanguage>;
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    /// The numeric `kind_from_raw` table is hand-written, so it is exactly the
159    /// thing that rots silently when a kind is inserted mid-enum.
160    #[test]
161    fn syntax_kind_round_trips() {
162        for kind in SyntaxKind::ALL {
163            assert_eq!(
164                DcfLanguage::kind_from_raw(kind.into()),
165                kind,
166                "{kind:?} does not round-trip through kind_from_raw"
167            );
168        }
169    }
170
171    #[test]
172    fn all_is_in_discriminant_order() {
173        for (i, kind) in SyntaxKind::ALL.iter().enumerate() {
174            assert_eq!(rowan::SyntaxKind::from(*kind).0 as usize, i, "{kind:?}");
175        }
176    }
177}