Каждому правилу можно поставить в соответствие некое действие, которое будет выполняться всякий раз, как это правило будет распознано. Действия могут возвращать значения и могут пользоваться значениями, возвращенными предыдущими действиями. Более того, лексический анализатор может возвращать значения для токенов (дополнительно), если хочется. Действие - это обычный оператор языка Си, который может выполнять ввод, вывод, вызывать подпрограммы и изменять глобальные переменные.
Действия, состоящие из нескольких операторов, необходимо заключать в фигурные скобки. Например:
A : '(' B ')'
{ hello( 1, "abc" ); }
и
XXX : YYY ZZZ
{ printf("a message\n"); flag = 25; }
являются грамматическими правилами с действиями.
Чтобы обеспечить связь действий с анализатором, используется спецсимвол
"доллар" ($
). Чтобы вернуть значение, действие обычно присваивает его
псевдопеременной $$
. Например, действие, которое
не делает ничего, но возвращает единицу:
{ $$ = 1; }
Чтобы получить значения, возвращенные предыдущими действиями и лексическим
анализатором, действие может использовать псевдопеременные $1, $2
и т. д., которые соответствуют значениям, возвращенным компонентами
правой части правила, считая слева направо. Таким образом, если правило
имеет вид:
A : B C D ;
то $2
соответствует значению, возвращенному нетерминалом C, a $3
- нетерминалом D. Более конкретный пример:
expr : '(' expr ')' ;
Значением, возвращаемым этим правилом, обычно является значение выражения
в скобках, что может быть записано так:
expr : '(' expr ')'
{ $$ = $2 ; }
По умолчанию, значением правила считается значение, возвращенное первым
элементом ($1
). Таким образом, если правило не имеет действия, Yacc
автоматически добаляет его в виде $$=$1;
, благодаря чему
для правила вида
A : B ;
обычно не требуется самому писать действие.
В вышеприведенных примерах все действия стояли в конце правил, но иногда желательно выполнить что-либо до того, как правило будет полностью разобрано. Для этого Yacc позволяет записывать действия не только в конце правила, но и в его середине. Значение такого действия доступно действиям, стоящим правее, через обычный механизм:
A : B
{ $$ = 1; }
C
{ x = $2; y = $3; }
;
В результате разбора иксу (x) присвоится значение 1, а игреку (y) - значение, возвращенное
нетерминалом C. Действие, стоящее в середине правила, считается за
его компоненту, поэтому x=$2
присваивает X-у значение, возвращенное
действием $$=1;
Для действий, находящихся в середине правил, Yacc создает новый нетерминал и новое правило с пустой правой частью и действие выполняется после разбора этого нового правила. На самом деле Yacc трактует последний пример как
NEW_ACT : /* empty */ /* НОВОЕ ПРАВИЛО */
{ $$ = 1; }
;
A : B NEW_ACT C
{ x = $2; y = $3; }
;
В большинстве приложений действия не выполняют ввода/вывода, а конструируют
и обрабатывают в памяти структуры данных, например дерево разбора.
Такие действия проще всего выполнять вызывая подпрограммы для создания
и модификации структур данных. Предположим, что существует
функция node, написанная так, что вызов node( L, n1, n2)
создает вершину
с меткой L, ветвями n1
и n2
и возвращает индекс свежесозданной вершины.
Тогда дерево разбора может строиться так:
expr : expr '+' expr
{ $$ = node( '+', $1, $3 ); }
Программист может также определить собственные переменные, доступные
действиям. Их объявление и определение может быть сделано в секции
объявлений, заключенное в символы %{
и %}
.
Такие объявления имеют глобальную область видимости, благодаря
чему доступны как действиям, так и лексическому анализатору. Например:
%{
int variable = 0;
%}
Такие строчки, помещенные в раздел объявлений, объявляют переменную
variable типа int
и делают ее доступной для всех
действий. Все имена внутренних переменных Yacca начинаются c двух
букв y
, поэтому не следует давать своим переменным
имена типа yymy
.