1. Введение в архитектуру компилятора и лексический анализ
Введение в архитектуру компилятора и лексический анализ
Добро пожаловать в курс «Разработка компиляторов: от теории к практике». Многие программисты воспринимают компилятор как «черный ящик»: вы подаете на вход исходный код, а на выходе получаете работающую программу. В этом курсе мы откроем крышку этого ящика и разберем, как именно текст превращается в инструкции процессора.
В первой статье мы рассмотрим общую архитектуру компилятора и детально изучим его первый этап — лексический анализ.
Что такое компилятор?
Компилятор — это программа, которая переводит текст, написанный на одном языке (исходный язык), в эквивалентный текст на другом языке (целевой язык). Чаще всего мы переводим языки высокого уровня (C++, Rust, Java) в машинный код или байт-код.
Важно отличать компиляторы от интерпретаторов. Компилятор переводит программу целиком до ее запуска, создавая исполняемый файл. Интерпретатор же читает и выполняет код построчно во время работы программы.
Архитектура компилятора: вид с высоты птичьего полета
Процесс компиляции слишком сложен, чтобы выполнять его за один проход. Поэтому современные компиляторы разделены на фазы. Эти фазы обычно группируются в две большие части:
!Последовательность фаз работы классического компилятора
Основные этапы Front-end'а:
* Лексический анализ (Сканирование): Разбивает текст на слова (токены). * Синтаксический анализ (Парсинг): Строит дерево структуры программы (AST). * Семантический анализ: Проверяет смысл (типы данных, области видимости).
Сегодня мы сосредоточимся на самом первом этапе.
Лексический анализ (Scanning)
Задача лексического анализатора (лексера) — прочитать поток символов исходного кода и сгруппировать их в осмысленные последовательности, называемые лексемами. Для каждой лексемы лексер создает объект — токен.
Представьте, что вы читаете предложение на иностранном языке. Прежде чем понять смысл, вы сначала выделяете отдельные слова и знаки препинания. Лексер делает то же самое.
Рассмотрим пример кода:
Для компьютера это просто массив символов: ['x', ' ', '=', ' ', '1', '0', ' ', '+', ' ', '2']. Лексер преобразует это в поток токенов:
IDENTIFIER (значение: "x")ASSIGN (значение: "=")NUMBER (значение: "10")PLUS (значение: "+")NUMBER (значение: "2")Обратите внимание: пробелы были проигнорированы, так как в данном контексте они не несут смысла.
Структура токена
Токен обычно представляет собой пару:
Где:
* — тип токена (например, число, идентификатор, ключевое слово if).
* — (опционально) конкретное значение лексемы (например, само число 42 или имя переменной counter).
Также часто сохраняют метаданные: номер строки и позицию в строке для вывода понятных сообщений об ошибках.
Теоретическая база: Регулярные выражения
Как объяснить компьютеру, что такое «число» или «идентификатор»? Для этого используется математический аппарат регулярных выражений.
Регулярные выражения описывают множества строк (языки). Например, идентификатор в C-подобных языках можно описать так: «буква или подчеркивание, за которыми следует ноль или более букв, цифр или подчеркиваний».
В математической нотации это выглядит так:
Где: * — регулярное выражение для идентификатора. * — множество букв (Letters) . * — множество цифр (Digits) . * — операция объединения (или). * — операция конкатенации (следование друг за другом). — замыкание Клини (повторение 0 или более раз).
Реализация: Конечные автоматы
Чтобы превратить регулярное выражение в работающий код, мы используем Конечные Автоматы (Finite Automata). Это абстрактная машина, которая может находиться в одном из конечного числа состояний.
Формально детерминированный конечный автомат (DFA) определяется как пятерка:
Где: * — конечное множество состояний. * — входной алфавит (символы, которые мы читаем). * — функция переходов , которая говорит: «если мы в состоянии и видим символ , переходим в состояние ». * — начальное состояние (). * — множество допускающих (финальных) состояний ().
!Граф переходов автомата, распознающего последовательность цифр
Как это работает на практике?
Лексер работает как «жадный» автомат. Он пытается считать максимально длинную последовательность символов, которая подходит под одно из правил.
Простой алгоритм работы лексера:
@ в языке C).Пример простого лексера на Python
Давайте напишем примитивный лексер, который понимает только числа и знаки плюс/минус.
Этот код реализует логику конечного автомата вручную, используя условия if и циклы while. В промышленных компиляторах часто используют генераторы лексеров (например, Lex или Flex), которые сами строят таблицы переходов на основе регулярных выражений.
Обработка ошибок
Лексический анализ — это первый рубеж обороны. Ошибки здесь бывают простыми: «недопустимый символ» или «незакрытая строковая константа».
Важно, чтобы компилятор не падал при первой ошибке, а пытался восстановиться, чтобы найти остальные ошибки в файле. В лексере это часто делается пропуском символов до тех пор, пока не встретится знакомое начало нового токена (например, пробел или точка с запятой).
Заключение
Мы разобрали первый кирпичик в фундаменте компилятора. Лексический анализ преобразует хаос символов в порядок токенов, упрощая жизнь следующим этапам.
В следующей статье мы перейдем к синтаксическому анализу: узнаем, как из плоского списка токенов построить иерархическое дерево, отражающее логику вашей программы.