Этот раздел содержит рекомендации по подготовке эффективных, легко изменяемых и читаемых спецификаций. Следующие подразделы более или менее независимы.

Стиль записи

Довольно трудно добиться правильных и одновременно хорошо читаемых спецификаций. Поэтому постарайтесь соблюдать приводимые ниже соглашения:

  1. Для имен лексем используйте прописные буквы, для имен нетерминалов - строчные. Это правило поможет вам быстро найти виновника ошибки.
  2. Действия и правила размещайте на отдельных строках. Это позволит вам менять их независимо.
  3. Располагайте правила с одинаковой левой частью вместе. Левую часть записывайте только один раз, а правые части отделяйте вертикальной чертой.
  4. Точку с запятой помещайте на отдельной строке и только после последнего правила с заданной левой частью. Это позволит легко добавлять новые правила.
  5. Для тела правил делайте отступ в 2 табуляции, для тела действий - в 3.

Приведенные в этом разделе примеры следуют этому стилю (там, где позволяет пространство). У пользователя могут быть свои идеи по поводу стиля оформления, однако, в любом случае, нужно стремиться к тому, чтобы отделить правила от действий.

Левая рекурсия

Алгоритм, применяемый распознавателем, стимулирует употребление так называемой леворекурсивной формы:

name: name rest_of_rule

Эти правила часто употребляются при обработке последовательностей и списков:

list: item
            | list ',' item
            ;

и
seq: item
            | seq item
            ;

В каждом из этих правил первое правило сворачивается только для первого элемента, второе применяется ко второму и прочим элементам. В случае правой рекурсии

seq: item
           | item seq
           ;

распознаватель будет больше по размеру и свертка будет производиться справа налево. Что более серьезно, существует опасность переполнения внутреннего стека распознавателя при чтении слишком длинных входных последовательностей. Поэтому при возможности следует использовать левую рекурсию. Имеет смысл рассмотреть последовательность нулевой длины: если она допустима, запишите пустое правило:

seq:       /* empty */
           | seq item
           ;

Как и ранее, первое правило сворачивается только один раз перед чтением первого элемента, второе правило сворачивается для каждого прочитанного элемента. Допущение пустых последовательностей обычно увеличивает универсальность. Однако, могут возникнуть конфликты при попытке определить, какого типа читаемая пустая последовательность!

Взаимодействие с лексическим анализатором

Выполнение некоторых лексических действия зависит от контекста. Например, обычно лексический анализатор должен удалять пробелы, но не делать этого в случаях строк, заключенных в кавычки. Имена должны заноситься в таблицу символов, если они встречаются в определениях, но не в выражениях. Одним из способов решения данной задачи служит использование глобального флага, читаемого лексическим анализатором и устанавливаемого действиями. Предположим, например, что программа состоит из 0 или более определений, за которыми следует 0 или более операторов.

%{
int dflag
%}
%%
prog: decls stats
             ;
decls: /* пусто */
             { dflag=1; }
             | определения
             ;
stats: /* пусто */
             { dflag=0; }
             | операторы
             ;

Флаг dflag равен 0 при чтении операторов и 1 при чтении определений, за исключением первой лексемы первого оператора. Распознаватель должен распознать эту лексему перед тем, как сообщить, что кончился раздел определений и начался раздел операторов. В большинстве случаев это единственное исключение не влияет на процесс лексического анализа. Используя такой подход, можно, конечно и переусердствовать. Тем не менее, это иллюстрирует достижение определенных целей, которые трудно достижимы другими способами.

Обработка зарезервированных слов

Некоторые языки программирования допускают использование таких слов, как if в качестве меток или имен переменных, если это не противоречит другим именам в языке. В контексте yacc очень трудно обрабатывать подобные ситуации: лексический анализатор должен получать информацию такого рода - "это вхождение if переменная, а это - ключевое слово". Можно попытаться написать такие программы, но это довольно сложно. Лучше считать, что ключевые слова зарезервированы, и их нельзя использовать для имен переменных.

Эмуляция ошибок и конца ввода

Действия по обработке ошибок и концу ввода могут быть смоделированы в действиях с помощью макросов YYACCEPT и YYERROR. Первый заставлет функцию yyparse() вернуть значение 0, второй заставляет распознаватель вести себя так, как если бы он обнаружил ошибку: вызывается yyerror() и производится восстановление после ошибки. Эти механизмы могут использоваться для моделирования распознавателей с несколькими конечными маркерами и контекстно-чувствительным синтаксисом.

