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}