Не все синтаксически корректные программы являются семантически корректными. Например:
*p
не определено*p >> 4
не определено, даже если определено *p
p->im
также не определеноопределяет статические свойства языка, выходящие за рамки синтаксиса. Например, что все идентификаторы должны быть определены перед использованием, что вызов функции должен принимать столько же аргументов, сколько указано в её определении и т.п.
определяет стратегию выполнения программы: каким образом исполняются инструкции, порядок их исполнения, значение управляющих структур и т.д.
определяет каким образом ЯП классифицирует значения и выражения, как они взаимодействуют и каким образом ЯП может манипулировать ими.
Динамическая семантика может определяться различными способами. Наиболее распространённые:
Альтернативный подход – интерпретация, т.е. непосредственное выполнение операций, указанных в исходном тексте программы.
Могут потребоваться другие программы и компоненты.
Обычно выделяют две фазы:
На этой фазе также возможен статический анализ исходного текста.
строится представление исходной программы в целевом коде.
На этой фазе так же возможны преобразования целевого кода, называемые оптимизациями.
Кроме того, между анализом и синтезом может находиться фаза преобразований промежуточного кода, называемая машинно-независимой оптимизацией.
Основных причин для разделения фаз лексического и снитаксического анализа несколько. Из них основные:
Важно понимать и разделять следующие понятия
Несколько определений из теории языков.
Термины “предложение” и “слово” часто используются как синонимы слова “строка”.
Длина строки \(s\) обозначается как \(|s|\), и равна количеству символов в строке. Пустая строка (содержащая 0 символов) обозначается \(\varepsilon\).
Если интерпретировать конкатенацию как “умножение”, то можно определить операцию “возведения в степень” – операцию повтора.
Другие теоретико-множественные операции, такие, как объединение \(\cup\) и пересечение \(\cap\) получаются непосредственно из определения языка как множества строк.
Таким образом, мы можем описывать языки композиционно, т.е. определив несколько простых конечных языков, на их основе определить возможно бесконечное множество других, более сложных языков.
Индуктивное определение регулярных выражений над алфавитом \(\Sigma\):
Если \(/r/\) и \(/s/\) – регулярные выражения, то:
Чтобы избежать избытка скобок при записи регулярных выражений, принимаются соглашения:
Прямо следуют из определений и свойств соответствующих операций над языками
Конечные автоматы разделяются на два класса:
НКА и ДКА являются равномощными, т.е. способны распознавать одинаковые множества языков.
НКА состоит из:
Индуктивный алгоритм МакНотона-Ямады-Томпсона
Для регулярного выражения \(/\varepsilon/\) строим НКА
\(i\) – новое состояние, \(f\) – новое принимающее состояние
Для регулярного выражения \(/a/,\) где \(a\in\Sigma\), строим НКА
\(i\) – новое состояние, \(f\) – новое принимающее состояние
Пусть \(N(s)\) и \(N(t)\) – НКА для регулярных выражений \(s\) и \(t\) соответственно. Тогда:
Для \(r = /s|t/\), строим НКА
где \(i\) – новое состояние, \(f\) – новое принимающее состояние, переходы из \(i\) осуществляются к начальному состоянию \(N(s)\) и \(N(t)\), а переходы к \(f\) происходят из принимающих состояний \(N(s)\) и \(N(t)\).
Для \(r = /st/\), строим НКА
где \(i\) – новое состояние, \(f\) –новое принимающее состояние. На самом деле ε-переходы можно опустить, отождествив \(i\) с начальным состоянием \(N(s)\), принимающие состояния \(N(s)\) с начальным состоянием \(N(t)\) и принимающие состояния \(N(t)\) с \(f\).
Для \(r=/s^*/\), строим НКА
где \(i\) – новое состояние, \(f\) –новое принимающее состояние.
Для \(r=/(s)/\), просто используем \(N(r)=N(s)\).
Определим некоторые вспомогательные функции:
Алгоритм моделирования НКА:
S = ε⁺(s₀);
c = nextChar();
while (c != eof) {
S = ε⁺(move(S, c));
c = nextChar();
}
if (S ∩ F != ∅) accept();
else reject();
Здесь nextChar()
читает следующий символ из входного потока, а eof
означает конец входного потока.
Чтобы выделить множество лексем:
if
может соответствовать токену ключевого слова и токену идентификатора, однако лексический анализатор, видимо, должен вернуть токен ключевого слова.Частный случай НКА, в котором:
s = s₀;
c = nextChar();
while (c != eof) {
s = move(s, c);
c = nextChar();
}
if (s ∈ F) accept();
else reject();
Здесь \(move : (S, Σ) → S\) – функция, возвращающая для текущего состояния \(s \in S\) и символа \(c \in \Sigma\) следующее состояние \(s \in S\).
Dstates = [ε⁺(s₀)]
while(в Dstates есть непомеченное состояние T) {
Пометить T;
for (a ∈ Σ) {
U = ε⁺(move(T, a));
if (U ∉ Dstates) {
Добавить U в Dstates как непомеченное;
}
Dtran[T, a] = U;
}
}
Алгоритм строит множество состояний ДКА Dstates
и таблицу переходов Dtran
.
Автомат | Построение | Работа |
---|---|---|
НКА | \(O(|r|)\) | \(O(|r|\cdot|x|)\) |
ДКА (типичный) | \(O(|r|^3)\) | \(O(|x|)\) |
ДКА (худший) | \(O(|r|^2 2^{|r|})\) | \(O(|x|)\) |