use super::*;
use crate::SyntaxKind::*;
fn assert_lossless(source: &str) -> Vec<(SyntaxKind, &str)> {
let tokens = lex(source);
let rebuilt: String = tokens.iter().map(|(_, text)| *text).collect();
assert_eq!(rebuilt, source, "lexer is not lossless for {source:?}");
tokens
}
#[test]
fn empty_source_is_empty() {
assert_eq!(lex(""), vec![]);
}
#[test]
fn keywords_classify() {
let toks = assert_lossless(
"flow fn var const flags struct extern import use module return ref if match else while for in until as or true false END DONE",
);
let kinds: Vec<_> = toks
.iter()
.map(|(k, _)| *k)
.filter(|k| !k.is_trivia())
.collect();
assert_eq!(
kinds,
vec![
KW_FLOW, KW_FN, KW_VAR, KW_CONST, KW_FLAGS, KW_STRUCT, KW_EXTERN, KW_IMPORT, KW_USE,
KW_MODULE, KW_RETURN, KW_REF, KW_IF, KW_MATCH, KW_ELSE, KW_WHILE, KW_FOR, KW_IN,
KW_UNTIL, KW_AS, KW_OR, KW_TRUE, KW_FALSE, KW_END, KW_DONE,
]
);
}
#[test]
fn break_continue_classify_as_keywords() {
let toks = assert_lossless("break continue");
let kinds: Vec<_> = toks
.iter()
.map(|(k, _)| *k)
.filter(|k| !k.is_trivia())
.collect();
assert_eq!(kinds, vec![KW_BREAK, KW_CONTINUE]);
}
#[test]
fn plain_ident_is_not_a_keyword() {
let toks = assert_lossless("flowchart");
assert_eq!(toks, vec![(IDENT, "flowchart")]);
}
#[test]
fn case_sensitive_keywords() {
let toks = assert_lossless("Flow FLOW");
assert_eq!(toks[0].0, IDENT);
assert_eq!(toks[2].0, IDENT);
}
#[test]
fn compound_tokens() {
let toks = assert_lossless("@[ <> -> <- => :: ==");
let kinds: Vec<_> = toks
.iter()
.map(|(k, _)| *k)
.filter(|k| !k.is_trivia())
.collect();
assert_eq!(
kinds,
vec![
AT_L_BRACKET,
GLUE,
DIVERT,
THREAD,
FAT_ARROW,
COLON_COLON,
EQ_EQ
]
);
}
#[test]
fn lone_at_is_error_token_not_swallowed() {
let toks = assert_lossless("@name");
assert_eq!(toks[0], (AT, "@"));
assert_eq!(toks[1], (IDENT, "name"));
}
#[test]
fn double_pipe_is_two_tokens_not_compound() {
let toks = assert_lossless("||");
assert_eq!(toks, vec![(PIPE, "|"), (PIPE, "|")]);
}
#[test]
fn integers_and_floats() {
let toks = assert_lossless("42 3.14 0");
let kinds: Vec<_> = toks
.iter()
.map(|(k, _)| *k)
.filter(|k| !k.is_trivia())
.collect();
assert_eq!(kinds, vec![INTEGER, FLOAT, INTEGER]);
}
#[test]
fn digit_start_ident_stays_ident() {
let toks = assert_lossless("3d6");
assert_eq!(toks, vec![(IDENT, "3d6")]);
}
#[test]
fn dot_after_int_without_digit_is_separate_dot() {
let toks = assert_lossless("42.foo");
let kinds: Vec<_> = toks.iter().map(|(k, _)| *k).collect();
assert_eq!(kinds, vec![INTEGER, DOT, IDENT]);
}
#[test]
fn line_comment_runs_to_newline() {
let toks = assert_lossless("// hello\nvar");
assert_eq!(toks[0], (LINE_COMMENT, "// hello"));
assert_eq!(toks[1], (NEWLINE, "\n"));
}
#[test]
fn block_comment_spans_lines() {
let toks = assert_lossless("/* a\nb */var");
assert_eq!(toks[0], (BLOCK_COMMENT, "/* a\nb */"));
assert_eq!(toks[1], (KW_VAR, "var"));
}
#[test]
fn unterminated_block_comment_runs_to_eof_lossless() {
assert_lossless("/* never closes");
}
#[test]
fn string_literal_round_trips() {
let toks = assert_lossless("\"hello world\"");
assert_eq!(toks[0], (QUOTE, "\""));
assert_eq!(toks[1], (STRING_TEXT, "hello world"));
assert_eq!(toks[2], (QUOTE, "\""));
}
#[test]
fn string_escape_sequences() {
let toks = assert_lossless(r#""a\nb\t\\\"c""#);
let kinds: Vec<_> = toks.iter().map(|(k, _)| *k).collect();
assert!(kinds.contains(&STRING_ESCAPE));
}
#[test]
fn interpolation_inside_string_reenters_code_mode() {
let toks = assert_lossless("\"hp is {hp}\"");
let kinds: Vec<_> = toks.iter().map(|(k, _)| *k).collect();
assert_eq!(
kinds,
vec![QUOTE, STRING_TEXT, L_BRACE, IDENT, R_BRACE, QUOTE]
);
}
#[test]
fn interpolation_inside_string_with_trailing_text() {
let toks = assert_lossless("\"hp is {hp} now\"");
let kinds: Vec<_> = toks.iter().map(|(k, _)| *k).collect();
assert_eq!(
kinds,
vec![
QUOTE,
STRING_TEXT,
L_BRACE,
IDENT,
R_BRACE,
STRING_TEXT,
QUOTE
]
);
}
#[test]
fn unterminated_string_ends_at_newline() {
let toks = assert_lossless("\"never closes\nvar x");
assert_eq!(toks[0].0, QUOTE);
assert_eq!(toks[1].0, STRING_TEXT);
assert_eq!(toks[2].0, NEWLINE);
assert_eq!(toks[3].0, KW_VAR);
}
#[test]
fn brackets_recognized_inside_string_mode() {
let toks = assert_lossless("\"tired[.\"]\"");
let kinds: Vec<_> = toks.iter().map(|(k, _)| *k).collect();
assert!(kinds.contains(&L_BRACKET));
assert!(kinds.contains(&R_BRACKET));
}
#[test]
fn glue_breaks_string_text() {
let toks = assert_lossless("\"a<>b\"");
let kinds: Vec<_> = toks.iter().map(|(k, _)| *k).collect();
assert_eq!(kinds, vec![QUOTE, STRING_TEXT, GLUE, STRING_TEXT, QUOTE]);
}
#[test]
fn bom_is_whitespace_trivia() {
let src = "\u{FEFF}flow x() {}";
let toks = assert_lossless(src);
assert_eq!(toks[0].0, WHITESPACE);
assert_eq!(toks[0].1, "\u{FEFF}");
}
#[test]
fn crlf_and_lf_both_single_newline_tokens() {
let toks = assert_lossless("a\r\nb\nc\rd");
let newlines: Vec<_> = toks.iter().filter(|(k, _)| *k == NEWLINE).collect();
assert_eq!(newlines.len(), 3);
assert_eq!(newlines[0].1, "\r\n");
assert_eq!(newlines[1].1, "\n");
assert_eq!(newlines[2].1, "\r");
}
#[test]
fn unicode_prose_text_is_lossless_error_tokens() {
assert_lossless("héllo wörld 日本語 emoji: 🎉");
}
#[test]
fn mixed_content_never_panics_or_drops_bytes() {
let src = "flow greet(name) {\n Hello, {name}! <>\n {? * [Hi] Hi back! }\n}\n";
assert_lossless(src);
}
#[test]
fn plain_double_slash_is_line_comment() {
let toks = assert_lossless("// just a comment");
assert_eq!(toks, vec![(LINE_COMMENT, "// just a comment")]);
}
#[test]
fn triple_slash_is_doc_comment_outer() {
let toks = assert_lossless("/// a doc comment");
assert_eq!(toks, vec![(DOC_COMMENT_OUTER, "/// a doc comment")]);
}
#[test]
fn bang_slash_slash_is_doc_comment_inner() {
let toks = assert_lossless("//! module-level doc");
assert_eq!(toks, vec![(DOC_COMMENT_INNER, "//! module-level doc")]);
}
#[test]
fn quadruple_slash_stays_line_comment() {
let toks = assert_lossless("//// separator");
assert_eq!(toks, vec![(LINE_COMMENT, "//// separator")]);
}
#[test]
fn five_or_more_slashes_stay_line_comment() {
let toks = assert_lossless("///////");
assert_eq!(toks, vec![(LINE_COMMENT, "///////")]);
}
#[test]
fn bare_triple_slash_at_eof_is_doc_comment_outer() {
let toks = assert_lossless("///");
assert_eq!(toks, vec![(DOC_COMMENT_OUTER, "///")]);
}
#[test]
fn doc_comments_are_not_trivia() {
assert!(!DOC_COMMENT_OUTER.is_trivia());
assert!(!DOC_COMMENT_INNER.is_trivia());
assert!(DOC_COMMENT_OUTER.is_token());
assert!(DOC_COMMENT_INNER.is_token());
}
#[test]
fn doc_comment_lines_roundtrip_losslessly() {
let src = "/// line one\n/// line two\n//! inner doc\nflow x() {}\n";
assert_lossless(src);
}
#[test]
fn every_byte_is_covered_by_exactly_one_token() {
let src = "flow x(a, ref b) {\n @[effects(pure, silent, reads(gold, hp))]\n var y = 1 + 2 * (3 - 4)\n -> END\n}\n";
let tokens = assert_lossless(src);
let mut cursor = 0usize;
for (_, text) in &tokens {
assert_eq!(&src[cursor..cursor + text.len()], *text);
cursor += text.len();
}
assert_eq!(cursor, src.len());
}