1use beck_diag::{Diagnostic, Diagnostics, FileId, Span};
15use logos::Logos;
16
17#[derive(Clone, Debug, PartialEq, Logos)]
18#[logos(skip r"[ \t]+")]
19#[logos(skip(r"#[^\n]*", allow_greedy = true))]
21pub enum Raw {
22 #[regex(r"\n")]
23 Newline,
24
25 #[regex(r"[A-Za-z_][A-Za-z0-9_]*", |lex| lex.slice().to_string())]
26 Ident(String),
27
28 #[regex(r":[A-Za-z_][A-Za-z0-9_\-]*", |lex| lex.slice()[1..].to_string())]
30 Keyword(String),
31
32 #[regex(r"[0-9][0-9_]*\.[0-9][0-9_]*([eE][+-]?[0-9]+)?", float)]
38 #[regex(r"[0-9][0-9_]*[eE][+-]?[0-9]+", float)]
39 Float(f64),
40
41 #[regex(r"[0-9][0-9_]*", |lex| lex.slice().replace('_', "").parse::<i64>().ok())]
42 Int(i64),
43
44 #[regex(r#""([^"\\\n]|\\.)*""#, |lex| unescape(lex.slice()))]
45 Str(String),
46
47 #[regex(r#"[A-Za-z_][A-Za-z0-9_]*"[^"\n]*""#, sigil)]
52 Sigil(SigilText),
53
54 #[token("(")]
55 LParen,
56 #[token(")")]
57 RParen,
58 #[token("[")]
59 LBracket,
60 #[token("]")]
61 RBracket,
62 #[token("{")]
63 LBrace,
64 #[token("}")]
65 RBrace,
66
67 #[token("->")]
68 Arrow,
69 #[token("==")]
70 EqEq,
71 #[token("!=")]
72 NotEq,
73 #[token("<=")]
74 LtEq,
75 #[token(">=")]
76 GtEq,
77 #[token("$*")]
79 DollarStar,
80 #[token("$")]
81 Dollar,
82 #[token("=")]
83 Eq,
84 #[token("<")]
85 Lt,
86 #[token(">")]
87 Gt,
88 #[token("+")]
89 Plus,
90 #[token("-")]
91 Minus,
92 #[token("*")]
93 Star,
94 #[token("/")]
95 Slash,
96 #[token("%")]
97 Percent,
98 #[token(".")]
99 Dot,
100 #[token(",")]
101 Comma,
102 #[token(":")]
103 Colon,
104 #[token("@")]
105 At,
106 #[token("|")]
107 Pipe,
108 #[token("?")]
109 Question,
110}
111
112#[derive(Clone, Debug, PartialEq)]
122pub struct SigilText {
123 pub name: String,
124 pub raw: String,
125}
126
127fn sigil(lex: &mut logos::Lexer<Raw>) -> SigilText {
128 let s = lex.slice();
129 let q = s
131 .find('"')
132 .expect("a sigil's regex requires an opening quote");
133 SigilText {
134 name: s[..q].to_string(),
135 raw: s[q + 1..s.len() - 1].to_string(),
136 }
137}
138
139fn float(lex: &mut logos::Lexer<Raw>) -> Option<f64> {
140 lex.slice().replace('_', "").parse::<f64>().ok()
141}
142
143fn unescape(raw: &str) -> Option<String> {
144 let inner = &raw[1..raw.len() - 1];
145 let mut out = String::with_capacity(inner.len());
146 let mut chars = inner.chars();
147 while let Some(c) = chars.next() {
148 if c != '\\' {
149 out.push(c);
150 continue;
151 }
152 match chars.next()? {
153 'n' => out.push('\n'),
154 't' => out.push('\t'),
155 'r' => out.push('\r'),
156 '0' => out.push('\0'),
157 '\\' => out.push('\\'),
158 '"' => out.push('"'),
159 'u' => {
163 let mut hex = String::new();
164 if chars.next()? != '{' {
165 return None;
166 }
167 for c in chars.by_ref() {
168 if c == '}' {
169 break;
170 }
171 hex.push(c);
172 }
173 out.push(char::from_u32(u32::from_str_radix(&hex, 16).ok()?)?);
174 }
175 other => {
178 out.push('\\');
179 out.push(other);
180 }
181 }
182 }
183 Some(out)
184}
185
186#[derive(Clone, Debug, PartialEq)]
188pub enum Tok {
189 Raw(Raw),
190 Newline,
191 Indent,
192 Dedent,
193 Eof,
194}
195
196#[derive(Clone, Debug)]
197pub struct Token {
198 pub tok: Tok,
199 pub span: Span,
200}
201
202impl Token {
203 pub fn raw(&self) -> Option<&Raw> {
204 match &self.tok {
205 Tok::Raw(r) => Some(r),
206 _ => None,
207 }
208 }
209
210 pub fn is_ident(&self, name: &str) -> bool {
211 matches!(self.raw(), Some(Raw::Ident(s)) if s == name)
212 }
213
214 pub fn describe(&self) -> String {
215 match &self.tok {
216 Tok::Newline => "end of line".into(),
217 Tok::Indent => "an indented block".into(),
218 Tok::Dedent => "the end of a block".into(),
219 Tok::Eof => "end of file".into(),
220 Tok::Raw(r) => match r {
221 Raw::Ident(s) => format!("`{s}`"),
222 Raw::Keyword(s) => format!("`:{s}`"),
223 Raw::Int(n) => format!("`{n}`"),
224 Raw::Float(n) => format!("`{n}`"),
225 Raw::Str(_) => "a string".into(),
226 Raw::Sigil(t) => format!("`{}\"…\"`", t.name),
227 Raw::Newline => "end of line".into(),
228 other => format!("`{}`", punct(other)),
229 },
230 }
231 }
232}
233
234fn punct(r: &Raw) -> &'static str {
235 match r {
236 Raw::LParen => "(",
237 Raw::RParen => ")",
238 Raw::LBracket => "[",
239 Raw::RBracket => "]",
240 Raw::LBrace => "{",
241 Raw::RBrace => "}",
242 Raw::Arrow => "->",
243 Raw::EqEq => "==",
244 Raw::NotEq => "!=",
245 Raw::LtEq => "<=",
246 Raw::GtEq => ">=",
247 Raw::DollarStar => "$*",
248 Raw::Dollar => "$",
249 Raw::Eq => "=",
250 Raw::Lt => "<",
251 Raw::Gt => ">",
252 Raw::Plus => "+",
253 Raw::Minus => "-",
254 Raw::Star => "*",
255 Raw::Slash => "/",
256 Raw::Percent => "%",
257 Raw::Dot => ".",
258 Raw::Comma => ",",
259 Raw::Colon => ":",
260 Raw::At => "@",
261 Raw::Pipe => "|",
262 Raw::Question => "?",
263 _ => "?",
264 }
265}
266
267pub const KEYWORDS: &[&str] = &[
282 "and",
283 "by",
284 "case",
285 "contains",
286 "def",
287 "elif",
288 "else",
289 "expect",
290 "flow",
291 "for",
292 "given",
293 "identity",
294 "if",
295 "impl",
296 "import",
297 "in",
298 "lambda",
299 "macro",
300 "match",
301 "matches",
302 "model",
303 "newtype",
304 "no",
305 "not",
306 "nothing",
307 "on",
308 "once",
309 "or",
310 "page",
311 "parallel",
312 "place",
313 "property",
314 "quote",
315 "raise",
316 "reaches",
317 "return",
318 "row",
319 "sends",
320 "session",
321 "snapshot",
322 "state",
323 "stub",
324 "test",
325 "times",
326 "trait",
327 "try",
328 "type",
329 "typed",
330 "union",
331 "uses",
332 "var",
333 "when",
334 "while",
335 "wire_compatible_with",
336 "with",
337];
338
339pub fn lex(file: FileId, src: &str, diags: &mut Diagnostics) -> Vec<Token> {
344 let mut lexed: Vec<Token> = Vec::new();
345 let mut lx = Raw::lexer(src);
346 while let Some(res) = lx.next() {
347 let span = Span::new(file, lx.span());
348 match res {
349 Ok(r) => lexed.push(Token {
350 tok: Tok::Raw(r),
351 span,
352 }),
353 Err(())
356 if src[span.start as usize..span.end as usize]
357 .chars()
358 .any(crate::security::is_non_ascii_letter) =>
359 {
360 diags.push(
361 Diagnostic::error("B0103", "an identifier outside the ASCII profile", span)
362 .with_primary_label("not an identifier character in Beck")
363 .with_note(format!(
364 "Beck's identifiers are `[A-Za-z_][A-Za-z0-9_]*` — UTS #39's \
365 ASCII-Only restriction level, against Unicode {}. Two identifiers \
366 that look alike cannot then be different names",
367 crate::security::UNICODE
368 )),
369 )
370 }
371 Err(()) => diags.push(
372 Diagnostic::error("B0100", "unrecognised character", span)
373 .with_primary_label("not a Beck token"),
374 ),
375 }
376 }
377 layout(file, src, lexed, diags)
378}
379
380fn layout(file: FileId, src: &str, lexed: Vec<Token>, diags: &mut Diagnostics) -> Vec<Token> {
382 let mut out: Vec<Token> = Vec::new();
383 let mut stack: Vec<usize> = vec![0];
384 let mut depth: i32 = 0;
385 let mut at_line_start = true;
386 let mut line_has_content = false;
387
388 let mut i = 0;
389 while i < lexed.len() {
390 let t = &lexed[i];
391
392 if matches!(t.tok, Tok::Raw(Raw::Newline)) {
393 if depth == 0 && line_has_content {
394 out.push(Token {
395 tok: Tok::Newline,
396 span: t.span,
397 });
398 line_has_content = false;
399 at_line_start = true;
400 }
401 i += 1;
402 continue;
403 }
404
405 if at_line_start && depth == 0 {
406 let col = indent_width(src, t.span.start as usize);
407 let top = *stack.last().expect("indent stack is never empty");
408 if col > top {
409 stack.push(col);
410 out.push(Token {
411 tok: Tok::Indent,
412 span: t.span,
413 });
414 } else if col < top {
415 while *stack.last().expect("indent stack is never empty") > col {
416 stack.pop();
417 out.push(Token {
418 tok: Tok::Dedent,
419 span: t.span,
420 });
421 }
422 if *stack.last().expect("indent stack is never empty") != col {
423 diags.push(
424 Diagnostic::error("B0101", "inconsistent indentation", t.span)
425 .with_primary_label("this line does not match any enclosing block")
426 .with_note("indentation is significant: spaces only, four per level"),
427 );
428 stack.push(col);
429 }
430 }
431 at_line_start = false;
432 }
433
434 match &t.tok {
435 Tok::Raw(Raw::LParen | Raw::LBracket | Raw::LBrace) => depth += 1,
436 Tok::Raw(Raw::RParen | Raw::RBracket | Raw::RBrace) => depth -= 1,
437 _ => {}
438 }
439 line_has_content = true;
440 out.push(t.clone());
441 i += 1;
442 }
443
444 let end = Span::new(file, src.len()..src.len());
445 if line_has_content {
446 out.push(Token {
447 tok: Tok::Newline,
448 span: end,
449 });
450 }
451 while stack.len() > 1 {
452 stack.pop();
453 out.push(Token {
454 tok: Tok::Dedent,
455 span: end,
456 });
457 }
458 out.push(Token {
459 tok: Tok::Eof,
460 span: end,
461 });
462 out
463}
464
465fn indent_width(src: &str, offset: usize) -> usize {
470 let line_start = src[..offset].rfind('\n').map(|i| i + 1).unwrap_or(0);
471 src[line_start..offset].chars().count()
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 fn toks(src: &str) -> Vec<Tok> {
479 let mut map = beck_diag::SourceMap::new();
480 let f = map.add("t.beck", src);
481 let mut d = Diagnostics::new();
482 let out = lex(f, src, &mut d).into_iter().map(|t| t.tok).collect();
483 assert!(!d.has_errors(), "{}", d.render(&map));
484 out
485 }
486
487 #[test]
493 fn the_keyword_table_is_the_one_the_parser_matches() {
494 let parser = include_str!("parser.rs");
495 let mut matched: Vec<&str> = Vec::new();
496 for (at, _) in parser.match_indices("_kw(\"") {
497 let rest = &parser[at + "_kw(\"".len()..];
498 let Some(end) = rest.find('"') else { continue };
499 matched.push(&rest[..end]);
500 }
501 matched.sort_unstable();
502 matched.dedup();
503 assert!(
504 !matched.is_empty(),
505 "no `at_kw(\"…\")` calls found — this test has stopped reading the parser"
506 );
507 let listed: Vec<&str> = KEYWORDS.to_vec();
508 assert_eq!(
509 listed, matched,
510 "`KEYWORDS` and the words the parser matches have diverged"
511 );
512 }
513
514 #[test]
515 fn indentation_becomes_indent_and_dedent() {
516 let t = toks("def f():\n return 1\n");
517 let shape: Vec<&str> = t
518 .iter()
519 .map(|t| match t {
520 Tok::Indent => "INDENT",
521 Tok::Dedent => "DEDENT",
522 Tok::Newline => "NL",
523 Tok::Eof => "EOF",
524 Tok::Raw(_) => "tok",
525 })
526 .collect();
527 assert_eq!(
528 shape,
529 [
530 "tok", "tok", "tok", "tok", "tok", "NL", "INDENT", "tok", "tok", "NL", "DEDENT",
531 "EOF"
532 ]
533 );
534 }
535
536 #[test]
537 fn brackets_suppress_layout_so_calls_may_span_lines() {
538 let t = toks("f(\n 1,\n 2,\n)\n");
539 assert_eq!(t.iter().filter(|t| matches!(t, Tok::Indent)).count(), 0);
540 assert_eq!(t.iter().filter(|t| matches!(t, Tok::Newline)).count(), 1);
541 }
542
543 #[test]
544 fn blank_and_comment_lines_do_not_close_a_block() {
545 let t = toks("def f():\n a\n\n# a comment at column zero\n b\n");
546 assert_eq!(t.iter().filter(|t| matches!(t, Tok::Dedent)).count(), 1);
547 assert_eq!(t.iter().filter(|t| matches!(t, Tok::Indent)).count(), 1);
548 }
549
550 #[test]
551 fn keywords_beat_the_colon_operator_and_dollar_star_beats_dollar() {
552 assert!(matches!(
553 &toks(":id x")[0],
554 Tok::Raw(Raw::Keyword(k)) if k == "id"
555 ));
556 assert!(matches!(&toks("$*xs")[0], Tok::Raw(Raw::DollarStar)));
557 assert!(matches!(&toks("$x")[0], Tok::Raw(Raw::Dollar)));
558 }
559
560 #[test]
561 fn strings_unescape() {
562 assert!(matches!(
563 &toks(r#""a\nb\"c""#)[0],
564 Tok::Raw(Raw::Str(s)) if s == "a\nb\"c"
565 ));
566 }
567
568 #[test]
569 fn nested_dedents_all_close_at_once() {
570 let t = toks("if a:\n if b:\n c\nd\n");
571 let dedents = t.iter().filter(|t| matches!(t, Tok::Dedent)).count();
572 assert_eq!(dedents, 2);
573 }
574}