///|
/// The digits at `from`: the offset after them and whether there was one.
fn digits(text : String, from : @span.Offset) -> (@span.Offset, Bool) {
  let end = skip_while(text, from, is_digit)
  (end, end > from)
}

///|
/// `Number = [Minus] Int [Frac] [Exp]`, with `Int = "0" / DigitOneToNine
/// *DIGIT`. A run that does not fit is one `Error` token covering every
/// digit, sign, dot, `e`, `+`/`-` and glued letter, with `SM-SCAN-007`.
fn scan_number(text : String, at : @span.Offset) -> Step {
  let mut ok = true
  let mut end = at
  if unit_at(text, end) == Some('-') {
    end = end.next()
  }
  // Int
  match unit_at(text, end) {
    Some('0') => {
      end = end.next()
      let (after, more) = digits(text, end)
      if more {
        ok = false
        end = after
      }
    }
    Some(c) if is_digit(c) => end = digits(text, end).0
    _ => ok = false
  }
  // Frac
  if unit_at(text, end) == Some('.') {
    let (after, some) = digits(text, end.next())
    end = after
    if !some {
      ok = false
    }
  }
  // Exp
  if unit_at(text, end) is Some('e' | 'E') {
    end = end.next()
    if unit_at(text, end) is Some('+' | '-') {
      end = end.next()
    }
    let (after, some) = digits(text, end)
    end = after
    if !some {
      ok = false
    }
  }
  // A letter or dot glued to a number is part of the bad run (`1x`, `-a`).
  if unit_at(text, end) is Some(c) && (is_ident_char(c) || c == '.') {
    ok = false
    end = skip_while(text, end, c => is_ident_char(c) || c == '.')
  }
  if ok {
    token_step(Number, text, at, end)
  } else {
    let shown = slice(text, at, end)
    error_step(
      text,
      at,
      end,
      lexical(
        "SM-SCAN-007",
        "INVALID NUMBER",
        "I cannot read `\{shown}` as a number",
        at.to(end),
        notes=[
          "A number is an optional `-`, then `0` or digits with no leading zero, then an optional fraction like `.25` and an optional exponent like `e10` or `E-3`.",
        ],
        hint="Write `0.5` rather than `.5`, `1.0` rather than `1.`, and drop leading zeros.",
      ),
    )
  }
}

///|
/// Whether a `Number` lexeme has no fraction and no exponent.
pub fn is_integer_lexeme(lexeme : String) -> Bool {
  for ch in lexeme {
    if ch == '.' || ch == 'e' || ch == 'E' {
      return false
    }
  }
  true
}