///|
/// `IdentifierStart = (1*"_" (ALPHA / DIGIT)) / ALPHA`.
fn at_identifier_start(text : String, at : @span.Offset) -> Bool {
match unit_at(text, at) {
Some(c) if is_alpha(c) => true
Some('_') => {
let after = skip_while(text, at, c => c == '_')
match unit_at(text, after) {
Some(c) => is_alpha(c) || is_digit(c)
None => false
}
}
_ => false
}
}
///|
fn scan_identifier(text : String, at : @span.Offset) -> Step {
token_step(Identifier, text, at, skip_while(text, at, is_ident_char))
}
///|
/// Spaces and tabs; a lone `\r` too, since it is not a line break.
fn scan_whitespace(text : String, at : @span.Offset) -> Step {
let mut end = at
while unit_at(text, end) is Some(' ' | '\t' | '\r') {
if unit_at(text, end) == Some('\r') && peek(text, end, 1) == Some('\n') {
break
}
end = end.next()
}
trivia_step(Whitespace(slice(text, at, end), at.to(end)), end)
}
///|
fn scan_newline(text : String, at : @span.Offset) -> Step {
let end = if unit_at(text, at) == Some('\r') {
at.next().next()
} else {
at.next()
}
trivia_step(Newline(slice(text, at, end), at.to(end)), end)
}
///|
/// `//` or `///` to the end of the line (the line break is not included).
fn scan_comment(text : String, at : @span.Offset) -> Step {
let kind = if peek(text, at, 2) == Some('/') { Doc } else { Line }
let text_start = at.advance(
if kind == Doc {
@span.Length::new(3).unwrap()
} else {
@span.Length::new(2).unwrap()
},
)
let mut end = text_start
while unit_at(text, end) is Some(c) && c != '\n' {
if c == '\r' && peek(text, end, 1) == Some('\n') {
break
}
end = end.next()
}
trivia_step(
Comment({ kind, text: slice(text, text_start, end), span: at.to(end), }),
end,
)
}
///|
fn scan_single(kind : TokenKind, text : String, at : @span.Offset) -> Step {
token_step(kind, text, at, at.next())
}
///|
/// One unexpected character (both units of a surrogate pair) as an `Error`
/// token with `SM-SCAN-001`.
fn scan_unexpected(text : String, at : @span.Offset) -> Step {
let c = text.code_unit_at(at.0).to_int()
let is_high = c >= 0xD800 && c <= 0xDBFF
let low_follows = match peek(text, at, 1) {
Some(l) => l.to_int() >= 0xDC00 && l.to_int() <= 0xDFFF
None => false
}
let end = if is_high && low_follows { at.next().next() } else { at.next() }
let shown = slice(text, at, end)
error_step(
text,
at,
end,
lexical(
"SM-SCAN-001",
"UNEXPECTED CHARACTER",
"I don't know what to do with `\{shown}`",
at.to(end),
notes=[
"I was reading tokens and found a character that cannot start one. Smithy IDL uses letters, digits, `_`, quotes, numbers and the punctuation `@ : := = . # $ { } [ ] ( ) ,`.",
],
),
)
}
///|
/// One token or one piece of trivia at `at`, or `None` at the end of the
/// text. Pure: the same text and offset always give the same step.
pub fn scan_at(text : String, at : @span.Offset) -> Step? {
guard unit_at(text, at) is Some(c) else { return None }
Some(
match c {
'\n' => scan_newline(text, at)
'\r' =>
if peek(text, at, 1) == Some('\n') {
scan_newline(text, at)
} else {
scan_whitespace(text, at)
}
' ' | '\t' => scan_whitespace(text, at)
',' => trivia_step(Comma(at.to(at.next())), at.next())
'/' if peek(text, at, 1) == Some('/') => scan_comment(text, at)
'@' => scan_single(At, text, at)
':' =>
if peek(text, at, 1) == Some('=') {
token_step(Walrus, text, at, at.next().next())
} else {
scan_single(Colon, text, at)
}
'=' => scan_single(Equal, text, at)
'.' => scan_single(Dot, text, at)
'#' => scan_single(Pound, text, at)
'$' => scan_single(Dollar, text, at)
'{' => scan_single(LBrace, text, at)
'}' => scan_single(RBrace, text, at)
'[' => scan_single(LBracket, text, at)
']' => scan_single(RBracket, text, at)
'(' => scan_single(LParen, text, at)
')' => scan_single(RParen, text, at)
'"' => scan_string_or_text_block(text, at)
'-' => scan_number(text, at)
_ =>
if is_digit(c) {
scan_number(text, at)
} else if at_identifier_start(text, at) {
scan_identifier(text, at)
} else {
scan_unexpected(text, at)
}
},
)
}