///|
/// The code unit at `at` as a `Char`, or `None` past the end. A unit of a
/// surrogate pair comes back as itself; the scanner only compares against
/// ASCII, and lexemes are sliced from the text, so no character is lost.
fn unit_at(text : String, at : @span.Offset) -> Char? {
  if at.0 >= text.length() {
    None
  } else {
    Some(text.code_unit_at(at.0).to_int().unsafe_to_char())
  }
}

///|
/// The unit `k` places after `at`.
fn peek(text : String, at : @span.Offset, k : Int32) -> Char? {
  unit_at(text, @span.Offset::new(at.0 + k).unwrap_or(at))
}

///|
fn is_alpha(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}

///|
fn is_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
fn is_ident_char(c : Char) -> Bool {
  is_alpha(c) || is_digit(c) || c == '_'
}

///|
fn is_hex(c : Char) -> Bool {
  is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}

///|
/// Whether the text at `at` starts with `s`.
fn looking_at(text : String, at : @span.Offset, s : String) -> Bool {
  let n = s.length()
  if at.0 + n > text.length() {
    return false
  }
  for k in 0.. Bool,
) -> @span.Offset {
  let mut at = from
  while unit_at(text, at) is Some(c) && pred(c) {
    at = at.next()
  }
  at
}

///|
fn slice(text : String, from : @span.Offset, to : @span.Offset) -> String {
  text.unsafe_substring(start=from.0, end=to.0)
}

///|
/// A `Step` for a token from `at` to `end`.
fn token_step(
  kind : TokenKind,
  text : String,
  at : @span.Offset,
  end : @span.Offset,
) -> Step {
  {
    piece: Tok(kind, slice(text, at, end), at.to(end)),
    end,
    diagnostics: [][:],
  }
}

///|
fn trivia_step(t : Trivia, end : @span.Offset) -> Step {
  { piece: Triv(t), end, diagnostics: [][:], }
}

///|
/// A `Step` for an `Error` token with one diagnostic.
fn error_step(
  text : String,
  at : @span.Offset,
  end : @span.Offset,
  d : @diagnostic.Diagnostic,
) -> Step {
  {
    piece: Tok(Error, slice(text, at, end), at.to(end)),
    end,
    diagnostics: [d][:],
  }
}

///|
fn lexical(
  code : String,
  title : String,
  message : String,
  span : @span.Span,
  notes? : Array[String],
  hint? : String,
) -> @diagnostic.Diagnostic {
  @diagnostic.Diagnostic::new(
    code~,
    severity=Error,
    title~,
    message~,
    span~,
    notes?,
    hint?,
  )
}