1use rowan::{TextRange, TextSize};
4
5use crate::SyntaxKind;
6use crate::diagnostic::Diagnostic;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Token {
11 pub kind: SyntaxKind,
12 pub range: TextRange,
13}
14
15pub fn tokenize(source: &str) -> (Vec<Token>, Vec<Diagnostic>) {
19 let mut lexer = Lexer {
20 source,
21 pos: 0,
22 tokens: Vec::new(),
23 diagnostics: Vec::new(),
24 };
25 while lexer.pos < source.len() {
26 lexer.next_token();
27 }
28 (lexer.tokens, lexer.diagnostics)
29}
30
31struct Lexer<'a> {
32 source: &'a str,
33 pos: usize,
34 tokens: Vec<Token>,
35 diagnostics: Vec<Diagnostic>,
36}
37
38impl Lexer<'_> {
39 fn rest(&self) -> &str {
40 &self.source[self.pos..]
41 }
42
43 fn peek(&self) -> Option<char> {
44 self.rest().chars().next()
45 }
46
47 fn peek_nth(&self, n: usize) -> Option<char> {
48 self.rest().chars().nth(n)
49 }
50
51 fn next_token(&mut self) {
52 let start = self.pos;
53 let c = self.peek().expect("lexer called at end of input");
54 let kind = match c {
55 c if is_whitespace(c) => {
56 self.eat_while(is_whitespace);
57 SyntaxKind::Whitespace
58 }
59 '/' => self.slash(),
60 '\'' => self.quoted('\'', SyntaxKind::SingleQuotedString, "string"),
61 '`' => self.quoted('`', SyntaxKind::DelimitedIdent, "delimited identifier"),
62 '"' if self.rest().starts_with("\"\"\"") => self.triple_quoted(),
63 '"' => self.quoted('"', SyntaxKind::DoubleQuotedString, "string"),
64 '0'..='9' => self.number(),
65 '@' => self.date_time(),
66 '$' if self.peek_nth(1).is_some_and(is_ident_start) => {
67 self.bump();
68 self.eat_while(is_ident_continue);
69 SyntaxKind::DollarIdent
70 }
71 c if is_ident_start(c) => {
72 self.eat_while(is_ident_continue);
73 SyntaxKind::Ident
74 }
75 _ => self.punctuation(c),
76 };
77 self.push(kind, start);
78 }
79
80 fn push(&mut self, kind: SyntaxKind, start: usize) {
81 self.tokens.push(Token {
82 kind,
83 range: range(start, self.pos),
84 });
85 }
86
87 fn error(&mut self, start: usize, message: impl Into<String>) {
88 self.diagnostics
89 .push(Diagnostic::error(range(start, self.pos), message));
90 }
91
92 fn bump(&mut self) {
93 if let Some(c) = self.peek() {
94 self.pos += c.len_utf8();
95 }
96 }
97
98 fn bump_n(&mut self, n: usize) {
99 for _ in 0..n {
100 self.bump();
101 }
102 }
103
104 fn eat_while(&mut self, predicate: impl Fn(char) -> bool) {
105 while self.peek().is_some_and(&predicate) {
106 self.bump();
107 }
108 }
109
110 fn eat_digits(&mut self) -> usize {
111 let start = self.pos;
112 self.eat_while(|c| c.is_ascii_digit());
113 self.pos - start
114 }
115
116 fn slash(&mut self) -> SyntaxKind {
117 let rest = self.rest();
118 if rest.starts_with("///") && !rest.starts_with("////") && is_metadata_line(&rest[3..]) {
119 self.bump_n(3);
120 SyntaxKind::MetadataPrefix
121 } else if rest.starts_with("//") {
122 self.eat_while(|c| c != '\n' && c != '\r');
123 SyntaxKind::LineComment
124 } else if let Some(body) = rest.strip_prefix("/*") {
125 let start = self.pos;
126 match body.find("*/") {
127 Some(end) => {
128 self.pos += 2 + end + 2;
129 SyntaxKind::BlockComment
130 }
131 None => {
132 self.pos = self.source.len();
133 self.error(start, "unterminated block comment");
134 SyntaxKind::BlockComment
135 }
136 }
137 } else {
138 self.bump();
139 SyntaxKind::Slash
140 }
141 }
142
143 fn quoted(&mut self, quote: char, kind: SyntaxKind, what: &str) -> SyntaxKind {
144 let start = self.pos;
145 self.bump();
146 loop {
147 match self.peek() {
148 None => {
149 self.error(start, format!("unterminated {what}"));
150 return SyntaxKind::Unknown;
151 }
152 Some('\\') => {
153 self.bump();
154 self.bump();
155 }
156 Some(c) if c == quote => {
157 self.bump();
158 return kind;
159 }
160 Some(_) => self.bump(),
161 }
162 }
163 }
164
165 fn triple_quoted(&mut self) -> SyntaxKind {
166 let start = self.pos;
167 match self.rest()[3..].find("\"\"\"") {
168 Some(end) => {
169 self.pos += 3 + end + 3;
170 SyntaxKind::TripleQuotedString
171 }
172 None => {
173 self.pos = self.source.len();
174 self.error(start, "unterminated triple-quoted string");
175 SyntaxKind::Unknown
176 }
177 }
178 }
179
180 fn number(&mut self) -> SyntaxKind {
181 self.eat_digits();
182 if self.peek() == Some('.') && self.peek_nth(1).is_some_and(|c| c.is_ascii_digit()) {
183 self.bump();
184 self.eat_digits();
185 return SyntaxKind::Decimal;
186 }
187 if self.peek() == Some('L') && !self.peek_nth(1).is_some_and(is_ident_continue) {
188 self.bump();
189 return SyntaxKind::LongNumber;
190 }
191 SyntaxKind::Integer
192 }
193
194 fn date_time(&mut self) -> SyntaxKind {
196 let start = self.pos;
197 self.bump();
198 if self.peek() == Some('T') {
199 self.bump();
200 self.time();
201 return SyntaxKind::Time;
202 }
203 if self.eat_digits() != 4 {
204 self.error(start, "malformed date literal");
205 return SyntaxKind::Unknown;
206 }
207 for _ in 0..2 {
208 if self.peek() == Some('-') && self.peek_nth(1).is_some_and(|c| c.is_ascii_digit()) {
209 self.bump();
210 self.eat_digits();
211 }
212 }
213 if self.peek() != Some('T') {
214 return SyntaxKind::Date;
215 }
216 self.bump();
217 if self.peek().is_some_and(|c| c.is_ascii_digit()) {
218 self.time();
219 match self.peek() {
220 Some('Z') => self.bump(),
221 Some('+' | '-') if self.peek_nth(1).is_some_and(|c| c.is_ascii_digit()) => {
222 self.bump();
223 self.eat_digits();
224 if self.peek() == Some(':') {
225 self.bump();
226 self.eat_digits();
227 }
228 }
229 _ => {}
230 }
231 }
232 SyntaxKind::DateTime
233 }
234
235 fn time(&mut self) {
236 self.eat_digits();
237 for _ in 0..2 {
238 if self.peek() == Some(':') && self.peek_nth(1).is_some_and(|c| c.is_ascii_digit()) {
239 self.bump();
240 self.eat_digits();
241 }
242 }
243 if self.peek() == Some('.') && self.peek_nth(1).is_some_and(|c| c.is_ascii_digit()) {
244 self.bump();
245 self.eat_digits();
246 }
247 }
248
249 fn punctuation(&mut self, c: char) -> SyntaxKind {
250 let next = self.peek_nth(1);
251 let (kind, len) = match (c, next) {
252 ('-', Some('>')) => (SyntaxKind::Arrow, 2),
253 ('.', Some('.')) => (SyntaxKind::DotDot, 2),
254 ('=', Some('=')) => (SyntaxKind::EqEq, 2),
255 ('!', Some('=')) => (SyntaxKind::Neq, 2),
256 ('!', Some('~')) => (SyntaxKind::NotTilde, 2),
257 ('<', Some('<')) => (SyntaxKind::Shl, 2),
258 ('<', Some('=')) => (SyntaxKind::Le, 2),
259 ('>', Some('>')) => (SyntaxKind::Shr, 2),
260 ('>', Some('=')) => (SyntaxKind::Ge, 2),
261 ('(', _) => (SyntaxKind::LParen, 1),
262 (')', _) => (SyntaxKind::RParen, 1),
263 ('{', _) => (SyntaxKind::LBrace, 1),
264 ('}', _) => (SyntaxKind::RBrace, 1),
265 ('[', _) => (SyntaxKind::LBracket, 1),
266 (']', _) => (SyntaxKind::RBracket, 1),
267 (',', _) => (SyntaxKind::Comma, 1),
268 (';', _) => (SyntaxKind::Semicolon, 1),
269 (':', _) => (SyntaxKind::Colon, 1),
270 ('.', _) => (SyntaxKind::Dot, 1),
271 ('=', _) => (SyntaxKind::Eq, 1),
272 ('~', _) => (SyntaxKind::Tilde, 1),
273 ('<', _) => (SyntaxKind::Lt, 1),
274 ('>', _) => (SyntaxKind::Gt, 1),
275 ('+', _) => (SyntaxKind::Plus, 1),
276 ('-', _) => (SyntaxKind::Minus, 1),
277 ('*', _) => (SyntaxKind::Star, 1),
278 ('&', _) => (SyntaxKind::Amp, 1),
279 ('|', _) => (SyntaxKind::Pipe, 1),
280 ('%', _) => (SyntaxKind::Percent, 1),
281 _ => (SyntaxKind::Unknown, 1),
282 };
283 let start = self.pos;
284 self.bump_n(len);
285 if kind == SyntaxKind::Unknown {
286 self.error(start, format!("unexpected character {c:?}"));
287 }
288 kind
289 }
290}
291
292fn is_metadata_line(after: &str) -> bool {
295 let line = after.split(['\n', '\r']).next().unwrap_or("");
296 let line = line.trim_start_matches([' ', '\t']);
297 let key_len = line
298 .find(|c: char| !(is_ident_continue(c) || c == '.'))
299 .unwrap_or(line.len());
300 key_len > 0
301 && line.starts_with(is_ident_start)
302 && line[key_len..]
303 .trim_start_matches([' ', '\t'])
304 .starts_with('=')
305}
306
307fn is_whitespace(c: char) -> bool {
308 c.is_whitespace() || c == '\u{feff}'
309}
310
311fn is_ident_start(c: char) -> bool {
312 c.is_alphabetic() || c == '_'
313}
314
315fn is_ident_continue(c: char) -> bool {
316 c.is_alphanumeric() || c == '_'
317}
318
319fn range(start: usize, end: usize) -> TextRange {
320 TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32))
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 fn kinds(source: &str) -> Vec<(SyntaxKind, &str)> {
328 let (tokens, diagnostics) = tokenize(source);
329 assert!(diagnostics.is_empty(), "{diagnostics:?}");
330 tokens
331 .into_iter()
332 .filter(|t| t.kind != SyntaxKind::Whitespace)
333 .map(|t| (t.kind, &source[t.range]))
334 .collect()
335 }
336
337 #[test]
338 fn lossless() {
339 let source =
340 "/// url = 'x'\ngroup G(source s, target t) { s.a as v -> t.b = v \"r\"; } // c\n";
341 let (tokens, _) = tokenize(source);
342 let text: String = tokens.iter().map(|t| &source[t.range]).collect();
343 assert_eq!(text, source);
344 }
345
346 #[test]
347 fn metadata_prefix_needs_key_and_equals() {
348 assert_eq!(
349 kinds("/// url = 'x'")[0],
350 (SyntaxKind::MetadataPrefix, "///")
351 );
352 assert_eq!(
353 kinds("/// just a comment")[0],
354 (SyntaxKind::LineComment, "/// just a comment")
355 );
356 assert_eq!(kinds("//// x = 1")[0].0, SyntaxKind::LineComment);
357 }
358
359 #[test]
360 fn numbers_and_ranges() {
361 assert_eq!(
362 kinds("0..* 1.5 10L"),
363 vec![
364 (SyntaxKind::Integer, "0"),
365 (SyntaxKind::DotDot, ".."),
366 (SyntaxKind::Star, "*"),
367 (SyntaxKind::Decimal, "1.5"),
368 (SyntaxKind::LongNumber, "10L"),
369 ]
370 );
371 }
372
373 #[test]
374 fn dates() {
375 assert_eq!(
376 kinds("@2024-01-02 @2024-01-02T10:11:12.5+02:00 @T10:30"),
377 vec![
378 (SyntaxKind::Date, "@2024-01-02"),
379 (SyntaxKind::DateTime, "@2024-01-02T10:11:12.5+02:00"),
380 (SyntaxKind::Time, "@T10:30"),
381 ]
382 );
383 }
384
385 #[test]
386 fn strings() {
387 assert_eq!(
388 kinds(
389 r#"'a\'b' "c" """
390md
391""" `x y`"#
392 ),
393 vec![
394 (SyntaxKind::SingleQuotedString, r"'a\'b'"),
395 (SyntaxKind::DoubleQuotedString, "\"c\""),
396 (SyntaxKind::TripleQuotedString, "\"\"\"\nmd\n\"\"\""),
397 (SyntaxKind::DelimitedIdent, "`x y`"),
398 ]
399 );
400 }
401
402 #[test]
403 fn operators() {
404 assert_eq!(
405 kinds("-> <<type+>> != <= =="),
406 vec![
407 (SyntaxKind::Arrow, "->"),
408 (SyntaxKind::Shl, "<<"),
409 (SyntaxKind::Ident, "type"),
410 (SyntaxKind::Plus, "+"),
411 (SyntaxKind::Shr, ">>"),
412 (SyntaxKind::Neq, "!="),
413 (SyntaxKind::Le, "<="),
414 (SyntaxKind::EqEq, "=="),
415 ]
416 );
417 }
418
419 #[test]
420 fn unterminated_string_reports() {
421 let (_, diagnostics) = tokenize("'abc");
422 assert_eq!(diagnostics.len(), 1);
423 }
424}