Я собираюсь предположить, что независимо от того, какой подход вы выберете, ваш синтаксический анализатор будет строить какое-то абстрактное синтаксическое дерево. Теперь у вас есть два варианта. Либо парсер может заполнить дерево узлами-идентификаторами, которые хранят имя переменной или функции, на которую они ссылаются. Это оставляет вопрос о разрешении области действия более поздним, как это описано во многих учебниках для компиляторов.
Другой вариант заключается в том, чтобы синтаксический анализатор сразу же просматривал идентификатор в таблице символов, который он строит, когда он идет, и вместо этого сохраняйте указатель на символ в узле дерева абстрактных синтаксисов. Этот подход имеет тенденцию хорошо работать, если ваш язык не позволяет имплицировать прямые ссылки на имена, которые еще не были объявлены.
Недавно я реализовал последний подход в компиляторе, над которым я работаю, и до сих пор я очень доволен результатом. Я кратко опишу свое решение ниже.
Символы хранятся в структуре, которая выглядит примерно так:
typedef struct symbol {
char *name;
Type *type;
Scope *scope; // Points to the scope in which the symbol was defined.
} Symbol;
Так что же это Scope
вещь? Язык, который я компилирую, лексически ограничен, и каждое определение функции, блок и т. Д. Представляет новую область. Области образуют стек, где нижний элемент является глобальной областью. Вот структура:
typedef struct scope {
struct scope *parent;
Symbol *buckets;
size_t nbuckets;
} Scope;
В buckets
и nbuckets
полей является хэш-картой идентификаторов (строки) в Symbol
указателей. Следуя указателям parent
, можно найти стек области при поиске идентификатора.
С помощью структур данных на месте легко написать синтаксический анализатор, который разрешает имена в соответствии с правилами лексического охвата.
- После столкновения с заявлением или декларацией, которая вводит новую сферу (например, объявление функции или оператора блока), анализатор помещает новый
Scope
в стек. Поле parent
новой области указывает на старый масштаб.
- Когда анализатор видит идентификатор, он пытается найти его в текущей области. Если сбой поиска в текущей области, он продолжается рекурсивно в области
parent
и т. Д. Если не найдено соответствующего Symbol
, возникает ошибка. Если поиск выполнен успешно, парсер создает узел AST с указателем на символ.
- И наконец, когда встречается объявление переменной или функции, оно связано в текущей области.
Некоторые языки используют несколько пространств имен. Например, в Erlang функции и переменные занимают разные пространства имен, требуя неловкого синтаксиса, например fun foo:bar/1
, чтобы получить значение функции. Это легко реализовать в модели I, описанной выше, путем хранения нескольких стеков Scope
- по одному для каждого пространства имен.
Это не работа парсера. Парсер строит дерево синтаксиса. Другие этапы подтверждают правильность семантики. –
Похоже, вам нужен обзор первого исследования терминов [The Dragon Book.] (Http://en.wikipedia.org/wiki/Compilers:_Principles,_Techniques,_and_Tools). Лексеры строят токены с входа, Parsers проверяют синтаксис токенов-потоков против языковой грамматики. * Ни * не предназначены для задачи, которую вы описываете. – WhozCraig
@ n.m. Круто, так что парсер просто строит его, даже если что-то там не так? –