Лабораторная №4 — задание 4.1A #
Нужно добавить в спецификацию своего языка поддержку инструкций, ввода-вывода и переменных.
Порядок выполнения #
Содержание грамматик зависит от особенностей вашего языка — это описано далее. При разработке грамматики полезно проанализировать грамматики двух языков, примеры на которых вы готовили в предыдущих лабораторных.
- Создайте файл
docs/specification/top-level-grammar.md— там будет спецификация в формате Markdown с описанием грамматики программы на вашем языке программирования - Опишите в новом файле правила грамматики, выходящие за рамки выражений, например:
- Правило для основной программы (можно назвать program, file или module)
- Правила для инструкций (statements)
- Правила для объявлений переменных (variable declarations)
- Добавьте в файл описание семантических правил (например, запрет на повторное объявление переменных с тем же именем)
- Допишите в файле
docs/specification/expressions-grammar.mdправила для выражений, например:- Правило для доступа к переменным, как в выражении
x = y = 5
- Правило для доступа к переменным, как в выражении
- Проверьте, что теперь ваш язык поддерживает:
- Переменные и/или константы
- Инструкции, исполняемые последовательно
- Инструкции ввода-вывода
- Проверьте расширенную грамматику на отсутствие левой рекурсии и левой факторизации, чтобы не создавать проблем в реализации рекурсивного спуска
Общие требования к спецификациям #
- Все спецификации находятся в каталоге
docs/specification/основной ветки вашего репозитория - Все спецификации согласуются между собой — например, список ключевых слов в документе
lexical-structureсовпадает с набором ключевых слов, используемых в EBNF-грамматиках в остальных документах - Все спецификации имеют ясную структуру — например, правила EBNF-грамматик описываются в markdown только блоком кода с языком
ebnf, а не списками или иными способами
Требования к грамматике языка #
Документ docs/specification/top-level-grammar.md пишется с использованием возможностей markdown: заголовков, таблиц, списков:
- В начале документа обязательно должны быть один или несколько примеров кода
- В середине документа опишите кратко
- ключевые особенности языка
- семантические правила (например, запрет на повторное объявление переменных с тем же именем)
- В конце документа опишите грамматику в виде блока кода на языке EBNF — см. EBNF для описания грамматик
Далее описаны рекомендации по проектированию своего языка.
1. Структура программы #
Структура программы зависит от парадигмы и решений аналитика.
Возможные на данном этапе варианты:
- Программа состоит из объявления точки входа, внутри которой находятся инструкции
- пример: функция main в языках C / C++
- пример: класс Program с функцией main в Java
- Программа состоит из инструкций и объявлений
- пример:
PROGRAM Name; VAR ...; BEGIN ... END.в языке Pascal - из инструкций и объявлений функций состоят простые скрипты на Python или JavaScript
- пример:
- Программа состоит из выражений и объявлений
- характерно для функциональных языков программирования
- пример: язык Haskell
Язык Kaleidoscope следует функциональной парадигме — в нём программа состоит из выражений и объявлений функций/констант:
(* Программа находится в одном файле *)
program = top_level_statement, { top_level_statement } ;
(* Выражение либо объявление *)
top_level_statement = (
function_or_constant_definition
| extern_function_declaration
| operator_definition
| expression
), [ ";" ] ;
2. Область действия #
Scope переводится как «область действия» или «область видимости». Первый вариант перевода более точный, второй — более распространённый.
Символы — переменные, функции, константы — имеют ограниченную область действия. Принцип работы областей видимости выбирает проектировщик:
- Можно ограничить область видимости блоком кода
- примеры: C/C++
- Можно ограничить область видимости функцией
- примеры: Pascal, Python
- Существует глобальная область видимости — для переменных и констант, объявленных на верхнем уровне (top-level declarations).
Интересный факт: в языке JavaScript ключевые слова var и let задают разную область видимости и правила переопределения:
var x— переменная существует до конца функции и может быть повторно объявлена с тем же именем;let x— переменная существует до конца блока (до закрывающей фигурной скобки) и не может быть повторно объявлена с тем же именем.
3. Переменные и константы #
Переменные могут работать по-разному в зависимости от решений аналитика:
- Если переменные объявляются явно, то доступ к необъявленной переменной считается ошибкой
- повторное объявление переменной с тем же именем обычно тоже считается ошибкой
- Если переменные создаются присваиванием, то ошибкой считается использование переменной, которую не присваивали в данной области действия (scope)
- Аналитик может выбирать между переменными и константами:
- можно поддерживать только изменяемые (mutable) переменные
- можно поддерживать только неизменяемые (immutable) переменные
- можно поддерживать оба вида — с похожим либо различным синтаксисом
Примеры языков, где поддерживаются как изменяемые, так и неизменяемые переменные:
| Язык | Объявление неизменяемой переменной | Объявление изменяемой переменной |
|---|---|---|
| Rust | let x = init_expr; | let mut x = init_expr; |
| JavaScript | const x = init_expr; | let x = init_expr; |
| Kaleidoscope | def x init_expr | var x = init_expr in expr |
4. Ввод/вывод #
Аналитик выбирает способ поддержи ввода-вывода. Возможные варианты:
- Ввод-вывод с помощью встроенных функций, например:
x = readInt(); writeLine(x * x) - Ввод-вывод с помощью инструкций с ключевыми словами, например:
read x; print x * x; - Ввод-вывод с помощью особых операторов, например:
-> x; <- x * x; - Ввод-вывод с помощью встроенных объектов или модулей, например:
Console.Writeln(x * x);
5. Виды инструкций #
Аналитик выбирает допустимые виды инструкций. Возможные варианты для выбора:
- Любое выражение является инструкцией:
statement = expression | (* ... другие варианты правила *) ; - Присваивание не является выражением, но является инструкцией:
statement = assignment - Отдельные инструкции для чтения и печати, например:
print x + 10; - Отдельные инструкции объявления переменных и/или констант, например:
var x = y * 2;
В некоторых языках намерено запрещено правило «любое выражение является инструкцией», чтобы на уровне грамматики запретить ситуации, когда результат выражения никуда не сохраняется и не имеет побочных эффектов (что делает вычисление выражения бесполезным).
Тогда вместо statement = expression ; вводят более ограниченные правила:
statement = assignment_statement
| function_call_expression
| (* ... другие варианты правила *)
Такая грамматика разрешает использовать в качестве инструкции только выражения с побочными эффектами:
// допускается грамматикой:
var x = 10;
writeInt(x);
// не допускается грамматикой (не сохраняется результат, нет побочных эффектов):
x + 10;
6. Разделитель инструкций #
Эта лабораторная не требует реализации восстановления после ошибок.
Аналитик выбирает, нужны ли в языке разделители инструкций и какими они могут быть. Возможные варианты:
- Символы-разделители, такие как “;”
- Переносы строк как разделители
- Нет разделителей инструкций
Неочевидная задача разделителей — упрощать восстановление после ошибок разбора. Например, промышленный компилятор может:
- В случае появления ошибки разбора сообщать о ней и далее игнорировать все токены (лексемы) до разделителя (такого как точка с запятой)
- После получения разделителя обрабатывать файл дальше
- После завершения обработки файла всё равно сообщить об фатальной ошибке в духе
fatal errors: 10 compilation errors
Такая стратегия помогает программисту получить больше обратной связи. Если же восстановления после ошибок нет, то программист увидит только первую ошибку и будет вынужден циклично устранять проблемы по одной, пока ошибки не закончатся.