content: package com.tahini.lang;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  class Scanner {
  
      private final String source;
      private final String filename;
      private final List<Token> tokens = new ArrayList<>();
      private int start = 0;
      private int current = 0;
      private int line = 1;
  
      private static final Map<String, TokenType> keywords;
  
      static {
          keywords = new HashMap<>();
          keywords.put("and", TokenType.AND);
          keywords.put("class", TokenType.CLASS);
          keywords.put("else", TokenType.ELSE);
          keywords.put("false", TokenType.FALSE);
          keywords.put("for", TokenType.FOR);
          keywords.put("fun", TokenType.FUN);
          keywords.put("if", TokenType.IF);
          keywords.put("nil", TokenType.NIL);
          keywords.put("or", TokenType.OR);
          keywords.put("print", TokenType.PRINT);
          keywords.put("return", TokenType.RETURN);
          keywords.put("super", TokenType.SUPER);
          keywords.put("this", TokenType.THIS);
          keywords.put("true", TokenType.TRUE);
          keywords.put("var", TokenType.VAR);
          keywords.put("while", TokenType.WHILE);
          keywords.put("yolo", TokenType.QUESTION_MARK);
          keywords.put("goBust", TokenType.COLON);
          keywords.put("break", TokenType.BREAK);
          keywords.put("continue", TokenType.CONTINUE);
          keywords.put("precondition", TokenType.PRECONDITION);
          keywords.put("postcondition", TokenType.POSTCONDITION);
          keywords.put("assertion", TokenType.ASSERTION);
          keywords.put("check", TokenType.WARNING);
          keywords.put("test", TokenType.TEST);
          keywords.put("scoop", TokenType.SCOOP);
          keywords.put("into", TokenType.INTO);
      }
  
      Scanner(String source, String filename) {
          this.source = source;
          this.filename = filename;
      }
  
      List<Token> scanTokens() {
          while (!isAtEnd()) {
              // We are at the beginning of the next lexeme.
              start = current;
              scanToken();
          }
  
          tokens.add(new Token(TokenType.EOF, "", null, line, filename));
          return tokens;
      }
  
      private boolean isAtEnd() {
          return current >= source.length();
      }
  
      private void scanToken() {
          char c = advance();
          switch (c) {
              case '(' ->
                  addToken(TokenType.LEFT_PAREN);
              case ')' ->
                  addToken(TokenType.RIGHT_PAREN);
              case '{' ->
                  addToken(TokenType.LEFT_BRACE);
              case '}' ->
                  addToken(TokenType.RIGHT_BRACE);
              case '[' ->
                  addToken(TokenType.LEFT_SQUARE);
              case ']' ->
                  addToken(TokenType.RIGHT_SQUARE);
              case ',' ->
                  addToken(TokenType.COMMA);
              case '.' ->
                  addToken(TokenType.DOT);
              case '-' ->
                  addToken(TokenType.MINUS);
              case '+' ->
                  addToken(TokenType.PLUS);
              case ';' ->
                  addToken(TokenType.SEMICOLON);
              case '*' ->
                  addToken(TokenType.STAR);
              case '%' ->
                  addToken(TokenType.MODULO);
              case '!' ->
                  addToken(match('=') ? TokenType.BANG_EQUAL : TokenType.BANG);
              case '=' ->
                  addToken(match('=') ? TokenType.EQUAL_EQUAL : TokenType.EQUAL);
              case '<' ->
                  addToken(match('=') ? TokenType.LESS_EQUAL : TokenType.LESS);
              case '>' ->
                  addToken(match('=') ? TokenType.GREATER_EQUAL : TokenType.GREATER);
              case '/' -> {
                  if (match('/')) {
                      // A comment goes until the end of the line.
                      while (peek() != '\n' && !isAtEnd()) {
                          advance();
                      }
                  } else {
                      addToken(TokenType.SLASH);
                  }
              }
              case ' ', '\r', '\t' -> {
                  // Ignore whitespace.
              }
              case '\n' ->
                  line++;
              case '"' ->
                  string();
              case '?' ->
                  addToken(TokenType.QUESTION_MARK);
              case ':' -> {
                  if (match(':')) {
                      addToken(TokenType.NAMESPACE_SEPARATOR);
                  } else {
                      addToken(TokenType.COLON);
                  }
              }
              default -> {
                  if (isDigit(c)) {
                      number();
                  } else if (isAlpha(c)) {
                      identifier();
                  } else {
                      Tahini.error(filename, line, "Unexpected character.");
                  }
              }
          }
      }
  
      private void identifier() {
          while (isAlphaNumeric(peek())) {
              advance();
          }
  
          String text = source.substring(start, current);
          TokenType type = keywords.get(text);
          if (type == null) {
              type = TokenType.IDENTIFIER;
          }
          addToken(type);
      }
  
      private void number() {
          while (isDigit(peek())) {
              advance();
          }
  
          // Look for a fractional part.
          if (peek() == '.' && isDigit(peekNext())) {
              // Consume the "."
              advance();
  
              while (isDigit(peek())) {
                  advance();
              }
          }
  
          addToken(TokenType.NUMBER,
                  parseDouble(source, start, current));
      }
  
      private double parseDouble(String source, int start, int end) {
          return Double.parseDouble(source.subSequence(start, end).toString());
      }
  
      private void string() {
          while (peek() != '"' && !isAtEnd()) {
              if (peek() == '\n') {
                  line++;
              }
              advance();
          }
  
          if (isAtEnd()) {
              Tahini.error(filename, line, "Unterminated string.");
              return;
          }
  
          // The closing ".
          advance();
  
          // Trim the surrounding quotes.
          String value = source.substring(start + 1, current - 1);
          addToken(TokenType.STRING, value);
      }
  
      private boolean match(char expected) {
          if (isAtEnd()) {
              return false;
          }
          if (source.charAt(current) != expected) {
              return false;
          }
  
          current++;
          return true;
      }
  
      private char peek() {
          if (isAtEnd()) {
              return '\0';
          }
          return source.charAt(current);
      }
  
      private char peekNext() {
          if (current + 1 >= source.length()) {
              return '\0';
          }
          return source.charAt(current + 1);
      }
  
      private boolean isAlpha(char c) {
          return (c >= 'a' && c <= 'z')
                  || (c >= 'A' && c <= 'Z')
                  || c == '_';
      }
  
      private boolean isAlphaNumeric(char c) {
          return isAlpha(c) || isDigit(c);
      }
  
      private boolean isDigit(char c) {
          return c >= '0' && c <= '9';
      }
  
      private char advance() {
          return source.charAt(current++);
      }
  
      private void addToken(TokenType type) {
          addToken(type, null);
      }
  
      private void addToken(TokenType type, Object literal) {
          String text = source.substring(start, current);
          tokens.add(new Token(type, text, literal, line, filename));
      }
  }
  