Доступ к значениям охватывающих правил

Действию может понадобиться доступ к значениям, возвращаемым действиями, находящимися слева от текущего правила. Здесь используется такой же механизм, как и в случае обычных действий, знак $ и число, но в этом случае число может быть меньше или равно нулю. Например:

sent: adj noun verb adj noun
               { анализ предложения } ;
adj: THE {$$=THE;}
               |YOUNG {$$=YOUNG;}
             ...
               ;
    noun: DOG {$$=DOG;}
               |CRONE {if($0==YOUNG){
                   printf("what\n");
                  }
                  $$=CRONE;
               }
               ;
             ...

В действии, идущем после слова CRONE, проверяется, равна ли предыдущая лексема слову YOUNG. Очевидно, что это возможно только в том случае, когда точно известно, что может предшествовать слову noun во входном потоке. К тому же, подход явно неструктурный. Тем не менее, иногда он может сэкономить немало усилий, в особенности, когда нужно сделать несколько исключений из регулярной структуры.

Значения произвольных типов

По умолчанию, значения, возвращаемые действиями и лексическим анализатором, считаются целыми. Yacc также поддерживает значения и других типов, в том числе структурных. В дополнение к этому, yacc следит за значениями типов и подставляет при необходимости соответствующие имена полей объединений, что приводит к получению распознавателей, в которых соблюдается контроль типов. Стек в yacc объявлен как объединение, допускающее значения различных типов. Объединение определяется пользователем и каждое его поле связывается с лексемой или нетерминалом. При обращении к значению при помощи конструкций $$ или $n yacc автоматически вставляет соответствующее имя объединения, чтобы избежать неверных конструкций приведения. Это также снижает число диагностических сообщений верификатора программ.
Для обеспечения подобной типизации существуют три механизма. Во-первых, задание объединения, которое должно выполняться пользователем, так как ряд программ, в особенности лексический анализатор, должны знать имена полей. Во-вторых, существует способ связи имен полей объединения с и лексемами и нетерминалами. И наконец, есть механизм описания типов того небольшого количества значений, для которых yacc не может определить тип. Объединение определяется в разделе объявлений:

%union        {
                тело объединения
              }

Это приводит к тому, что стек значений и внешние переменные yyval и yylval будут иметь тип этого объединения. Если yacc запущен с флагом -d, определение объединения копируется в файл y.tab.h. В другом варианте объединение может определяться в макрофайле, а для ссылок используется переменная YYSTYPE, задаваемая оператором typedef. В макрофайле должны помещаться следующие строки:

typedef union   {
                  тело определения 
                } YYSTYPE

Макрофайл включается в раздел объявлений с помощью конструкции %{ %}. Если YYSTYPE определено, все имена полей объединения должны быть связаны с именами терминальных и нетерминальных символов. Для идентификации поля объединения используется конструкция

< name >

Если она помещается непосредственно после директив %left, %right, %token или %noassoc, имя поля связывается с именем описанной лексемы. Таким образом, строка

%left <optype> '+' '-'

помечает любую ссылку на значения, возвращаемые этими лексемами, именем поля объединения optype. Для связи имен полей и нетерминалов также применяется директива %type. Поэтому можно написать следующую строку:

%type <nodetype> expr stat

Существует ряд ситуаций, в которых этого механизма недостаточно. Если правило содержит внутри себя действие, тип возвращаемого этим действием значения заранее не определен. Аналогично, обращение к левому контексту затрудняет определение типа. В этом случае ссылке можно присвоить некоторый тип путем указания имени поля объединения между символами < и > сразу после первого символа $. Пример:

rule: aaa {$<intval>$=3;}bbb {fun($<intval>2,$<other>0);}
;

Вряд ли можно пропагандировать эту конструкцию, но к счастью, она нужна двольно редко.
В следующем разделе приводится пример спецификации. Описанные в этом подразделе средства не активны пока к ним не было обращений. Этот механизм, в частности, включается конструкциями %type. При их применении можно достигнуть достаточно серьезного уровня контроля типов. Например, отмечаются диагностическими сообщениями все обращения посредством $ к объектам неопределенного типа. Если эти средства не используются, стек значений содержит значения целого типа, как это было в первых версиях программы.