В прошлой статье мы кратко
обсудили процесс анализа выражений и составили грамматику нашего калькулятора. В этой статье - мы напишем
код лексического анализатора.
Код будем писать на Java 8.
Итак, лексический анализатор - это программа, которая преобразует поток символов в поток лексем.
Лексемы могут принадлежать к разным классам. При этом в процессе анализа может возникнуть ошибка. Для того, чтобы выразить эти концепции явно в коде, создадим следующие классы:
Код будем писать на Java 8.
Итак, лексический анализатор - это программа, которая преобразует поток символов в поток лексем.
Лексемы могут принадлежать к разным классам. При этом в процессе анализа может возникнуть ошибка. Для того, чтобы выразить эти концепции явно в коде, создадим следующие классы:
package ru.blogspot.toolkas.math.lexer;
public class TokenizeException extends Exception {
public TokenizeException() {
}
public TokenizeException(String message) {
super(message);
}
public TokenizeException(String message, Throwable cause) {
super(message, cause);
}
public TokenizeException(Throwable cause) {
super(cause);
}
public TokenizeException(String message, Throwable cause,
boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
Token - перечисление, которое содержит список токенов:
package ru.blogspot.toolkas.math.lexer;
public enum Token {
NUMBER, OPERATOR, BRACKET, EOF;
}
Lexem - класс лексемы, который хранит тип лексемы (токен) и значение:
package ru.blogspot.toolkas.math.lexer;
public class Lexem {
private final Token token;
private final String value;
public Lexem(Token token, String value) {
this.token = token;
this.value = value;
}
public Token getToken() {
return token;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "Lexem{" +
"token=" + token +
", value='" + value + '\'' +
'}';
}
}
Теперь сделаем самое интересное: напишем непосредственно лексический анализатор.
Для разработки алгоритма лексического анализатора напишем класс MathLexer, конструктор которого принимает исходное выражение в виде строки, и который имеет следующий метод:
package ru.blogspot.toolkas.math.lexer;
public class MathLexer {
public List tokenize() throws TokenizeException;
}
Такая структура класса в явном виде выражает тот факт, что лексический анализатор принимает поток символов, а возвращает последовательность лексем.
Не будем тянуть рекурсию за хвост - сразу приведу полный код лексического анализатора:
package ru.blogspot.toolkas.math.lexer;
import org.apache.commons.lang.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class MathLexer {
private TokenizeState state = TokenizeState.DEFAULT;
private String expression;
private String value = "";
private int position = -1;
public MathLexer(String expression) {
Objects.requireNonNull(expression, "expression can't be null");
this.expression = expression;
}
public List tokenize() throws TokenizeException {
List lexems = new ArrayList<>();
int ch;
while ((ch = get()) != -1) {
Lexem lexem = nextLexem(ch);
if (lexem != null) {
lexems.add(lexem);
}
}
if (state != TokenizeState.DEFAULT) {
Lexem lexem = nextLexem(ch);
if (lexem != null) {
lexems.add(lexem);
}
}
if (StringUtils.isNotBlank(value)) {
throw new TokenizeException("can't parse '" + value + "'");
}
lexems.add(new Lexem(Token.EOF, null));
return lexems;
}
private Lexem nextLexem(int b) throws TokenizeException {
char ch = (char) b;
switch (state) {
case DEFAULT:
if (Character.isDigit(ch)) {
state = TokenizeState.NUMBER_START;
position -= 1;
} else if (ch == '+' || ch == '-' || ch == '*' || ch == '/') {
state = TokenizeState.OPERATOR;
position -= 1;
} else if (ch == '(' || ch == ')') {
state = TokenizeState.BRACKET;
position -= 1;
} else if (Character.isWhitespace(ch)) {
return null;
} else {
throw new TokenizeException(
"unexpected character '" + ch +
"' in state " + state
);
}
break;
case NUMBER_START:
if (Character.isDigit(ch)) {
value += ch;
} else if (ch == '.') {
value += ch;
state = TokenizeState.NUMBER_END;
} else {
return createToken(Token.NUMBER, true);
}
break;
case NUMBER_END:
if (Character.isDigit(ch)) {
value += ch;
} else {
return createToken(Token.NUMBER, true);
}
break;
case OPERATOR:
if (ch == '+' || ch == '-' || ch == '*' || ch == '/') {
value += ch;
return createToken(Token.OPERATOR, false);
}
break;
case BRACKET:
if (ch == '(' || ch == ')') {
value += ch;
return createToken(Token.BRACKET, false);
}
break;
}
return null;
}
private int get() {
position += 1;
return position < expression.length() ?
expression.charAt(position) : -1;
}
private Lexem createToken(Token token, boolean back) {
Lexem lexem = new Lexem(token, value);
value = "";
state = TokenizeState.DEFAULT;
if (back) {
position -= 1;
}
return lexem;
}
private enum TokenizeState {
DEFAULT, NUMBER_START, NUMBER_END, OPERATOR, BRACKET;
}
}
Теперь разберем приведенный выше код.
В основе лексического анализатора лежит алгоритм конечного автомата. Конечный автомат в нашем случае имеет следующие состояния:
- DEFAULT - стартовое состояние конечного автомата;
- NUMBER_START - анализ целой части числа;
- NUMBER_END - анализ дробной части числа;
- OPERATOR - анализ оператора;
- BRACKET - анализ скобок;
Лексический анализатор переходит из состояния в состояние при встрече определенных символов.
Правила перехода из состояния в состояние можно описать так:
- DEFAULT. Получен символ цифры => переход в NUMBER_START;
- DEFAULT. Получен '+' => переход в OPERATOR;
- DEFAULT. Получен '-' => переход в OPERATOR;
- DEFAULT. Получен '*' => переход в OPERATOR;
- DEFAULT. Получен '/' => переход в OPERATOR;
- DEFAULT. Получен '(' => переход в BRACKET;
- DEFAULT. Получен ')' => переход в BRACKET;
- DEFAULT. Получен ' ' => переход в DEFAULT. Пропуск символа;
- DEFAULT. Получен любой другой символ => ошибка;
- NUMBER_START. Получена цифра => переход в NUMBER_START
- NUMBER_START. Получена точка=> переход в NUMBER_END;
- NUMBER_START. Получен любой другой символ => переход в DEFAULT.
- NUMBER_END. Получена цифра => переход в NUMBER_END;
- NUMBER_END. Получен любой другой символ => переход в DEFAULT;
- OPERATOR. Получен '+' => переход в DEFAULT;
- OPERATOR. Получен '-' => переход в DEFAULT;
- OPERATOR. Получен '*' => переход в DEFAULT;
- OPERATOR. Получен '/' => переход в DEFAULT;
- BRACKET. Получен '(' => переход в DEFAULT;
- BRACKET. Получен ')' => переход в DEFAULT;
В процессе перехода из состояния в состояние лексический анализатор иногда делает откат в чтении на символ назад - position -= 1 - для того, чтобы передать символ повторно на анализ в другом состоянии после перехода.
Также в процессе создаются лексемы - createToken(Token.BRACKET, false) - что и является конечной целью создания лексического анализатора.
На этом, я завершаю статью про лексический анализатор. Исходный код приведенных выше классов доступен на GitHub. Там же доступен исходный код модульных тестов нашего лексического анализатора, ибо - код без тестов не код, а просто нагромождение символов(((
В следующей части мы перейдем к реализации синтаксического анализатора и построению абстрактного синтаксического дерева. Всем добра))
Комментариев нет:
Отправить комментарий