Лексер с заглядыванием вперёд
Реальные языки требуют от лексера смотреть не только на текущий символ, но и на следующий.
Заглядывание вперёд (lookahead) — приём, при котором лексер или парсер смотрит на ещё не обработанные символы, чтобы принять верное решение.
Наш базовый лексер обрабатывал одиночные символы. Но что, если оператор состоит из двух знаков, например == или <=? Увидев =, лексер не может сразу решить, это присваивание = или начало ==. Нужно заглянуть на следующий символ.
Многосимвольные операторы
Решение — посмотреть на пару символов. Если за = идёт ещё один =, это оператор сравнения, и мы съедаем оба символа. Иначе — одиночный оператор.
def tokenize(text):
tokens = []
i = 0
while i < len(text):
ch = text[i]
nxt = text[i + 1] if i + 1 < len(text) else ""
if ch == " ":
i += 1; continue
if ch == "=" and nxt == "=":
tokens.append(("EQ", "==")); i += 2; continue
if ch == "=":
tokens.append(("ASSIGN", "=")); i += 1; continue
if ch == "<" and nxt == "=":
tokens.append(("LE", "<=")); i += 2; continue
if ch == "<":
tokens.append(("LT", "<")); i += 1; continue
raise ValueError("Неизвестный символ: " + repr(ch))
return tokens
# на входе только операторы, чтобы показать заглядывание вперёд
for tok in tokenize("== = <= <"):
print(tok)Вывод:
('EQ', '==')
('ASSIGN', '=')
('LE', '<=')
('LT', '<')Главное правило при заглядывании: сначала проверяй более длинные варианты. Если бы мы сначала ловили одиночный =, мы бы никогда не дошли до ==. Порядок проверок имеет значение.
Числа с точкой
Похожая логика для дробных чисел. Цифры могут разделяться одной точкой. Лексер читает цифры, при встрече точки запоминает, что дробь уже началась, и читает дальше.
def read_number(text, i):
num = ""
seen_dot = False
while i < len(text) and (text[i].isdigit() or text[i] == "."):
if text[i] == ".":
if seen_dot:
break
seen_dot = True
num += text[i]
i += 1
value = float(num) if seen_dot else int(num)
return value, i
val, _ = read_number("3.14abc", 0)
print(val, type(val).__name__)Вывод:
3.14 float
Как работает под капотом
Заглядывание вперёд формально характеризует мощность лексера. Лексеры на регулярных языках обычно требуют конечного, заранее известного заглядывания (часто на один символ — так называемые LL(1)-сканеры). Если для распознавания токена нужно «бесконечное» заглядывание, это сигнал, что задача уже не лексическая, а синтаксическая, и решать её должен парсер.
Частые ошибки
Главная ловушка — неправильный порядок проверок: одиночный вариант стоит раньше двойного, и двойной никогда не срабатывает. Всегда сначала проверяйте более длинные лексемы.
Вторая — вторая точка в числе. 3.14.15 не является корректным числом, и лексер должен остановиться на второй точке, а не клеить всё в одну строку и падать в float().
Итог
- Lookahead позволяет распознавать многосимвольные операторы и числа.
- Длинные варианты проверяй раньше коротких, иначе они не сработают.
- Дробные числа требуют учёта одной точки и остановки на второй.
- Потребность в «бесконечном» заглядывании — признак, что задача синтаксическая, а не лексическая.