Семантический анализ и таблица символов
Грамматика проверяет форму, но не смысл — за смысл отвечает семантический анализ.
Семантический анализ — фаза, проверяющая правила, которые нельзя выразить грамматикой: объявлены ли имена, совместимы ли типы, корректны ли вызовы.
Строка x + 1 синтаксически безупречна. Но если x нигде не объявлен, программа бессмысленна. Грамматика этого не ловит — она не знает, какие переменные существуют. Это работа семантического анализа.
Что проверяет семантика
- Объявлена ли переменная до использования.
- Не объявлена ли переменная дважды в одной области.
- Совместимы ли типы операндов (
число + строка— обычно ошибка). - Совпадает ли число аргументов при вызове функции.
Таблица символов
Таблица символов — структура, хранящая сведения об объявленных именах: их типы, области видимости, иногда адреса в памяти.
Простейшая таблица символов — это словарь «имя → информация». Когда встречается объявление, мы добавляем запись; когда использование — ищем имя в таблице.
class SymbolTable:
def __init__(self):
self.symbols = {}
def declare(self, name, type_):
if name in self.symbols:
raise NameError("Повторное объявление: " + name)
self.symbols[name] = type_
def lookup(self, name):
if name not in self.symbols:
raise NameError("Не объявлено: " + name)
return self.symbols[name]
table = SymbolTable()
table.declare("x", "int")
print("x имеет тип", table.lookup("x"))
try:
table.lookup("y")
except NameError as e:
print("Ошибка:", e)Вывод:
x имеет тип int Ошибка: Не объявлено: y
Области видимости
В реальных языках одно имя может означать разное внутри функции и снаружи. Поэтому таблицы символов организуют в стопку (стек) областей: при входе в блок добавляется новая таблица, при выходе — удаляется. Поиск идёт от внутренней области к внешним.
class Scopes:
def __init__(self):
self.stack = [{}] # глобальная область
def push(self): self.stack.append({})
def pop(self): self.stack.pop()
def declare(self, name, t): self.stack[-1][name] = t
def lookup(self, name):
for scope in reversed(self.stack):
if name in scope:
return scope[name]
raise NameError("Не найдено: " + name)
s = Scopes()
s.declare("g", "int") # глобальная
s.push()
s.declare("local", "str") # локальная
print(s.lookup("local")) # видна локальная
print(s.lookup("g")) # видна и внешняя
s.pop()Вывод:
str int
Как работает под капотом
Семантический анализатор обходит AST (тем самым visitor из прошлого раздела). Встречая объявление — пишет в таблицу символов. Встречая использование имени — ищет его и проверяет тип. Так на дерево «навешивается» смысловая информация, которой потом пользуется кодогенератор.
Частые ошибки
Первая — пытаться поймать «необъявленную переменную» грамматикой. Это невозможно: грамматика контекстно-свободна, а знание о множестве объявленных имён — контекстная информация. Только таблица символов решает задачу.
Вторая — игнорировать области видимости и держать все имена в одном словаре. Тогда локальные и глобальные переменные конфликтуют, и язык ведёт себя непредсказуемо.
Итог
- Семантика проверяет смысл: объявления, типы, корректность вызовов.
- Таблица символов хранит сведения об именах (тип, область видимости).
- Области видимости организуют в стек: поиск идёт изнутри наружу.
- Эти проверки невозможны грамматикой — нужна контекстная информация.