docs: @Requires("nextToken == null")
    @Ensures("result == (nextToken != null)")
    protected boolean lex() throws IOException {
      int startOffset = currentOffset;
      StringBuilder buffer = new StringBuilder();
      int c = readChar(true);
      if (c == -1) {
        return false;
      }
      buffer.append((char) c);
      switch (c) {
        case '/':
          /* Comments. */
          c = readChar(true);
          switch (c) {
            case '/':
              buffer.append((char) c);
              do {
                c = readChar(false);
                buffer.append((char) c);
              } while (c != '\n');
              nextToken = new Token(TokenKind.COMMENT, buffer.toString(),
                                    startOffset);
              break;
            case '*':
              buffer.append((char) c);
              for (;;) {
                c = readChar(false);
                buffer.append((char) c);
                if (c == '*') {
                  c = readChar(false);
                  buffer.append((char) c);
                  if (c == '/') {
                    break;
                  }
                }
              }
              nextToken = new Token(TokenKind.COMMENT, buffer.toString(),
                                    startOffset);
              break;
            default:
              unreadChar(c);
              nextToken = new Token(TokenKind.SYMBOL, buffer.toString(),
                                    startOffset);
          }
          break;
  
        case '\'':
        case '"':
          /* Quoted strings. */
          int delim = c;
          for (;;) {
            c = readChar(false);
            buffer.append((char) c);
            if (c == delim) {
              break;
            }
            if (c == '\\') {
              buffer.append((char) readChar(false));
            }
          }
          nextToken = new Token(TokenKind.QUOTE, buffer.toString(), startOffset);
          break;
  
        default:
          if (Character.isJavaIdentifierStart(c)) {
            /* Identifiers. */
            while ((c = readChar(true)) != -1
                   && Character.isJavaIdentifierPart(c)) {
              buffer.append((char) c);
            }
            unreadChar(c);
            nextToken = new Token(TokenKind.WORD, buffer.toString(), startOffset);
          } else if (Character.isWhitespace(c)) {
            /* White space. */
            while ((c = readChar(true)) != -1 && Character.isWhitespace(c)) {
              buffer.append((char) c);
            }
            unreadChar(c);
            nextToken = new Token(TokenKind.SPACE, buffer.toString(),
                                  startOffset);
          } else {
            /* Symbol. */
            nextToken = new Token(TokenKind.SYMBOL, buffer.toString(),
                                  startOffset);
          }
      }
      return true;
    }