Ответ на вопрос
Задание (кратко)
- Постройте лексер и парсер для небольшого языка MiniLang. Реализуйте генерацию AST, позиционирование токенов и базовую обработку ошибок. Напишите набор тестов и короткую документацию по грамматике и API.
Язык (минимум)
- Типы: целые числа, булевы.
- Выражения: арифметика \(`+`, `-`, `*`, `/`), сравнения \(``, `==`, `!=`\), логика \(`&&`, `||`, `!`\).
- Операции присваивания и объявления: `let id = expr;`
- Управление потоком: `if (cond) stmt [else stmt]`, `while (cond) stmt`
- Блоки: `{ stmt* }`
- Функции: `fn id(params) { stmt* }` и вызовы `id(args)`
- Комментарии: `//` до конца строки, блочные `/* ... */`
- Лексические сущности: идентификаторы, числа (целые), строки (в кавычках), ключевые слова, операторы, символы и т.п.
Требования к лексеру
- Регулярные правила для токенов: идентификатор, число, строка, ключевое слово, операторы, разделители.
- Правило «longest match» и приоритет ключевого слова над идентификатором.
- Игнорирование пробелов и комментариев, но сохранение информации о позиции (line, column, byte/char offset) для каждого токена.
- Предоставить API: nextToken(), peek(n), позиция токена в AST-узлах.
Требования к парсеру
- Построить AST с типами узлов: Program, FunctionDecl, VarDecl, If, While, Block, Return, Expr (Binary, Unary, Call, Literal, Identifier).
- Поддержать приоритеты и ассоциативность операций (например `*` выше `+`).
- Обработать «dangling else» правильно (else связывать с ближайшим if).
- Предусмотреть способы восстановления после ошибки (panic mode или фразовое восстановление), выдавать понятные сообщения об ошибках с позициями.
- Допустимы реализации: рекурсивный спуск (с устранением левой рекурсии) или генератор LR/LL (укажите ограничения и почему выбран подход).
Пример минимальной грамматики (EBNF, ориентировочно)
program ::= { functionDecl | stmt }
functionDecl ::= "fn" identifier "(" [identifier { "," identifier }] ")" block
stmt ::= block | varDecl | ifStmt | whileStmt | returnStmt | exprStmt
varDecl ::= "let" identifier "=" expr ";"
ifStmt ::= "if" "(" expr ")" stmt [ "else" stmt ]
whileStmt ::= "while" "(" expr ")" stmt
returnStmt ::= "return" [expr] ";"
exprStmt ::= expr ";"
block ::= "{" { stmt } "}"
expr ::= assignment
assignment ::= logicalOr [ "=" assignment ]
logicalOr ::= logicalAnd { "||" logicalAnd }
logicalAnd ::= equality { "&&" equality }
equality ::= relational { ("==" | "!=") relational }
relational ::= additive { ("" | "=") additive }
additive ::= multiplicative { ("+" | "-") multiplicative }
multiplicative::= unary { ("*" | "/") unary }
unary ::= ("!" | "-") unary | primary
primary ::= literal | identifier | "(" expr ")" | call
call ::= identifier "(" [expr {"," expr}] ")"
Ожидаемые сложности и рекомендации
- Неоднозначность грамматики:
- «Dangling else»: решить на уровне грамматики (правило stmt так, чтобы else связывался с ближайшим if) или в парсере — приоритеты/ассоциативность.
- Проблемы с левой рекурсией в рекурсивном спуске: убрать левую рекурсию или использовать парсер-генератор.
- Проблемы токенизации:
- Ключевые слова vs идентификаторы: после распознавания идентификатора проверять список ключевых слов.
- Числа/точки/операторы: коль скоро язык расширится (например, `.` для доступа), нужна четкая приоритизация.
- Строки с управляющими последовательностями и корректное закрытие кавычек.
- Обработка ошибок:
- Ясные и локализованные сообщения с указанием позиции: \("line", "column"\) и фрагмента исходника.
- Восстановление: пропуск токенов до ближайшего синтаксического маркера (например `;`, `}`) — стандартный panic mode.
- Не пытаться «угадывать» слишком далеко — лучше выдавать сообщение и продолжить разбор оставшегося кода.
- Позиционирование:
- Хранить для токена: начальную позицию \((line, column, offset)\) и длину/конечную позицию.
- Переносить позиции в AST-узлы (часто узел хранит позицию первого/ключевого токена).
- Учесть символы табуляции и разные окончания строк при подсчёте column/offset.
- Производительность и память:
- Для учебного задания достаточно стримового лексера/парсера; при большом коде — избегать многократного копирования строк.
Тесты и проверка
- Набор позитивных тестов: корректные программы (функции, вложенные if/while, вызовы).
- Набор негативных тестов: синтаксические ошибки (пропущенная скобка, лишний/отсутствующий `;`), лексические ошибки (не закрытая строка).
- Проверять: токены (тип, лексема, позиция), парс-результат (AST-структура), сообщения об ошибках (позиция и текст).
Критерии оценки (сумма баллов \(100\))
- Лексер корректность и покрытие лексем: \(\,25\) баллов
- Правильные токены для всех тестовых кейсов, корректное позиционирование.
- Парсер и AST: \(\,35\) баллов
- Правильно строит AST для всех позитивных тестов, учитывает приоритеты и ассоциативность, решён dangling-else.
- Обработка ошибок: \(\,15\) баллов
- Понятные сообщения с позициями + базовое восстановление и продолжение разбора.
- Тесты и автоматизация: \(\,10\) баллов
- Набор позитив/негатив тестов, CI-скрипт или команда запуска тестов.
- Документация и код: \(\,15\) баллов
- Короткая документация по грамматике, API лексера/парсера, читаемый код, комментарии.
Дополнительные бонусы (до \(\,10\) баллов сверх основных)
- Поддержка строк с экранированием, чисел с плавающей точкой, многобайтовых символов (UTF-8).
- Использование генератора парсеров с пояснением выбора (LR/GLR/LL).
- Наглядные сообщения об ошибках с контекстом строки.
Короткая инструкция по сдаче
- Репозиторий с кодом, тестами и readme. Примеры запуска и тестов.
- В readme укажите, какие ошибки ваш парсер способен восстановить, и как токены/позиции экспортируются.
Если нужно, могу дать: конкретный набор регулярных выражений для токенов, пример кода лексера/парсера (рекурсивный спуск) или готовые тест-кейсы.
Еще