Добавлена лекция 2 по языкам программирования
This commit is contained in:
@@ -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).
|
||||
|
||||
В основе синтаксиса грамматики лежат пара простых символов: `::=` (раскрывается в) и `|` (альтернатива, "или"). Нетерминалы в этой грамматике чаще всего берутся в угловые скобки или пишутся курсивом. Терминалы же берутся в кавычки.
|
||||
|
||||
Вывод строки в этой грамматике происходит пошагово. На каждом шаге раскрывается по одному нетерминалу. Порядок раскрытия может быть любым - это не влияет на итоговый результат.
|
||||
|
||||
При этом, если для строки не существует последовательности шагов, за которую ее можно было бы вывести, то она не принадлежит языку. Это и есть формальный, механический критерий "правильности" строки.
|
||||
#### Пример вывода строки
|
||||
|
||||
Рассмотрим грамматику, заданную следующими условиями.
|
||||
```
|
||||
<expr> ::= <expr> "+" <term>
|
||||
| <term>
|
||||
|
||||
<term> ::= <term> "*" <factor>
|
||||
| <factor>
|
||||
|
||||
<factor> ::= "(" <expr> ")"
|
||||
| <number>
|
||||
|
||||
<number> ::= "0" | "1" | "2" | ... | "9"
|
||||
```
|
||||
|
||||
С такой грамматикой выведем строку "2+3" и таким образом докажем, что она принадлежит языку.
|
||||
```
|
||||
<expr>
|
||||
-> <expr> + <term>
|
||||
-> <term> + <term>
|
||||
-> <factor> + <term>
|
||||
-> 2 + <term>
|
||||
-> 2 + <factor>
|
||||
-> 2 + 3
|
||||
```
|
||||
|
||||
#### Спецификация языка
|
||||
|
||||
По своей сути, спецификация языка - это та же формальная грамматика.
|
||||
|
||||
Рассмотрим фрагмент реальной спецификации языка (упрощенно, в духе C).
|
||||
```
|
||||
<if-statement> ::=
|
||||
"if" "(" <expr> ")" <statement>
|
||||
| "if" "(" <expr ")" <statement>
|
||||
"else" <statement>
|
||||
|
||||
<statement> ::=
|
||||
<if-statement>
|
||||
| <block>
|
||||
| <expr> ";"
|
||||
|
||||
<block> ::= "{" <statement>* "}"
|
||||
```
|
||||
Про звезду * в последней строке после `<statement>` поговорим [чуть позже](#Построение%20Томпсона).
|
||||
|
||||
В примере видно, что `<statement>` ссылается сам на себя, используя для этого `<if-statement>` и `<block>`. Такая рекурсивная вложенность в грамматике и делает синтаксис язык [контекстно-свободным, а не регулярным](#Иерархия%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 реализует переход по состоянию. Цикл по строке - это метка + условный переход назад.
|
||||
@@ -1,5 +1,6 @@
|
||||
## Лекции
|
||||
- [Парадигмы и машинная модель вычислений (неполная)](Парадигмы%20и%20машинная%20модель%20вычислений.md)
|
||||
- [Формальные грамматики и теория автоматов](Формальные%20грамматики%20и%20теория%20автоматов.md)
|
||||
- (и тд)
|
||||
|
||||
## Ссылки
|
||||
@@ -7,3 +8,6 @@
|
||||
|
||||
## Доп. материалы
|
||||
- Книга - Engineering a Compiler - Keith D. Cooper
|
||||
- Книга - Введение в теорию автоматов, языков и вычислений - Хопкрофт, Мотвани, Ульман
|
||||
- Книга - Indroduction to the Theory of Computation - Sipser
|
||||
- Учебник - Low-level programming - Igor Zhirkov
|
||||
Reference in New Issue
Block a user