mod reclassify;
mod token;
use logos::Logos;
#[expect(
clippy::expect_used,
reason = "the span comes from lexing this same `src`, so it always slices"
)]
fn span_text(src: &str, span: core::ops::Range<usize>) -> &str {
src.get(span)
.expect("a span produced by lexing this source should slice it")
}
use crate::v1_1::lexer::reclassify::reclassify;
pub use crate::v1_1::lexer::token::Token;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Language {
Adl,
Odin,
Bel,
El,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Spanned {
pub token: Token,
pub span: std::ops::Range<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unrecognised token {text:?} at byte offset {}", .span.start)]
pub struct LexError {
pub span: std::ops::Range<usize>,
pub text: String,
}
fn retag_odin_value_words_in_key_position(src: &str, spanned: &mut [Spanned]) {
let mut index = 0;
while index < spanned.len() {
let is_value_word = matches!(
spanned.get(index).map(|s| &s.token),
Some(Token::SymTrue | Token::SymFalse | Token::SymInfinity)
);
let before_eq = matches!(spanned.get(index + 1).map(|s| &s.token), Some(Token::SymEq));
if is_value_word
&& before_eq
&& let Some(entry) = spanned.get_mut(index)
{
let slice = span_text(src, entry.span.clone());
entry.token = if slice.starts_with(|c: char| c.is_ascii_uppercase()) {
Token::AlphaUcId(slice.to_owned())
} else {
Token::AlphaLcId(slice.to_owned())
};
}
index += 1;
}
}
pub fn lex(language: Language, src: &str) -> Result<Vec<Spanned>, LexError> {
let mut out = Vec::new();
let mut lexer = Token::lexer(src);
while let Some(result) = lexer.next() {
let span = lexer.span();
let text = span_text(src, span.clone());
let Ok(produced) = result else {
let end = stuck_at(language, src, span.start, span.end);
return Err(LexError {
text: span_text(src, span.start..end).to_owned(),
span: span.start..end,
});
};
match reclassify(language, &produced, text, src, span.start) {
Some(Token::Bom) => {}
Some(read) => out.push(Spanned { token: read, span }),
None => {
let resumed = narrow(language, src, span.start, span.end, &mut out)?;
lexer = Token::lexer(src);
lexer.bump(resumed);
}
}
}
if language == Language::Odin {
retag_odin_value_words_in_key_position(src, &mut out);
}
Ok(out)
}
fn narrow(
language: Language,
src: &str,
start: usize,
limit: usize,
out: &mut Vec<Spanned>,
) -> Result<usize, LexError> {
let mut end = previous_boundary(src, limit, start);
while end > start {
if let Some(produced) = single_token(src, start, end) {
let text = span_text(src, start..end);
if let Some(read) = reclassify(language, &produced, text, src, start) {
if !matches!(read, Token::Bom) {
out.push(Spanned {
token: read,
span: start..end,
});
}
return Ok(end);
}
}
end = previous_boundary(src, end, start);
}
let end = next_boundary(src, start);
Err(LexError {
span: start..end,
text: span_text(src, start..end).to_owned(),
})
}
fn stuck_at(language: Language, src: &str, start: usize, union_end: usize) -> usize {
let cannot_start =
language != Language::Adl && src.get(start..).is_some_and(|rest| rest.starts_with('?'));
if cannot_start {
next_boundary(src, start)
} else {
union_end
}
}
fn single_token(src: &str, start: usize, end: usize) -> Option<Token> {
let text = src.get(start..end)?;
let mut lexer = Token::lexer(text);
let produced = lexer.next()?.ok()?;
(lexer.span().end == text.len()).then_some(produced)
}
fn previous_boundary(src: &str, at: usize, floor: usize) -> usize {
let mut back = at.saturating_sub(1);
while back > floor && !src.is_char_boundary(back) {
back -= 1;
}
back
}
fn next_boundary(src: &str, at: usize) -> usize {
let mut forward = at.saturating_add(1);
while forward < src.len() && !src.is_char_boundary(forward) {
forward += 1;
}
forward.min(src.len())
}
#[cfg(test)]
mod tests {
use super::{Language, Token, lex};
fn adl(src: &str) -> Vec<Token> {
lex(Language::Adl, src)
.unwrap_or_else(|e| panic!("lex failed: {e}"))
.into_iter()
.map(|s| s.token)
.collect()
}
fn odin(src: &str) -> Vec<Token> {
lex(Language::Odin, src)
.unwrap_or_else(|e| panic!("lex failed: {e}"))
.into_iter()
.map(|s| s.token)
.collect()
}
fn bel(src: &str) -> Vec<Token> {
lex(Language::Bel, src)
.unwrap_or_else(|e| panic!("lex failed: {e}"))
.into_iter()
.map(|s| s.token)
.collect()
}
#[test]
fn codes_and_identifiers() {
assert_eq!(adl("id1"), vec![Token::RootIdCode("id1".into())]);
assert_eq!(adl("id1.1"), vec![Token::RootIdCode("id1.1".into())]);
assert_eq!(adl("id2"), vec![Token::IdCode("id2".into())]);
assert_eq!(adl("id0.1"), vec![Token::IdCode("id0.1".into())]);
assert_eq!(adl("at0000"), vec![Token::AtCode("at0000".into())]);
assert_eq!(adl("at0.1"), vec![Token::AtCode("at0.1".into())]);
assert_eq!(adl("ac1"), vec![Token::AcCode("ac1".into())]);
assert_eq!(
adl("OBSERVATION"),
vec![Token::AlphaUcId("OBSERVATION".into())]
);
assert_eq!(adl("items"), vec![Token::AlphaLcId("items".into())]);
assert_eq!(adl("identity"), vec![Token::AlphaLcId("identity".into())]);
}
#[test]
fn archetype_and_version_ids() {
assert_eq!(
adl("openehr-TEST_PKG-WHOLE.most_minimal.v2.0.0"),
vec![Token::ArchetypeId(
"openehr-TEST_PKG-WHOLE.most_minimal.v2.0.0".into()
)]
);
assert_eq!(
adl("openehr-TASK_PLANNING-TASK_PLAN.good_include.v0"),
vec![Token::ArchetypeId(
"openehr-TASK_PLANNING-TASK_PLAN.good_include.v0".into()
)]
);
assert_eq!(adl("2.0.5"), vec![Token::VersionId("2.0.5".into())]);
assert_eq!(adl("1.0.2"), vec![Token::VersionId("1.0.2".into())]);
}
#[test]
fn iso_values_and_partials() {
assert_eq!(
adl("2004-06-01"),
vec![Token::Iso8601Date("2004-06-01".into())]
);
assert_eq!(adl("2004-06"), vec![Token::Iso8601Date("2004-06".into())]);
assert_eq!(
adl("2004-06-??"),
vec![Token::Iso8601Date("2004-06-??".into())]
);
assert_eq!(
adl("2004-06-01T10:30:00"),
vec![Token::Iso8601DateTime("2004-06-01T10:30:00".into())]
);
assert_eq!(adl("10:30:00"), vec![Token::Iso8601Time("10:30:00".into())]);
assert_eq!(adl("P1Y2M"), vec![Token::Iso8601Duration("P1Y2M".into())]);
assert_eq!(adl("PT30M"), vec![Token::Iso8601Duration("PT30M".into())]);
assert_eq!(adl("P0W"), vec![Token::Iso8601Duration("P0W".into())]);
}
#[test]
fn constraint_patterns() {
assert_eq!(
adl("yyyy-mm-dd"),
vec![Token::DateConstraintPattern("yyyy-mm-dd".into())]
);
assert_eq!(
adl("yyyy-??-XX"),
vec![Token::DateConstraintPattern("yyyy-??-XX".into())]
);
assert_eq!(
adl("hh:mm:ss"),
vec![Token::TimeConstraintPattern("hh:mm:ss".into())]
);
assert_eq!(
adl("yyyy-mm-ddThh:mm:ss"),
vec![Token::DateTimeConstraintPattern(
"yyyy-mm-ddThh:mm:ss".into()
)]
);
assert_eq!(
adl("PYMD"),
vec![Token::DurationConstraintPattern("PYMD".into())]
);
assert_eq!(
adl("POINT_EVENT"),
vec![Token::AlphaUcId("POINT_EVENT".into())]
);
}
#[test]
fn interval_and_range_symbols() {
assert_eq!(
adl("1..5"),
vec![
Token::Integer("1".into()),
Token::SymIvlSep,
Token::Integer("5".into()),
]
);
assert_eq!(adl("1.5"), vec![Token::Real("1.5".into())]);
assert_eq!(
adl("|>=0.0..<10.0|"),
vec![
Token::SymIvlDelim,
Token::SymGe,
Token::Real("0.0".into()),
Token::SymIvlSep,
Token::SymLt,
Token::Real("10.0".into()),
Token::SymIvlDelim,
]
);
}
#[test]
fn term_code_ref_and_embedded_uri() {
assert_eq!(
adl("[ISO_639-1::en]"),
vec![Token::TermCodeRef("[ISO_639-1::en]".into())]
);
assert_eq!(
adl("<http://loinc.org/id/9272-6>"),
vec![Token::EmbeddedUri("<http://loinc.org/id/9272-6>".into())]
);
assert_eq!(
adl("<[ISO_639-1::en]>"),
vec![
Token::SymLt,
Token::TermCodeRef("[ISO_639-1::en]".into()),
Token::SymGt,
]
);
}
#[test]
fn brackets_with_codes_are_not_term_codes() {
assert_eq!(
adl("[id2]"),
vec![
Token::LBracket,
Token::IdCode("id2".into()),
Token::RBracket
]
);
assert_eq!(
adl("[ac1]"),
vec![
Token::LBracket,
Token::AcCode("ac1".into()),
Token::RBracket
]
);
}
#[test]
fn unicode_operators_lex() {
assert_eq!(adl("\u{2208}"), vec![Token::SymMatches]);
assert_eq!(adl("\u{2227}"), vec![Token::SymAnd]);
assert_eq!(adl("\u{2203}"), vec![Token::SymThereExists]);
assert_eq!(adl("matches"), vec![Token::SymMatches]);
assert_eq!(adl("*"), vec![Token::SymStar]);
assert_eq!(adl("\u{2217}"), vec![Token::SymStar]);
}
#[test]
fn comments_and_overlay_separator_are_skipped() {
assert_eq!(
adl("id1 -- a trailing comment\nitems"),
vec![
Token::RootIdCode("id1".into()),
Token::AlphaLcId("items".into())
]
);
assert_eq!(
adl("--------------\ntemplate_overlay"),
vec![Token::AlphaLcId("template_overlay".into())]
);
}
#[test]
fn strings_and_escapes() {
assert_eq!(
adl(r#""a\"x'c\\d""#),
vec![Token::String(r#""a\"x'c\\d""#.into())]
);
assert_eq!(
adl("\"line1\nline2\""),
vec![Token::String("\"line1\nline2\"".into())]
);
assert!(lex(Language::Adl, r#""bad \d escape""#).is_err());
assert_eq!(
adl("\u{feff}archetype"),
vec![Token::AlphaLcId("archetype".into())]
);
}
#[test]
fn character_escapes() {
for ok in [
r"'\n'", r"'\t'", r"'\r'", r"'\\'", r#"'\"'"#, r"'\''", "'x'", "'ü'",
] {
assert!(
lex(Language::Adl, ok).is_ok(),
"legal character must lex: {ok}"
);
}
assert!(lex(Language::Adl, r"'\q'").is_err());
assert!(lex(Language::Adl, r"'\d'").is_err());
}
#[test]
fn boolean_word_symbols() {
assert_eq!(adl("True"), vec![Token::SymTrue]);
assert_eq!(adl("False"), vec![Token::SymFalse]);
assert_eq!(adl("true"), vec![Token::SymTrue]);
}
#[test]
fn keywords_are_case_insensitive() {
assert_eq!(adl("MATCHES"), vec![Token::SymMatches]);
assert_eq!(adl("Is_In"), vec![Token::SymMatches]);
assert_eq!(adl("OCCURRENCES"), vec![Token::SymOccurrences]);
assert_eq!(adl("Existence"), vec![Token::SymExistence]);
assert_eq!(adl("CaRdInAlItY"), vec![Token::SymCardinality]);
assert_eq!(adl("ORDERED"), vec![Token::SymOrdered]);
assert_eq!(adl("UNORDERED"), vec![Token::SymUnordered]);
assert_eq!(adl("Unique"), vec![Token::SymUnique]);
assert_eq!(adl("INFINITY"), vec![Token::SymInfinity]);
assert_eq!(adl("Use_Node"), vec![Token::SymUseNode]);
assert_eq!(adl("ALLOW_ARCHETYPE"), vec![Token::SymAllowArchetype]);
assert_eq!(adl("Include"), vec![Token::SymInclude]);
assert_eq!(adl("EXCLUDE"), vec![Token::SymExclude]);
assert_eq!(adl("Before"), vec![Token::SymBefore]);
assert_eq!(adl("AFTER"), vec![Token::SymAfter]);
assert_eq!(adl("TRUE"), vec![Token::SymTrue]);
assert_eq!(adl("fAlSe"), vec![Token::SymFalse]);
assert_eq!(adl("NOT"), vec![Token::SymNot]);
assert_eq!(adl("Implies"), vec![Token::SymImplies]);
assert_eq!(adl("there_exists"), vec![Token::SymThereExists]);
assert_eq!(
adl("There_Exists"),
vec![Token::AlphaUcId("There_Exists".into())]
);
}
#[test]
fn infinity_is_a_keyword_not_an_identifier() {
assert_eq!(
adl("|0..infinity|"),
vec![
Token::SymIvlDelim,
Token::Integer("0".into()),
Token::SymIvlSep,
Token::SymInfinity,
Token::SymIvlDelim,
]
);
assert_eq!(
adl("|-infinity..5.0|"),
vec![
Token::SymIvlDelim,
Token::SymMinus,
Token::SymInfinity,
Token::SymIvlSep,
Token::Real("5.0".into()),
Token::SymIvlDelim,
]
);
}
#[test]
fn pattern_variants() {
assert_eq!(
adl("1995-??-XX"),
vec![Token::DateConstraintPattern("1995-??-XX".into())]
);
assert_eq!(
adl("1995-mm-dd"),
vec![Token::DateConstraintPattern("1995-mm-dd".into())]
);
assert_eq!(
adl("hh:mm:ss+hh:mm"),
vec![Token::TimeConstraintPattern("hh:mm:ss+hh:mm".into())]
);
assert_eq!(
adl("hh:mm:ss-hh"),
vec![Token::TimeConstraintPattern("hh:mm:ss-hh".into())]
);
assert_eq!(
adl("hh:mm:ss+hhmm"),
vec![Token::TimeConstraintPattern("hh:mm:ss+hhmm".into())]
);
assert_eq!(
adl("hh:mm:ssZ"),
vec![Token::TimeConstraintPattern("hh:mm:ssZ".into())]
);
assert_eq!(
adl("yyyy-mm-dd hh:mm:XX"),
vec![Token::DateTimeConstraintPattern(
"yyyy-mm-dd hh:mm:XX".into()
)]
);
assert_eq!(
adl("yyyy-mm-ddThh:mm:ss\u{00B1}hh:mm"),
vec![Token::DateTimeConstraintPattern(
"yyyy-mm-ddThh:mm:ss\u{00B1}hh:mm".into()
)]
);
assert_eq!(
adl("2004-06-01"),
vec![Token::Iso8601Date("2004-06-01".into())]
);
}
#[test]
fn caret_delimited_regexp_lexes() {
assert_eq!(
adl("{^km/h|mi/h^}"),
vec![Token::ContainedRegexp("{^km/h|mi/h^}".into())]
);
assert_eq!(
adl(r"{/km\/h|mi\/h/}"),
vec![Token::ContainedRegexp(r"{/km\/h|mi\/h/}".into())]
);
}
#[test]
fn odin_reserves_no_cadl_or_bel_keyword() {
for word in [
"matches",
"occurrences",
"existence",
"cardinality",
"ordered",
"include",
"then",
"and",
"or",
"not",
"implies",
"for_all",
"there_exists",
"in",
"use_archetype",
] {
assert_eq!(
odin(word),
vec![Token::AlphaLcId(word.to_owned())],
"{word} must be an ODIN identifier"
);
}
assert_eq!(odin("MATCHES"), vec![Token::AlphaUcId("MATCHES".into())]);
}
#[test]
fn odin_has_no_adl_only_token_classes() {
assert_eq!(odin("at0000"), vec![Token::AlphaLcId("at0000".into())]);
assert_eq!(
odin("id1.1"),
vec![
Token::AlphaLcId("id1".into()),
Token::SymDot,
Token::Integer("1".into()),
]
);
assert_eq!(
odin("2.0.5"),
vec![
Token::Real("2.0".into()),
Token::SymDot,
Token::Integer("5".into()),
]
);
assert_eq!(
odin("yyyy-mm-dd"),
vec![
Token::AlphaLcId("yyyy".into()),
Token::SymMinus,
Token::AlphaLcId("mm".into()),
Token::SymMinus,
Token::AlphaLcId("dd".into()),
]
);
for refused in ["$v", "{", ":", "%", "^", "!="] {
assert!(
lex(Language::Odin, refused).is_err(),
"ODIN must refuse {refused:?}"
);
}
assert_eq!(odin("@"), vec![Token::SymAt]);
}
#[test]
fn local_term_code_ref_is_odin_only() {
assert_eq!(
odin("[at0200]"),
vec![Token::LocalTermCodeRef("[at0200]".into())]
);
assert_eq!(
adl("[at0200]"),
vec![
Token::LBracket,
Token::AtCode("at0200".into()),
Token::RBracket,
]
);
assert_eq!(
odin("[1]"),
vec![Token::LBracket, Token::Integer("1".into()), Token::RBracket,]
);
}
#[test]
fn space_separated_date_time_is_an_odin_widening() {
assert_eq!(
odin("2004-11-02 09:31:04+1000"),
vec![Token::Iso8601DateTime("2004-11-02T09:31:04+1000".into())]
);
assert_eq!(
adl("2004-11-02 09:31:04+1000"),
vec![
Token::DateTimeConstraintPattern("2004-11-02 09:31:04".into()),
Token::SymPlus,
Token::Integer("1000".into()),
]
);
}
#[test]
fn multi_line_string_leaders_are_stripped_for_odin_only() {
let src = " text = <\"first\n second\">";
let stripped = Token::String("\"first\nsecond\"".into());
let verbatim = Token::String("\"first\n second\"".into());
assert!(odin(src).contains(&stripped));
assert!(adl(src).contains(&verbatim));
}
#[test]
fn bel_keeps_its_own_narrower_lexical_surface() {
assert_eq!(bel("matches"), vec![Token::SymMatches]);
assert_eq!(bel("MATCHES"), vec![Token::AlphaUcId("MATCHES".into())]);
assert_eq!(bel("in"), vec![Token::SymIn]);
assert_eq!(adl("in"), vec![Token::AlphaLcId("in".into())]);
assert_eq!(bel("<>"), vec![Token::SymNe]);
assert_eq!(adl("<>"), vec![Token::SymLt, Token::SymGt]);
assert_eq!(
bel("29e6"),
vec![Token::Integer("29".into()), Token::AlphaLcId("e6".into())]
);
assert_eq!(adl("29e6"), vec![Token::Integer("29e6".into())]);
assert_eq!(bel("TRUE"), vec![Token::AlphaUcId("TRUE".into())]);
assert_eq!(adl("TRUE"), vec![Token::SymTrue]);
assert!(lex(Language::Bel, "_default").is_err());
assert_eq!(bel("//foo/bar"), vec![Token::AdlPath("//foo/bar".into())]);
assert_eq!(
bel("foo[at0001]"),
vec![Token::AdlPath("foo[at0001]".into())]
);
assert_eq!(
adl("foo[at0001]"),
vec![
Token::AlphaLcId("foo".into()),
Token::LBracket,
Token::AtCode("at0001".into()),
Token::RBracket,
]
);
}
#[test]
fn path_segment_head_case_differs_between_cadl_and_odin() {
assert_eq!(odin("/Foo/bar"), vec![Token::AdlPath("/Foo/bar".into())]);
assert_eq!(
adl("/Foo/bar"),
vec![
Token::SymSlash,
Token::AlphaUcId("Foo".into()),
Token::AdlPath("/bar".into()),
]
);
}
#[test]
fn byte_order_mark_is_language_specific() {
assert_eq!(
adl("\u{feff}archetype"),
vec![Token::AlphaLcId("archetype".into())]
);
assert_eq!(
odin("\u{feff}archetype"),
vec![Token::AlphaLcId("archetype".into())]
);
let error = lex(Language::Bel, "\u{feff}archetype").expect_err("BEL refuses a BOM");
assert_eq!(error.span, 0..3);
}
}