- SLR-parser
- недоделанная часть с теорией
Проект использует систему сборки CMake (минимальная версия 3.14). Для работы требуется компилятор C++17, Flex и доступ к интернету (для первой загрузки библиотеки тестов Google Test).
- ОС: Linux, macOS или Windows (через WSL).
- Компилятор: GCC/G++ 9+ или Clang 10+ (с поддержкой C++17).
- Инструменты: cmake, make, flex.
- Зависимости:
- libfl (библиотека Flex).
- Google Test (загружается автоматически при конфигурации).
Выполните в терминале:
sudo apt-get update
sudo apt-get install build-essential cmake flex libfl-devСоздайте директорию сборки, сконфигурируйте проект и соберите его:
mkdir build
cd build
cmake ..
makeИсполняемый файл находится в папке build/src/SLR/. Парсер работает в интерактивном режиме (ввод завершается сочетанием клавиш Ctrl+D на Linux/Mac или Ctrl+Z на Windows).
./src/SLR/slr_parserПроект включает набор End-to-End тестов, проверяющих лексический анализ, синтаксический разбор, приоритет операций и обработку ошибок.
Для запуска всех тестов используйте утилиту ctest из папки build:
ctest --output-on-failure
##Запуск конкретного теста:
ctest -R ParenthesesAndPrecedence --output-on-failure
##Пример вывода успешных тестов:
Test project /path/to/SLR-parser/build
Start 1: ParserE2ETest.SimpleAddition
1/6 Test #1: ParserE2ETest.SimpleAddition ............... Passed 0.01 sec
Start 2: ParserE2ETest.MultiplicationPrecedence
2/6 Test #2: ParserE2ETest.MultiplicationPrecedence .... Passed 0.01 sec
...
100% tests passed, 0 tests failed out of 6Проект организован модульно, разделяя этапы лексического анализа, построения грамматик и самого парсинга.
SLR-parser/
├── CMakeLists.txt # Корневой файл сборки проекта
├── README.md # Документация проекта
│
├── scripts/ # 🛠 Скрипты генерации кода
│ └── generate_grammar.rb # Ruby-скрипт: парсит грамматику и генерирует C++ заголовки
│
├── data/ # 📊 Данные и конфигурация
│ ├── grammar.txt # Исходное описание правил грамматики (BNF)
│ └── slr_cache.json # Кэш таблиц ACTION/GOTO (генерируется автоматически)
│
├── src/ # 📂 Исходный код C++
│ ├── lexer/ # Лексический анализатор
│ │ ├── lexer.l # Спецификация токенов для Flex
│ │ ├── lexer_global.cpp# Глобальные переменные лексера (yylval)
│ │ ├── token_struct.hpp# Структура Token (тип, значение, позиция)
│ │ └── token_type.hpp # Enum TokenType (устаревший алиас на Symbol)
│ │
│ ├── grammar/ # Модуль работы с грамматикой
│ │ ├── first_follow.cpp# Алгоритмы вычисления множеств First и Follow
│ │ ├── items.cpp # Работа с позициями (LR(0)-items)
│ │ ├── symbol_utils.cpp# Утилиты для конвертации символов
│ │ └── generated/ # ⚠️ АВТОГЕНЕРИРУЕМЫЕ ФАЙЛЫ (не редактировать!)
│ │ ├── grammar_rules.cpp # Вектор правил продукции
│ │ └── symbol_enums.hpp # Enum всех терминалов и нетерминалов
│ │
│ └── SLR/ # Ядро синтаксического анализатора
│ ├── main.cpp # Точка входа: связка Lexer + Parser
│ ├── slr_parser.cpp # Реализация алгоритма разбора (Shift/Reduce)
│ ├── slr_parser_dump.cpp # Функции вывода состояния (трассировка)
│ ├── table_builder.cpp # Построитель таблиц ACTION и GOTO
│ └── slr_parser.hpp # Заголовок класса парсера
│
├── tests/ # 🧪 Набор тестов
│ ├── CMakeLists.txt # Конфигурация сборки тестов
│ └── parser_e2e_test.cpp # End-to-End тесты (Google Test)
│
└── build/ # 🏗 Артефакты сборки (игнорируется в git)
├── src/SLR/slr_parser # Исполняемый файл парсера
└── tests/parser_tests # Исполняемый файл тестов
Данный проект представляет собой реализацию синтаксического анализатора (парсера), работающего по алгоритму SLR(1) (Simple LR). Парсер предназначен для разбора арифметических выражений, проверки их синтаксической корректности и построения последовательности действий (сдвиг/свертка) на основе заданной контекстно-свободной грамматики.
Проект написан на C++17 и демонстрирует полный цикл работы компилятора: от лексического анализа до синтаксической верификации с поддержкой обработки ошибок и модульного тестирования.
(это набросок от нейронки)
Проект модульный и разделен на три основных компонента: Лексер, Генератор таблиц и Парсер.
Вот эта часть кода на Ruby не up2date!
Перед сборкой C++ кода выполняется этап подготовки данных грамматики.
- Инструмент: Скрипт на языке Ruby (
scripts/generate_grammar.rb). - Процесс: Скрипт читает файл с описанием правил грамматики и генерирует C++ заголовочные файлы в директорию
src/grammar/generated/. - Результат:
symbol_enums.hpp: Enum-ы для всех терминалов и нетерминалов.grammar_rules.cpp: Вектор структурRule, описывающих продукцию грамматики.- Эти файлы используются парсером для построения множеств позиций (items) и таблиц разбора.
⚠️ Важно: При изменении грамматики необходимо заново запустить Ruby-скрипт перед пересборкой проекта.
Модуль отвечает за разбиение исходной строки на токены.
- Реализация: Файл
lexer.lописывает регулярные выражения для токенов. - Генерация: Утилита Flex преобразует
.lфайл в C++ код (lexer.cpp). - Структура токена: Используется единая структура
Token, хранящая тип (Symbol), текстовое значение, числовое значение (для литералов) и позицию в исходном коде (строка/колонка).
Ядро системы, расположенное в src/SLR/.
- Построение таблиц: Класс
TableBuilderдинамически вычисляет множества First/Follow, строит каноническую систему множеств позиций (DFA) и заполняет таблицы ACTION и GOTO.- Таблицы кэшируются в JSON-файл (
data/slr_cache.json) для ускорения запуска.
- Таблицы кэшируются в JSON-файл (
- Алгоритм разбора:
- Инициализация стека состояний и стека символов.
- Цикл чтения токенов из лексера.
- Поиск действия в таблице по текущему состоянию и токену.
- Выполнение действия:
- Shift: Сдвиг токена и нового состояния в стек.
- Reduce: Свертка группы символов в нетерминал согласно правилу грамматики.
- Accept: Успешное завершение.
- Error: Выброс исключения с метаданными о позиции ошибки.
graph LR
A[Grammar File] -->|Ruby Script| B(Generated C++ Headers)
C[Source Code] -->|Flex| D(Lexer C++ Code)
B --> E[Parser Core]
D --> E
E --> F{Action Table}
F -->|Cache JSON| G[Runtime Execution]
G --> H[Parse Tree / Error Report]
Полученная таблица действий для парсера (см. построение парсера).
Ниже представлена таблица переходов для синтаксического анализатора.
- S = Shift (Сдвиг)
- R = Reduce (Свертка)
- A = Accept (Допуск)
- - = Ошибка (Error)
| State | PLUS | MINUS | STAR | SLASH | LPAREN | RPAREN | NUMBER | VAR | END_MARKER |
|---|---|---|---|---|---|---|---|---|---|
| 0 | - | - | - | - | S4 | - | S5 | S6 | - |
| 1 | S7 | S8 | - | - | - | - | - | - | A |
| 2 | R6 | R6 | R6 | R6 | - | R6 | - | - | R6 |
| 3 | R3 | R3 | S9 | S10 | - | R3 | - | - | R3 |
| 4 | - | - | - | - | S4 | - | S5 | S6 | - |
| 5 | R7 | R7 | R7 | R7 | - | R7 | - | - | R7 |
| 6 | R8 | R8 | R8 | R8 | - | R8 | - | - | R8 |
| 7 | - | - | - | - | S4 | - | S5 | S6 | - |
| 8 | - | - | - | - | S4 | - | S5 | S6 | - |
| 9 | - | - | - | - | S4 | - | S5 | S6 | - |
| 10 | - | - | - | - | S4 | - | S5 | S6 | - |
| 11 | S7 | S8 | - | - | - | S16 | - | - | - |
| 12 | R1 | R1 | S9 | S10 | - | R1 | - | - | R1 |
| 13 | R2 | R2 | S9 | S10 | - | R2 | - | - | R2 |
| 14 | R4 | R4 | R4 | R4 | - | R4 | - | - | R4 |
| 15 | R5 | R5 | R5 | R5 | - | R5 | - | - | R5 |
| 16 | R9 | R9 | R9 | R9 | - | R9 | - | - | R9 |
с помощью скрипта grammar_gen.rb из описания грамматики в grammar.txt генерируются в /grammar/generated:
- enum Symbol в symbol_enums.hpp
- вектор грамматических правил в grammar_rules.cpp
токены: по описанию в grammar/tokens_def.rb генерируются enum-ы токенов в C++.
Приходится в ручную поддерживать когерентность описаний Symbol, Token, и описанием лексики в lexer.l.
- генерацию грамматики проверить, и где вообще когерентность нужно поддерживать
- тесты добавить на парсер, на построение таблиц тест можно в целом тоже
- описание проекта бы более подробно сделать, с мини описанием мотивации решений архитектурных, языковых и т.д.
- рефакторинг примерно всего?
- описание бы адекватное сделать
- план 200%: сделать Panic mode и error recovery
LR(k) (Left-toRight scanning, Rightmost derivation in reverse, k-lookahead) - класс контекстно-свободных грамматик, для которых возможно построение детерминированного восходящего синтаксического анализатора.
Восходящий синтаксический анализатор принимает решения на основе состояния следующих элементов:
- Input (уже прочитанный вход, не более чем на k символов вперед)
- Stack (содержание стэка)
В данном проекте используется подмножество LR(1) - SLR(1) для учебных целей ввиду простоты реализации по сравнению с LR(1).
В данном методе разбора входная строка из терминалов постепенно "сворачивается" (операция "reduce") по подстрокам к нетерминалам, пока не получится начальный символ грамматики.
Для распознавания правых порождений в обратном порядке можно использовать конечный автомат, где
- Состояния DFA: множества пунктов (items) с замыканием
- Переходы: GOTO(I, X), где X - символ грамматики
Конечный автомат здесь моделирует все промежуточные точки в процессе распознавания правил грамматики.
Пункт(Item) - правило грамматики с точкой ( . ), показывающей, до какого места уже распознано грамматическое правило.
Т.е. эта конструкция введена как эквивалент текущего положения при парсинге.
Пример:
Для правила:
E -> E + TВозможные пункты:
E -> . E + T // ничего не распознано
E -> E . + T // распознано E
E -> E + . T // распознано E +
E -> E + T . // все распознано и готово к сверткеС помощью пунктов можно описывать состояния парсера, отсюда возникает модель конечного автомата (DFA - Deterministic Finite Automaton).
Множество всех состояний - это замыкание(closure) множества всех пунктов
Замыкание(closure) - операция над множеством пунктов, которая для пунктов вида
A -> a . B bДобавляет все пункты вида
B -> . yПрмиер:
Исходный пункт:
E -> . TПри наличии правил
T -> T * P
T -> Pclosure добавит:
T -> . T * P
T -> . P
P -> . ...Через замыкание добавляются все возможные сценарии последующих символов.
GOTO - функция перехода LR(0) автомата по нетерминалам.
GOTO(I, X) - это состояние автомата, в которое мы переходим из состояния I по символу X, где X - нетерминал.
Пример:
I = { // текущее состояние
E -> T .,
T -> T . * P
}
GOTO(I, *) = { T -> T * . P } // состояние после перехода по '*'
т.е. это как бы сдвиг на выбранный символ.
| Action | Описание |
|---|---|
| Shift | перемещение следующего символа строки на вершину стэка |
| Reduce | если вершина стэка совпадает с сентенциальной формой, заменяем порожденную форму на начальную. Т.е. если на вершине стэка a, совпадающее с правой частью правила A -> a, заменяем a на A на вершине стэка |
| Accept | строку удалось сматчить до начального символа грамматики |
| Error | ошибка, нет подходящего грамматического правила |
----------------- здесь про модель конечного автомата добавить
---- добавить
SLR-таблица строится на основе свех состояний LR(0)-автомата, где каждое состояние - это замыкание(closure) множества пунктов
Таблица ACTION[state, terminal] определяет, что делать, когда на вершине стека состояние state, а следующий входной символ — terminal. Возможные действия: shift, reduce, accept, error.
Таблица GOTO[state, nonterminal] определяет, в какое состояние перейти после того, как выполнен reduce. Это просто переход в новое состояние DFA (LR(0)-автомата) по символу-нетерминалу.
SLR парсер использует два стэка: стэк состояний и стэк символов
State stack: содержит одно состояние - начальное
- на входе - EOF('$')
- на вершине стэка состояний - состояние start
- в таблице ACTION[start]['$'] = accept