///|
/// Lexer for the moonpython interpreter.

///|
priv enum TokenKind {
  IntLit(@bigint.BigInt)
  FloatLit(Double)
  ComplexLit(Double)
  StrLit(String)
  FStrLit(String)
  BytesLit(String)
  BoolLit(Bool)
  NoneLit
  Ident(String)
  LParen
  RParen
  LBracket
  RBracket
  LBrace
  RBrace
  Comma
  Colon
  ColonAssign
  Dot
  Plus
  Minus
  Star
  At
  Slash
  SlashSlash
  Percent
  StarStar
  BitAnd
  BitOr
  BitXor
  ShiftLeft
  ShiftRight
  Tilde
  EqEq
  NotEq
  Lt
  Lte
  Gt
  Gte
  Assign
  And
  Or
  Not
  In
  Is
  If
  Else
  For
  Async
  Def
  Return
  Yield
  Await
  Lambda
  Assert
  Import
  From
  Del
  Global
  Nonlocal
  Raise
  Try
  Except
  Finally
  Class
  Pass
  Break
  Continue
  Invalid
  Eof
} derive(Eq)

///|
priv struct Token {
  kind : TokenKind
  start : Int32
  end : Int32
  line : Int32
  column : Int32
}

///|
fn make_parse_error(
  kind : ParseErrorKind,
  message : String,
  token : Token,
) -> ParseError {
  ParseError::{
    kind,
    message,
    span: Span::{
      start: token.start,
      end: token.end,
      line: token.line,
      column: token.column,
    },
  }
}

///|
fn unexpected_token_error(token : Token) -> ParseError {
  let message = if token.kind == Eof {
    "invalid syntax"
  } else if token.kind == RParen {
    "unmatched ')'"
  } else if token.kind == RBracket {
    "unmatched ']'"
  } else if token.kind == RBrace {
    "unmatched '}'"
  } else {
    "invalid syntax"
  }
  make_parse_error(ParseErrorKind::UnexpectedToken, message.to_string(), token)
}

///|
fn substring_chars(chars : Array[Char], start : Int32, end : Int32) -> String {
  let buf = StringBuilder()
  let safe_start = if start < 0 { 0 } else { start }
  let safe_end = if end > chars.length() { chars.length() } else { end }
  if safe_end <= safe_start {
    return ""
  }
  for i = safe_start; i < safe_end; i = i + 1 {
    buf.write_char(chars[i])
  }
  buf.to_string()
}

///|
fn substring(source : String, start : Int32, end : Int32) -> String {
  substring_chars(source.to_array(), start, end)
}

///|
fn find_char(source : String, target : Char) -> Int32? {
  let chars = source.to_array()
  for i = 0; i < chars.length(); i = i + 1 {
    if chars[i] == target {
      return Some(i)
    }
  }
  None
}

///|
fn find_last_char(source : String, target : Char) -> Int32? {
  let chars = source.to_array()
  let mut idx = chars.length() - 1
  while idx >= 0 {
    if chars[idx] == target {
      return Some(idx)
    }
    if idx == 0 {
      break
    }
    idx -= 1
  }
  None
}

///|
fn is_alpha(c : Char) -> Bool {
  let code = c.to_int()
  (c >= 'a' && c <= 'z') ||
  (c >= 'A' && c <= 'Z') ||
  (
    code >= 128 &&
    code != 0xA0 &&
    code != 0xA4 &&
    code != 0xFFFD &&
    code != 0xFF0C &&
    code != 0x3002
  )
}

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

///|
fn is_alnum(c : Char) -> Bool {
  is_alpha(c) || is_digit(c)
}

