diff --git a/2 курс/Языки программирования/Формальные грамматики и теория автоматов.md b/2 курс/Языки программирования/Формальные грамматики и теория автоматов.md new file mode 100644 index 0000000..3206617 --- /dev/null +++ b/2 курс/Языки программирования/Формальные грамматики и теория автоматов.md @@ -0,0 +1,281 @@ +## Содержание +- [Формальные грамматики](#Формальные%20грамматики) + - [Основы](#Основы) + - [Форма Бэкуса-Наура](#Форма%20Бэкуса-Наура) + - [Пример вывода строки](#Пример%20вывода%20строки) + - [Спецификация языка](#Спецификация%20языка) +- [Теория автоматов](#Теория%20автоматов) + - [История](#История) + - [Иерархия Хомского](#Иерархия%20Хомского) + - [Конечный автомат](#Конечный%20автомат) + - [ДКА и НКА](#ДКА%20и%20НКА) + - [Регулярные выражения и автоматы](#Регулярные%20выражения%20и%20автоматы) + - [Построение Томпсона](#Построение%20Томпсона) + - [Минимизация](#Минимизация) + - [Границы конечных автоматов](#Границы%20конечных%20автоматов) + - [Формы представления конечного автомата](#Формы%20представления%20конечного%20автомата) + - [Примеры конечных автоматов](#Примеры%20конечных%20автоматов) + - [Автомат для идентификаторов](#Автомат%20для%20идентификаторов) + - [Автомат для комментариев](#Автомат%20для%20комментариев) + - [Автомат для регулярного выражения](#Автомат%20для%20регулярного%20выражения) + - [Автомат для четного числа символов "a"](#Автомат%20для%20четного%20числа%20символов%20"a") + +## Формальные грамматики + +>**Формальная грамматика** - точный, механический способ задать, какие строки принадлежат языку. + +Формальные грамматики состоят из четырех компонентов: терминалы, нетерминалы, правила вывода и стартовый символ. + +>**Терминалы** - конечные символы, которые реально появляются в тексте (к примеру, числа, арифметические символы, скобки). + +>**Нетерминалы** - промежуточные категории, которые раскрываются по правилам (к примеру, "выражение", "слагаемое"). + +>**Вывод строки** - это последовательное применение правил от стартового символа до терминалов. +### Основы + +Формальные грамматики состоят из нескольких уровней: синтаксиса, семантики и прагматики. + +**Синтаксис** отвечает за правильность написания самого текста программы. Примером синтаксиса может являться правильность расставленных скобок, постановка точки с запятой в конце инструкции и так далее. + +**Семантика** отвечает за значение записанной информации. Примером семантики может быть значение записи "a+b" (что именно должна вычислить данная запись) и тому подобное. + +**Прагматика** отвечает за то, как данные инструкции могут быть использованы на практике. Примерами прагматики являются стиль, идиомы, соглашения сообщества и так далее. + +Тем не менее, очень важно не путать уровни между собой (хотя это и не просто). К примеру, выражение $\frac{1}{0}$ синтаксически безупречно (форм записи оно не нарушает), но семантически возникает вопрос: а что данное выражение должно вычислить? В разных языках ответ на этот вопрос представлен по-разному: исключение, бесконечность, неопределенное поведение. +### Форма Бэкуса-Наура + +>**Форма Бэкуса-Наура** (БНФ) - стандартная нотация записи формальных грамматик. + +Данная форма была названа в честь [Джона Бэкуса](https://ru.wikipedia.org/wiki/Бэкус,_Джон) и [Питера Наура](https://ru.wikipedia.org/wiki/Наур,_Петер). Впервые данная форма была использована ими для описания [Algol 60](https://en.wikipedia.org/wiki/ALGOL_60?ysclid=mujsiofdar620274942). + +В основе синтаксиса грамматики лежат пара простых символов: `::=` (раскрывается в) и `|` (альтернатива, "или"). Нетерминалы в этой грамматике чаще всего берутся в угловые скобки или пишутся курсивом. Терминалы же берутся в кавычки. + +Вывод строки в этой грамматике происходит пошагово. На каждом шаге раскрывается по одному нетерминалу. Порядок раскрытия может быть любым - это не влияет на итоговый результат. + +При этом, если для строки не существует последовательности шагов, за которую ее можно было бы вывести, то она не принадлежит языку. Это и есть формальный, механический критерий "правильности" строки. +#### Пример вывода строки + +Рассмотрим грамматику, заданную следующими условиями. +``` + ::= "+" + | + + ::= "*" + | + + ::= "(" ")" + | + + ::= "0" | "1" | "2" | ... | "9" +``` + +С такой грамматикой выведем строку "2+3" и таким образом докажем, что она принадлежит языку. +``` + + -> + + -> + + -> + + -> 2 + + -> 2 + + -> 2 + 3 +``` + +#### Спецификация языка + +По своей сути, спецификация языка - это та же формальная грамматика. + +Рассмотрим фрагмент реальной спецификации языка (упрощенно, в духе C). +``` + ::= + "if" "(" ")" + | "if" "(" + "else" + + ::= + + | + | ";" + + ::= "{" * "}" +``` +Про звезду * в последней строке после `` поговорим [чуть позже](#Построение%20Томпсона). + +В примере видно, что `` ссылается сам на себя, используя для этого `` и ``. Такая рекурсивная вложенность в грамматике и делает синтаксис язык [контекстно-свободным, а не регулярным](#Иерархия%20Хомского). +## Теория автоматов +### История + +Изначально, теория автоматов родилась не из программирования, а из вопроса "что вообще значит вычислять?". Программирование появилось позже, как массовая практика, и унаследовало уже готовый аппарат. + +В самом начале, в 1943 году, [Уоррен Маккаллок](https://ru.wikipedia.org/wiki/Мак-Каллок,_Уоррен) и [Уолтер Питтс](https://ru.wikipedia.org/wiki/Питтс,_Уолтер) создали [формальную модель нейрона](https://ru.wikipedia.org/wiki/Искусственный_нейрон), которая по своей сути является переключательным автоматом. В 1950-ых годах [Стивен Клини](https://ru.wikipedia.org/wiki/Клини,_Стивен_Коул) вывел [связь регулярных выражений и конечных автоматов](#Регулярные%20выражения%20и%20автоматы). В 1956 году [Ноам Хомский](https://ru.wikipedia.org/wiki/Хомский,_Ноам) предложил [иерархию формальных грамматик](#Иерархия%20Хомского). В 1959 году [Михаэль Рабин](https://ru.wikipedia.org/wiki/Рабин,_Михаэль) и [Дана Скотт](https://ru.wikipedia.org/wiki/Скотт,_Дана) дали [строгое определение НКА](#ДКА%20и%20НКА), а также доказали его эквивалентность с ДКА. +### Иерархия Хомского + +В своей теории Хомский делит грамматику языка на четыре типа - от 3 до 0. Чем ниже номер типа, тем мощнее грамматика и тем сложнее устройство, нужное для распознавания. + +Тип 3 - **регулярные** грамматики. Примерами языка с такой грамматикой будут являться различные идентификаторы или числа. Для распознавания такой грамматики в основном используется [конечный автомат](#Конечный%20автомат). + +Тип 2 - **контекстно-свободные** грамматики. Примером языка с такой грамматикой может быть синтаксис любого языка программирования. Для распознавания такой грамматики используется [автомат с магазинной памятью](https://ru.wikipedia.org/wiki/Автомат_с_магазинной_памятью). + +Тип 1 - **контекстно-зависимые** грамматики. Примером языка с такой грамматикой может быть какой-либо согласование типов или размерностей. Для распознавания такой грамматики используется [линейно ограниченный автомат](https://en.wikipedia.org/wiki/Linear_bounded_automaton). + +Тип 0 - **рекурсивно перечислимые** грамматики. Примером языка с такой грамматикой может быть любой алгоритмически заданный язык. Для распознавания такой грамматики используется [машина Тьюринга](https://ru.wikipedia.org/wiki/Машина_Тьюринга). +### Конечный автомат + +>**Конечный автомат** - пятерка ($Q, \Sigma, \delta, q_0, F$), где $Q$ - конечное множество состояний, $\Sigma$ - алфавит (то есть множество допустимых входных символов), $\delta$ - функция переходов из состояния и символа в новое состояние, $q_0$ - начальное состояние, $F$ - множество принимающих (или финальных) состояний. + +Строка принимается автоматом, если, начав в $q_0$ и применяя $\delta$ символ за символом, автомат оказывается в одном из состояний $F$. + +Такие автоматы можно встретить в лексическом анализе (разбивает текст программы на токены), сетевых протоколах (разбирает TCP-состояние, парсит [HTTP-заголовки](../Веб-программирование/Общие%20стандарты%20и%20протоколы%20сети%20Интернет.md#Заголовки%20HTTP)), играх (поведение NPC), [регулярных выражениях](#Регулярные%20выражения%20и%20автоматы) и схемотехнике (сам по себе конечный автомат является стандартной моделью цифровых схем управления). +#### ДКА и НКА + +>**Детерминированный конечный автомат** (ДКА) - конечный автомат, в котором существует ровно один возможный переход из определенного состояния по данному символу. + +>**Недетерминированный конечный автомат** (НКА) - конечный автомат, в котором существует ноль, один или несколько возможных переходов из определенного состояния по данному символу, а также возможный $\varepsilon$-переходы (то есть переходы без чтения символа). + +Выразительная сила как у ДКА, так и у НКА одинакова. То есть, любой НКА можно преобразовать в эквивалентный ДКА. + +Тем не менее, НКА иногда естественнее описывает поставленную задачу, но ДКА проще моделировать словами. Чаще всего при переходе из НКА в ДКА теряется удобство описания. +#### Регулярные выражения и автоматы + +>**Теорема Клини** - регулярные выражения и конечные автоматы имеют одинаковую выразительную силу. + +Иными словами, любому регулярному выражению можно сопоставить эквивалентный конечный автомат, и наоборот - любому конечному автомату можно сопоставить регулярное выражение. Регулярное выражение - это просто более компактная и понятная для человека форма записи того же языка. +##### Построение Томпсона + +Пусть у нас есть регулярное выражение. По теореме Клини, конечный автомат, эквивалентный данному выражению, существует. Но как его получить? + +>**Построение Томпсона** - алгоритм для получения конечного автомата из регулярного выражения, согласно которому необходимо сначала разобрать регулярное выражение на элементарные части, а после собрать снизу вверх. + +Всего таких элементарных частей три. +- Конкатенация (ab) - соединить автомат для a с автоматом для b по $\varepsilon$-переходу +- Альтернатива (a|b) - новое начальное состояние с двумя $\varepsilon$-переходами к автоматам a и b +- Звезда Клини (a*) - $\varepsilon$-переход, позволяющий пройти через автомат для a ноль, один или более раз. + +Именно по такому алгоритму работает большинство движков для регулярных выражений в стандартных библиотеках языков программирования. +#### Минимизация + +[Построение Томпсона](#Построение%20Томпсона) очень хорошо работает для построения автомата, но обычно дает автомат с избыточным числом состояний. Именно поэтому существуют методы минимизации полученного конечного автомата. + +>**Теорема Майхилла-Нероуда** - для каждого регулярного языка существует единственный минимальный ДКА. + +Данную теорему придумали и доказали [Джон Майхилл](https://en.wikipedia.org/wiki/John_Myhill) и [Энил Нероуд](https://en.wikipedia.org/wiki/Anil_Nerode) в 1958 году. + +По сути это означает, что для минимизации конечного автомата можно объединить два состояния, если они ведут себя одинаково для любого продолжения строки. На практике это означает, что будет меньше состояний, а значит на регистр или переменную будет уходить меньше памяти, а значит переход будет быстрее. + +Современные компиляторные генераторы лексеров (flex/lex) минимизируют автомат автоматически, прежде чем сгенерировать код. +#### Границы конечных автоматов + +Конечно, не все языки можно выразить через конечные автоматы. + +Классическим примером этого является язык сбалансированных скобок. Для того, чтобы конечный автомат смог его разобрать, ему необходима неограниченная память (ведь он должен помнить количество скобок, которые еще не закрылись). + +Формально такую ограниченность конечных автоматов можно доказать через [лемму о накачке](https://en.wikipedia.org/wiki/Pumping_lemma_for_regular_languages). + +Почти в любом языке программирования синтаксис представляет собой [контекстно-свободный, а не регулярный](#Иерархия%20Хомского) язык. Поэтому лексер (конечный автомат) сам по себе не может разобрать структуру программы целиком, для этого ему нужен парсер с дополнительной памятью (стеком). +#### Формы представления конечного автомата + +Конечный автомат можно описать в виде пять разных форм. +- Словесное описание +- Регулярные выражения +- Диаграмма конечного автомата +- Формальная функция переходов $\delta$ +- Код +#### Примеры конечных автоматов +##### Автомат для идентификаторов + +В данном случае, идентификатор - буква, а затем произвольное количество букв или цифр. Необходимо проверить, является ли данный идентификатор частью языка. Тогда у нас есть два состояния: q0 и q1. + +q0 - это начальное состояние, когда ни одного символа еще не прочитано. Если следующим символом будет прочитана буква, то состояние переходит в q1, иначе строка не идентификатор. + +q1 - это принимающее состояние, когда прочитана хотя бы одна буква, и дальше можно продолжать как буквами, так и цифрами. Если следующим будет прочитана либо буква, либо цифра, то состояние переходит снова в q1, иначе строка не идентификатор. +##### Автомат для комментариев + +Пусть у лексера есть задача распознать комментарий вида /\* текст \*/. В идеале, этот автомат мог бы состоять из двух состояний, но тут есть ловушка: внутри может быть символ "\*", который не заканчивает комментарий. Поэтому в данном случае необходимо четыре состояния. + +Эти состояния можно задать следующим образом. +``` +q0: обычный текст + '/' -> q1 + иначе -> q0 + +q1: увидели '/', ждем '*' + '*' -> q2 + иначе -> q0 + +q2: внутри комментария + '*' -> q3 + иначе -> q2 + +q3: увидели '*' внутри, ждем '/' + '/' -> q0 (комментарий закрыт) + '*' -> q3 (еще одна звездочка) + иначе -> q2 (это была не концовка) +``` +##### Автомат для регулярного выражения + +Пусть нам необходимо прочитать только те строки, которые начинаются с TODO. Для этого используется регулярное выражение ^TODO. + +Тогда для реализации автомата нам понадобится четыре состояния, по одному на каждую букву слова TODO. При любом отклонении от шаблона строка сразу выбрасывается. + +Тогда можем представить такой автомат. +``` +$ grep '^TODO' notes.txt + +; автомат для ^TODO (упрощенно) +; q0 --T--> q1 --O--> q2 --D--> q3 --O--> q4 (accept) +; любой другой символ из q0...q3 -> reject + +q0: читаем 'T' -> q1, иначе reject +q1: читаем 'O' -> q2, иначе reject +q2: читаем 'D' -> q3, иначе reject +q3: читаем 'O' -> q4 (accept) +``` +##### Автомат для четного числа символов "a" + +В данном случае, язык - строки над алфавитом {a, b} (то есть строки, состоящие только из символов a и b), где количество символов a - четное. + +Тогда для того, чтобы описать этот автомат, можем использовать два состояния: q_even (начальное и принимающее) и q_odd. + +Символ a переключает четность, символ b четность не меняет. А значит, будет существовать всего четыре правила переходов. +``` +δ(q_even, a) = q_odd +δ(q_even, b) = q_even +δ(q_odd, a) = q_even +δ(q_odd, b) = q_odd +``` + +Тогда можем закодировать этот автомат на языке C. +``` +enum { Q_EVEN, Q_ODD } state = Q_EVEN; + +for (const char *p = input; *p; p++) { + if (*p == 'a') { + state = (state == Q_EVEN) ? Q_ODD : Q_EVEN; + } + // символ 'b' состояние не меняет +} + +int accepted = (state == Q_EVEN); +``` +При таком кодировании автомата, состояние - это переменная или регистр. В главном цикле читается каждый очередной символ и определяется текущее состояние. Далее выполняется switch или if по состоянию и реализация правил переходов напрямую. В конце проверяется нахождение итогового значения во множестве принимающих. + +Аналогичным образом можем закодировать этот автомат на ассемблере. +``` +; rdi - указатель на строку, rsi - длина +; al = 0 (Q_EVEN) / 1 (Q_ODD) +count_parity: + xor al, al ; state = Q_EVEN + xor rcx, rcx ; i = 0 +.loop: + cmp rcx, rsi + jge .done + mov dl, [rdi + rcx] + cmp dl, 'a' + jne .next ; символ b - state не меняем + xor al, 1 ; переключить четность +.next: + inc rcx + jmp .loop +.done: + ret ; al = итоговое состояние +``` +В данном случае регистр играет роль переменной state. Условный переход jne/je реализует переход по состоянию. Цикл по строке - это метка + условный переход назад. \ No newline at end of file diff --git a/2 курс/Языки программирования/Языки программирования.md b/2 курс/Языки программирования/Языки программирования.md index 5a22026..f0af519 100644 --- a/2 курс/Языки программирования/Языки программирования.md +++ b/2 курс/Языки программирования/Языки программирования.md @@ -1,9 +1,13 @@ ## Лекции - [Парадигмы и машинная модель вычислений (неполная)](Парадигмы%20и%20машинная%20модель%20вычислений.md) +- [Формальные грамматики и теория автоматов](Формальные%20грамматики%20и%20теория%20автоматов.md) - (и тд) ## Ссылки - [Гитлаб](https://gitlab.se.ifmo.ru/programming-languages/cse-programming-languages-fall-2026/main) ## Доп. материалы -- Книга - Engineering a Compiler - Keith D. Cooper \ No newline at end of file +- Книга - Engineering a Compiler - Keith D. Cooper +- Книга - Введение в теорию автоматов, языков и вычислений - Хопкрофт, Мотвани, Ульман +- Книга - Indroduction to the Theory of Computation - Sipser +- Учебник - Low-level programming - Igor Zhirkov \ No newline at end of file