///|
fn strip_underscores(text : String) -> String {
  let buf = StringBuilder()
  for c in text.to_array() {
    if c != '_' {
      buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
fn digit_value(c : Char) -> Int32? {
  if c >= '0' && c <= '9' {
    Some(c.to_int() - 48)
  } else if c >= 'a' && c <= 'f' {
    Some(c.to_int() - 'a'.to_int() + 10)
  } else if c >= 'A' && c <= 'F' {
    Some(c.to_int() - 'A'.to_int() + 10)
  } else {
    None
  }
}

///|
fn parse_int_literal(text : String) -> @bigint.BigInt {
  let cleaned = strip_underscores(text)
  let chars = cleaned.to_array()
  let mut start = 0
  let mut base = 10
  if chars.length() >= 2 && chars[0] == '0' {
    match chars[1] {
      'x' | 'X' => {
        base = 16
        start = 2
      }
      'o' | 'O' => {
        base = 8
        start = 2
      }
      'b' | 'B' => {
        base = 2
        start = 2
      }
      _ => {
        base = 10
        start = 0
      }
    }
  }
  let mut value = 0N
  for i = start; i < chars.length(); i = i + 1 {
    let d = digit_value(chars[i])
    if d is Some(d) {
      if d >= base {
        break
      }
      value = value * @bigint.BigInt::from_int(base) +
        @bigint.BigInt::from_int(d)
    } else {
      break
    }
  }
  value
}

///|
fn parse_float_literal(text : String) -> Double {
  // The lexer has already validated the decimal syntax. Core conversion rounds
  // once to binary64; repeated decimal arithmetic can change the stored value.
  let cleaned = strip_underscores(text)
  @string.parse_double(cleaned[:]) catch {
    Failure("value out of range") => 1.0 / 0.0
    _ => 0.0
  }
}

///|
fn slice_tokens(
  tokens : Array[Token],
  start : Int32,
  end : Int32,
) -> Array[Token] {
  let result : Array[Token] = []
  for i = start; i < end; i = i + 1 {
    result.push(tokens[i])
  }
  result
}

///|
fn char_to_string(c : Char) -> String {
  let buf = StringBuilder()
  buf.write_char(c)
  buf.to_string()
}

///|
fn parse_hex_sequence(
  chars : Array[Char],
  start : Int32,
  count : Int32,
) -> (Int32, Int32)? {
  let mut value = 0
  let mut idx = start
  for i = 0; i < count; i = i + 1 {
    if idx >= chars.length() {
      return None
    }
    let d = digit_value(chars[idx])
    if d is Some(d) {
      value = value * 16 + d
      idx += 1
    } else {
      return None
    }
  }
  Some((value, idx))
}

///|
fn is_valid_codepoint(value : Int32) -> Bool {
  // CPython allows surrogate code points in string literals (e.g. "\uDC80"),
  // but still rejects values outside the Unicode range.
  value >= 0 && value <= 0x10FFFF
}

///|
fn parse_escape(chars : Array[Char], pos : Int32) -> (String, Int32, Bool) {
  if pos >= chars.length() {
    return ("\\", pos, true)
  }
  let c = chars[pos]
  match c {
    'a' => ("\u{07}", pos + 1, true) // bell
    'b' => ("\u{08}", pos + 1, true) // backspace
    'f' => ("\u{0c}", pos + 1, true) // form feed
    'n' => ("\n", pos + 1, true)
    't' => ("\t", pos + 1, true)
    'r' => ("\r", pos + 1, true)
    'v' => ("\u{0b}", pos + 1, true) // vertical tab
    '\\' => ("\\", pos + 1, true)
    '\'' => ("'", pos + 1, true)
    '"' => ("\"", pos + 1, true)
    'x' => {
      let seq = parse_hex_sequence(chars, pos + 1, 2)
      if seq is Some((value, next)) {
        (char_to_string(Int32::unsafe_to_char(value)), next, true)
      } else {
        ("\\x", pos + 1, true)
      }
    }
    'u' => {
      let seq = parse_hex_sequence(chars, pos + 1, 4)
      if seq is Some((value, next)) {
        if is_valid_codepoint(value) {
          (char_to_string(Int32::unsafe_to_char(value)), next, true)
        } else {
          (substring_chars(chars, pos - 1, next), next, false)
        }
      } else {
        ("\\u", pos + 1, true)
      }
    }
    'U' => {
      let seq = parse_hex_sequence(chars, pos + 1, 8)
      if seq is Some((value, next)) {
        if is_valid_codepoint(value) {
          (char_to_string(Int32::unsafe_to_char(value)), next, true)
        } else {
          (substring_chars(chars, pos - 1, next), next, false)
        }
      } else {
        ("\\U", pos + 1, true)
      }
    }
    _ =>
      if c >= '0' && c <= '7' {
        let mut value = c.to_int() - 48
        let mut idx = pos + 1
        for i = 0; i < 2; i = i + 1 {
          if idx < chars.length() && chars[idx] >= '0' && chars[idx] <= '7' {
            value = value * 8 + (chars[idx].to_int() - 48)
            idx += 1
          } else {
            break
          }
        }
        (char_to_string(Int32::unsafe_to_char(value)), idx, true)
      } else {
        ("\\" + c.to_string(), pos + 1, true)
      }
  }
}

///|
fn raw_quote_escaped(chars : Array[Char], pos : Int32) -> Bool {
  if pos == 0 {
    return false
  }
  let mut idx = pos - 1
  let mut count = 0
  while idx >= 0 && chars[idx] == '\\' {
    count += 1
    idx -= 1
  }
  count % 2 == 1
}

///|
fn has_unescaped_quote_ahead(
  chars : Array[Char],
  start : Int32,
  quote : Char,
) -> Bool {
  let mut i = start
  while i < chars.length() {
    if chars[i] == quote && !raw_quote_escaped(chars, i) {
      return true
    }
    i += 1
  }
  false
}

///|
fn parse_string_literal(
  chars : Array[Char],
  start : Int32,
  raw : Bool,
  fstring : Bool,
) -> (String, Int32, Bool, Bool) {
  let quote = chars[start]
  let triple = start + 2 < chars.length() &&
    chars[start + 1] == quote &&
    chars[start + 2] == quote
  let mut i = if triple { start + 3 } else { start + 1 }
  let buf = StringBuilder()
  let mut invalid_escape = false
  let mut brace_depth = 0
  let mut expr_quote : Char? = None
  let mut expr_escape = false
  while i < chars.length() {
    let c = chars[i]
    if fstring && brace_depth > 0 {
      // When lexing inside an f-string expression, treat quotes as part of the
      // embedded Python source. However, if the braces are unbalanced and we
      // reach the outer string delimiter, we still need to terminate the string
      // literal so that f-string evaluation can report a better SyntaxError.
      if expr_quote is None &&
        !triple &&
        c == quote &&
        !raw_quote_escaped(chars, i) &&
        !has_unescaped_quote_ahead(chars, i + 1, quote) {
        return (buf.to_string(), i + 1, true, invalid_escape)
      }
      match expr_quote {
        Some(q) =>
          if expr_escape {
            expr_escape = false
          } else if c == '\\' {
            expr_escape = true
          } else if c == q {
            expr_quote = None
          }
        None =>
          if c == '\'' || c == '"' {
            expr_quote = Some(c)
            expr_escape = false
          } else if c == '{' {
            brace_depth += 1
          } else if c == '}' {
            if brace_depth > 0 {
              brace_depth -= 1
            }
          }
      }
      buf.write_char(c)
      i += 1
      continue
    }
    if fstring && c == '{' {
      if i + 1 < chars.length() && chars[i + 1] == '{' {
        // Escaped brace in f-string literal.
        buf.write_char(c)
        i += 1
      } else {
        brace_depth = 1
        buf.write_char(c)
        i += 1
        continue
      }
    }
    if !raw && c == '\\' {
      if i + 1 < chars.length() && chars[i + 1] == '\n' {
        // Line continuation inside string literal
        i += 2
        continue
      }
      let (segment, next_i, ok) = parse_escape(chars, i + 1)
      if !ok {
        invalid_escape = true
        return (buf.to_string(), next_i, true, true)
      }
      buf.write_string(segment)
      i = next_i
      continue
    }
    if !triple && c == '\n' {
      return (buf.to_string(), i, false, invalid_escape)
    }
    if c == quote {
      if raw && raw_quote_escaped(chars, i) {
        // keep quote as part of raw string literal
      } else if triple {
        if i + 2 < chars.length() &&
          chars[i + 1] == quote &&
          chars[i + 2] == quote {
          i += 3
          return (buf.to_string(), i, true, invalid_escape)
        }
      } else if !fstring || brace_depth == 0 {
        i += 1
        return (buf.to_string(), i, true, invalid_escape)
      }
    }
    buf.write_char(c)
    i += 1
  }
  (buf.to_string(), i, false, invalid_escape)
}

///|
fn fstring_has_empty_expr(text : String) -> Bool {
  let chars = text.to_array()
  let mut i = 0
  let len = chars.length()
  while i < len {
    let c = chars[i]
    if c == '{' {
      if i + 1 < len && chars[i + 1] == '{' {
        i += 2
        continue
      }
      let after_open = substring(text, i + 1, len)
      match find_fstring_expr_close(after_open) {
        None => return false
        Some(close_rel) => {
          let close = i + 1 + close_rel
          let inner = substring(text, i + 1, close)
          if inner.trim(chars=" \t").length() == 0 {
            return true
          }
          i = close + 1
          continue
        }
      }
    }
    if c == '}' {
      if i + 1 < len && chars[i + 1] == '}' {
        i += 2
        continue
      }
    }
    i += 1
  }
  false
}

///|
fn tokenize_expr(
  source : String,
  line : Int32,
  base_offset : Int32,
  base_column : Int32,
) -> Result[Array[Token], ParseError] {
  fn column_at(pos : Int32, line_start : Int32, base_column : Int32) -> Int32 {
    let col_base = if line_start == 0 { base_column } else { 1 }
    pos - line_start + col_base
  }

  let chars = source.to_array()
  let mut i = 0
  let tokens : Array[Token] = []
  let len = chars.length()
  let mut current_line = line
  let mut line_start = 0
  while i < len {
    let c = chars[i]
    if c == '\n' {
      current_line += 1
      i += 1
      line_start = i
      continue
    }
    if c.to_int() == 0 {
      let token = {
        kind: Eof,
        start: base_offset + i,
        end: base_offset + i + 1,
        line: current_line,
        column: column_at(i, line_start, base_column),
      }
      return Err(
        make_parse_error(
          ParseErrorKind::Syntax,
          "ValueError: source code string cannot contain null bytes".to_string(),
          token,
        ),
      )
    }
    if c.to_int() == 0xA0 {
      let token = {
        kind: Eof,
        start: base_offset + i,
        end: base_offset + i + 1,
        line: current_line,
        column: column_at(i, line_start, base_column),
      }
      return Err(
        make_parse_error(
          ParseErrorKind::Syntax,
          "invalid non-printable character U+00A0".to_string(),
          token,
        ),
      )
    }
    if c.to_int() == 0xA4 {
      let token = {
        kind: Eof,
        start: base_offset + i,
        end: base_offset + i + 1,
        line: current_line,
        column: column_at(i, line_start, base_column),
      }
      return Err(
        make_parse_error(
          ParseErrorKind::Syntax,
          "invalid character '¤' (U+00A4)".to_string(),
          token,
        ),
      )
    }
    if c.to_int() == 0xFFFD {
      let token = {
        kind: Eof,
        start: base_offset + i,
        end: base_offset + i + 1,
        line: current_line,
        column: column_at(i, line_start, base_column),
      }
      return Err(
        make_parse_error(
          ParseErrorKind::Syntax,
          "invalid character '�' (U+FFFD)".to_string(),
          token,
        ),
      )
    }
    if c.to_int() == 0xFF0C {
      let token = {
        kind: Eof,
        start: base_offset + i,
        end: base_offset + i + 1,
        line: current_line,
        column: column_at(i, line_start, base_column),
      }
      return Err(
        make_parse_error(
          ParseErrorKind::Syntax,
          "invalid character ',' (U+FF0C)".to_string(),
          token,
        ),
      )
    }
    if c.to_int() == 0x3002 {
      let token = {
        kind: Eof,
        start: base_offset + i,
        end: base_offset + i + 1,
        line: current_line,
        column: column_at(i, line_start, base_column),
      }
      return Err(
        make_parse_error(
          ParseErrorKind::Syntax,
          "invalid character '。' (U+3002)".to_string(),
          token,
        ),
      )
    }
    if c == '\\' {
      if i + 1 < len && chars[i + 1] == '\n' {
        // Line continuation
        i += 2
        current_line += 1
        line_start = i
        continue
      }
      if i + 1 == len {
        let token = {
          kind: Eof,
          start: base_offset + i,
          end: base_offset + i + 1,
          line: current_line,
          column: column_at(i + 1, line_start, base_column),
        }
        return Err(
          make_parse_error(
            ParseErrorKind::UnexpectedEof,
            "unexpected EOF while parsing".to_string(),
            token,
          ),
        )
      }
      let token = {
        kind: Eof,
        start: base_offset + i + 1,
        end: base_offset + i + 2,
        line: current_line,
        column: column_at(i + 1, line_start, base_column),
      }
      return Err(
        make_parse_error(
          ParseErrorKind::Syntax,
          "unexpected character after line continuation character".to_string(),
          token,
        ),
      )
    }
    if c == '#' {
      // Comment until end of line
      while i < len && chars[i] != '\n' {
        i += 1
      }
      continue
    }
    if c == ' ' || c == '\t' {
      i += 1
    } else if c == '.' &&
      i + 2 < len &&
      chars[i + 1] == '.' &&
      chars[i + 2] == '.' {
      tokens.push({
        kind: NoneLit,
        start: base_offset + i,
        end: base_offset + i + 3,
        line: current_line,
        column: column_at(i, line_start, base_column),
      })
      i += 3
    } else if is_digit(c) || (c == '.' && i + 1 < len && is_digit(chars[i + 1])) {
      let start = i
      let mut has_dot = false
      let mut has_exp = false
      let mut is_prefixed = false
      if c == '0' && i + 1 < len {
        let next = chars[i + 1]
        if next == 'x' ||
          next == 'X' ||
          next == 'o' ||
          next == 'O' ||
          next == 'b' ||
          next == 'B' {
          is_prefixed = true
          let base = if next == 'x' || next == 'X' {
            16
          } else if next == 'o' || next == 'O' {
            8
          } else {
            2
          }
          let label = if base == 16 {
            "hexadecimal"
          } else if base == 8 {
            "octal"
          } else {
            "binary"
          }
          i += 2
          let mut digit_count = 0
          while i < len {
            let d = chars[i]
            if d == '_' {
              i += 1
            } else if digit_value(d) is Some(value) {
              if value >= base {
                let token = {
                  kind: Eof,
                  start: base_offset + i,
                  end: base_offset + i + 1,
                  line: current_line,
                  column: column_at(i, line_start, base_column),
                }
                return Err(
                  make_parse_error(
                    ParseErrorKind::Syntax,
                    "invalid digit '" +
                    d.to_string() +
                    "' in " +
                    label +
                    " literal",
                    token,
                  ),
                )
              }
              digit_count += 1
              i += 1
            } else {
              break
            }
          }
          if digit_count == 0 {
            let token = {
              kind: Eof,
              start: base_offset + start + 1,
              end: base_offset + start + 2,
              line: current_line,
              column: column_at(start + 1, line_start, base_column),
            }
            return Err(
              make_parse_error(
                ParseErrorKind::Syntax,
                "invalid " + label + " literal",
                token,
              ),
            )
          }
        }
      }
      if !is_prefixed {
        if c == '.' {
          has_dot = true
          i += 1
        }
        while i < len {
          let d = chars[i]
          if d == '_' {
            i += 1
          } else if is_digit(d) {
            i += 1
          } else if d == '.' && !has_dot && !has_exp {
            has_dot = true
            i += 1
          } else if (d == 'e' || d == 'E') && !has_exp {
            if d == 'e' &&
              i + 3 < len &&
              chars[i + 1] == 'l' &&
              chars[i + 2] == 's' &&
              chars[i + 3] == 'e' {
              break
            }
            let mut exp_index = i + 1
            let mut sign_pos : Int32? = None
            if exp_index < len &&
              (chars[exp_index] == '+' || chars[exp_index] == '-') {
              sign_pos = Some(exp_index)
              exp_index += 1
            }
            if exp_index < len && is_digit(chars[exp_index]) {
              has_exp = true
              i = exp_index
            } else {
              let error_pos = match sign_pos {
                Some(pos) => pos
                None => start
              }
              let token = {
                kind: Eof,
                start: base_offset + error_pos,
                end: base_offset + error_pos + 1,
                line: current_line,
                column: column_at(error_pos, line_start, base_column),
              }
              return Err(
                make_parse_error(
                  ParseErrorKind::Syntax,
                  "invalid decimal literal".to_string(),
                  token,
                ),
              )
            }
          } else {
            break
          }
        }
      }
      let mut is_complex = false
      if i < len && (chars[i] == 'j' || chars[i] == 'J') {
        is_complex = true
      } else if i < len && (is_alpha(chars[i]) || chars[i] == '_') {
        let mut j = i
        while j < len {
          let d = chars[j]
          if is_alnum(d) || d == '_' {
            j += 1
          } else {
            break
          }
        }
        let word = substring(source, i, j)
        let allow_juxtaposed = word == "if" ||
          word == "else" ||
          word == "for" ||
          word == "and" ||
          word == "or" ||
          word == "not" ||
          word == "in" ||
          word == "is"
        if !allow_juxtaposed {
          let token = {
            kind: Eof,
            start: base_offset + i,
            end: base_offset + i + 1,
            line: current_line,
            column: column_at(i, line_start, base_column),
          }
          return Err(
            make_parse_error(
              ParseErrorKind::Syntax,
              "invalid decimal literal".to_string(),
              token,
            ),
          )
        }
      }
      let text = substring(source, start, i)
      if is_complex && is_prefixed {
        let token = {
          kind: Eof,
          start: base_offset + start,
          end: base_offset + start + 1,
          line: current_line,
          column: column_at(start, line_start, base_column),
        }
        return Err(
          make_parse_error(
            ParseErrorKind::Syntax,
            "invalid decimal literal".to_string(),
            token,
          ),
        )
      }
      let complex_value = if is_complex {
        parse_float_literal(text)
      } else {
        0.0
      }
      let kind = if is_complex {
        ComplexLit(complex_value)
      } else if is_prefixed {
        IntLit(parse_int_literal(text))
      } else if has_dot || has_exp {
        FloatLit(parse_float_literal(text))
      } else {
        IntLit(parse_int_literal(text))
      }
      let end_pos = if is_complex { i + 1 } else { i }
      i = end_pos
      tokens.push({
        kind,
        start: base_offset + start,
        end: base_offset + end_pos,
        line: current_line,
        column: column_at(start, line_start, base_column),
      })
    } else if c == '"' || c == '\'' {
      let start = i
      let start_line = current_line
      let start_column = column_at(start, line_start, base_column)
      let triple = start + 2 < chars.length() &&
        chars[start + 1] == chars[start] &&
        chars[start + 2] == chars[start]
      let (value, next_i, closed, invalid_escape) = parse_string_literal(
        chars, i, false, false,
      )
      if !closed {
        let mut detected_line = current_line
        for ch in substring_chars(chars, start, chars.length()).to_array() {
          if ch == '\n' {
            detected_line += 1
          }
        }
        let message = if triple {
          "unterminated triple-quoted string literal (detected at line " +
          detected_line.to_string() +
          ")"
        } else {
          "unterminated string literal (detected at line " +
          detected_line.to_string() +
          ")"
        }
        let token = {
          kind: Eof,
          start: base_offset + start,
          end: base_offset + start + 1,
          line: current_line,
          column: column_at(start, line_start, base_column),
        }
        return Err(make_parse_error(ParseErrorKind::Syntax, message, token))
      }
      if invalid_escape {
        let token = {
          kind: Eof,
          start: base_offset + start,
          end: base_offset + start + 1,
          line: start_line,
          column: start_column,
        }
        return Err(
          make_parse_error(
            ParseErrorKind::Syntax,
            "invalid syntax".to_string(),
            token,
          ),
        )
      }
      i = next_i
      let kind = StrLit(value)
      tokens.push({
        kind,
        start: base_offset + start,
        end: base_offset + i,
        line: start_line,
        column: start_column,
      })
      let mut k = start
      while k < i {
        if chars[k] == '\n' {
          current_line += 1
          line_start = k + 1
        }
        k += 1
      }
    } else if is_alpha(c) || c == '_' {
      let start = i
      let mut raw_prefix = false
      let mut f_prefix = false
      let mut b_prefix = false
      let mut j = i
      while j < len {
        let d = chars[j]
        if d == 'r' || d == 'R' {
          raw_prefix = true
          j += 1
        } else if d == 'f' || d == 'F' {
          f_prefix = true
          j += 1
        } else if d == 'b' || d == 'B' {
          b_prefix = true
          j += 1
        } else if d == 'u' || d == 'U' {
          j += 1
        } else {
          break
        }
      }
      if j > i && j < len && (chars[j] == '"' || chars[j] == '\'') {
        let triple = j + 2 < chars.length() &&
          chars[j + 1] == chars[j] &&
          chars[j + 2] == chars[j]
        let (value, next_i, closed, invalid_escape) = parse_string_literal(
          chars, j, raw_prefix, f_prefix,
        )
        if !closed {
          let mut detected_line = current_line
          for ch in substring_chars(chars, start, chars.length()).to_array() {
            if ch == '\n' {
              detected_line += 1
            }
          }
          let message = if triple {
            "unterminated triple-quoted string literal (detected at line " +
            detected_line.to_string() +
            ")"
          } else {
            "unterminated string literal".to_string()
          }
          let token = {
            kind: Eof,
            start: base_offset + start,
            end: base_offset + start + 1,
            line: current_line,
            column: column_at(start, line_start, base_column),
          }
          return Err(make_parse_error(ParseErrorKind::Syntax, message, token))
        }
        if invalid_escape {
          let token = {
            kind: Eof,
            start: base_offset + start,
            end: base_offset + start + 1,
            line: current_line,
            column: column_at(start, line_start, base_column),
          }
          return Err(
            make_parse_error(
              ParseErrorKind::Syntax,
              "invalid syntax".to_string(),
              token,
            ),
          )
        }
        if b_prefix && f_prefix {
          let token = {
            kind: Eof,
            start: base_offset + start,
            end: base_offset + next_i,
            line: current_line,
            column: column_at(start, line_start, base_column),
          }
          return Err(
            make_parse_error(
              ParseErrorKind::Syntax,
              "bytes f-strings are not supported".to_string(),
              token,
            ),
          )
        }
        if f_prefix && fstring_has_empty_expr(value) {
          let mut err_line = current_line
          let mut err_line_start = line_start
          let mut k = start
          while k < next_i {
            if chars[k] == '\n' {
              err_line += 1
              err_line_start = k + 1
            }
            k += 1
          }
          let token = {
            kind: Eof,
            start: base_offset + next_i - 1,
            end: base_offset + next_i,
            line: err_line,
            column: column_at(next_i - 1, err_line_start, base_column),
          }
          return Err(
            make_parse_error(
              ParseErrorKind::Syntax,
              "f-string: empty expression not allowed".to_string(),
              token,
            ),
          )
        }
        i = next_i
        let kind = if b_prefix {
          BytesLit(value)
        } else if f_prefix {
          FStrLit(value)
        } else {
          StrLit(value)
        }
        tokens.push({
          kind,
          start: base_offset + start,
          end: base_offset + i,
          line: current_line,
          column: column_at(start, line_start, base_column),
        })
        let mut k = start
        while k < i {
          if chars[k] == '\n' {
            current_line += 1
            line_start = k + 1
          }
          k += 1
        }
      } else {
        while i < len {
          let d = chars[i]
          if is_alnum(d) || d == '_' {
            i += 1
          } else {
            break
          }
        }
        let ident = substring(source, start, i)
        let kind = match ident {
          "True" => BoolLit(true)
          "False" => BoolLit(false)
          "None" => NoneLit
          "and" => And
          "or" => Or
          "not" => Not
          "in" => In
          "is" => Is
          "if" => If
          "else" => Else
          "for" => For
          "async" => Async
          "def" => Def
          "return" => Return
          "yield" => Yield
          "await" => Await
          "lambda" => Lambda
          "assert" => Assert
          "import" => Import
          "from" => From
          "del" => Del
          "global" => Global
          "nonlocal" => Nonlocal
          "raise" => Raise
          "try" => Try
          "except" => Except
          "finally" => Finally
          "class" => Class
          "pass" => Pass
          "break" => Break
          "continue" => Continue
          _ => Ident(ident)
        }
        tokens.push({
          kind,
          start: base_offset + start,
          end: base_offset + i,
          line: current_line,
          column: column_at(start, line_start, base_column),
        })
      }
    } else {
      let start = i
      let (kind, consumed) = match c {
        '(' => (LParen, 1)
        ')' => (RParen, 1)
        '[' => (LBracket, 1)
        ']' => (RBracket, 1)
        '{' => (LBrace, 1)
        '}' => (RBrace, 1)
        ',' => (Comma, 1)
        ':' =>
          if i + 1 < len && chars[i + 1] == '=' {
            (ColonAssign, 2)
          } else {
            (Colon, 1)
          }
        '.' => (Dot, 1)
        '+' =>
          if i + 1 < len && chars[i + 1] == '=' {
            (Assign, 2)
          } else {
            (Plus, 1)
          }
        '-' =>
          if i + 1 < len && chars[i + 1] == '=' {
            (Assign, 2)
          } else {
            (Minus, 1)
          }
        '*' =>
          if i + 1 < len && chars[i + 1] == '*' {
            if i + 2 < len && chars[i + 2] == '=' {
              (Assign, 3)
            } else {
              (StarStar, 2)
            }
          } else if i + 1 < len && chars[i + 1] == '=' {
            (Assign, 2)
          } else {
            (Star, 1)
          }
        '@' => (At, 1)
        '/' =>
          if i + 1 < len && chars[i + 1] == '/' {
            if i + 2 < len && chars[i + 2] == '=' {
              (Assign, 3)
            } else {
              (SlashSlash, 2)
            }
          } else if i + 1 < len && chars[i + 1] == '=' {
            (Assign, 2)
          } else {
            (Slash, 1)
          }
        '%' =>
          if i + 1 < len && chars[i + 1] == '=' {
            (Assign, 2)
          } else {
            (Percent, 1)
          }
        '&' =>
          if i + 1 < len && chars[i + 1] == '=' {
            (Assign, 2)
          } else {
            (BitAnd, 1)
          }
        '|' =>
          if i + 1 < len && chars[i + 1] == '=' {
            (Assign, 2)
          } else {
            (BitOr, 1)
          }
        '^' =>
          if i + 1 < len && chars[i + 1] == '=' {
            (Assign, 2)
          } else {
            (BitXor, 1)
          }
        '<' =>
          if i + 1 < len && chars[i + 1] == '<' {
            if i + 2 < len && chars[i + 2] == '=' {
              (Assign, 3)
            } else {
              (ShiftLeft, 2)
            }
          } else if i + 1 < len && chars[i + 1] == '=' {
            (Lte, 2)
          } else {
            (Lt, 1)
          }
        '>' =>
          if i + 1 < len && chars[i + 1] == '>' {
            if i + 2 < len && chars[i + 2] == '=' {
              (Assign, 3)
            } else {
              (ShiftRight, 2)
            }
          } else if i + 1 < len && chars[i + 1] == '=' {
            (Gte, 2)
          } else {
            (Gt, 1)
          }
        '~' => (Tilde, 1)
        '=' =>
          if i + 1 < len && chars[i + 1] == '=' {
            (EqEq, 2)
          } else {
            (Assign, 1)
          }
        '!' =>
          if i + 1 < len && chars[i + 1] == '=' {
            (NotEq, 2)
          } else {
            // Preserve the invalid token so an earlier grammar error in the
            // same expression is diagnosed before this later character.
            (Invalid, 1)
          }
        _ => {
          let token = {
            kind: Eof,
            start: base_offset + start,
            end: base_offset + start + 1,
            line: current_line,
            column: column_at(start, line_start, base_column),
          }
          return Err(unexpected_token_error(token))
        }
      }
      tokens.push({
        kind,
        start: base_offset + start,
        end: base_offset + start + consumed,
        line: current_line,
        column: column_at(start, line_start, base_column),
      })
      i += consumed
    }
  }
  tokens.push({
    kind: Eof,
    start: base_offset + len,
    end: base_offset + len,
    line: current_line,
    column: column_at(len, line_start, base_column),
  })
  Ok(tokens)
}