summaryrefslogtreecommitdiff
path: root/doc
diff options
context:
space:
mode:
authorkx <kx@radix-linux.su>2026-09-30 21:14:58 +0300
committerkx <kx@radix-linux.su>2026-09-30 21:14:58 +0300
commit5b1c65152f77e03a4800fceae32d16efe2dadc9c (patch)
treecaffe4b2235503cccfedfb772dc266cb311840b3 /doc
parent8b354d2b9f2640d90705abd324a413a0d8fa3867 (diff)
downloadzubr-trunk.tar.xz
Version 4.1.0HEAD4.1.0trunk
Diffstat (limited to 'doc')
-rw-r--r--doc/zubr.md2307
1 files changed, 2307 insertions, 0 deletions
diff --git a/doc/zubr.md b/doc/zubr.md
new file mode 100644
index 0000000..fcf844b
--- /dev/null
+++ b/doc/zubr.md
@@ -0,0 +1,2307 @@
+# ZUBR — руководство по генератору синтаксических анализаторов LALR(1)
+
+ - **Версия документации:** ZUBR 4.1.0
+ - **Формат входных и выходных текстов ZUBR:** UTF-8
+ - **Внутреннее текстовое представление ZUBR:** UCS-2 (`__mpu_char16_t`)
+ - **Библиотеки:** LibMPU / LibMPUIO
+
+Это руководство является современной редакцией исходной документации **ZUBR** \[[3](#ref-3)\]. Изложение идет от вводных понятий к устройству спецификации, semantic actions, lexer, работе LALR-автомата и `z.output`, конфликтам, приоритетам, восстановлению после ошибок и более мощным средствам.
+
+Главная цель документа — не только перечислить директивы, но и дать начинающему разработчику представление о том, **как lexer и parser взаимодействуют и как LALR(1)-анализатор принимает решения**.
+
+## Содержание
+
+- [Что такое ZUBR?](#what)
+- [1. Введение](#introduction)
+ - [1.1. Нормальная форма Бэкуса — Наура](#bnf)
+- [2. Файл входной спецификации ZUBR](#spec)
+- [3. Семантические процедуры](#actions)
+- [4. Лексический анализ](#lex)
+- [5. Операции разбора и файл `z.output`](#parse)
+- [6. Неоднозначности и конфликты](#conflict)
+- [7. Приоритеты](#precedence)
+- [8. Обработка ошибок](#errors)
+- [9. Окружение](#environment)
+ - [9.1. Отладка построенных анализаторов](#zubr-debug)
+ - [9.2. Временные файлы](#tmp-files)
+- [10. Советы по подготовке входных спецификаций](#hints)
+ - [10.1. Стиль](#style)
+ - [10.2. Левосторонняя рекурсия](#recursion)
+ - [10.3. Лексическая привязка](#lex-anchor)
+- [11. Более мощные средства](#advanced)
+ - [11.1. `error`, `accept`, `abort`](#simulation)
+ - [11.2. Доступ к значениям слева](#access-values)
+ - [11.3. Произвольные типы значений](#value-types)
+- [12. Синонимы](#synonyms)
+- [13. Подмена lexer и parser на ходу](#runtime-switching)
+- [УПРАВЛЕНИЯ: командная строка](#controls)
+- [UTF-8 снаружи, UCS-2 внутри](#unicode-current)
+- [Примеры программ](#examples)
+- [Литература](#biblio)
+- [Без каких-либо гарантий](#warranty)
+
+---
+
+<a id="what"></a>
+
+## Что такое ZUBR?
+
+ZUBR — LALR(1)-генератор синтаксических анализаторов. Исторически язык входных спецификаций ZUBR строился как совместимый с AT&T Yacc и Berkeley Yacc, поэтому основные понятия `%token`, `%left`, `%right`, `%nonassoc`, `%prec`, `%start`, `%union`, `error`, `$$` и `$n` имеют привычный для yacc смысл. При этом у ZUBR есть собственные расширения командной строки и собственная организация генерируемого кода.
+
+По сравнению с подобными программами ZUBR имеет ряд усовершенствований, которые позволяют с его помощью создавать мультисинтаксические языки программирования (компиляторы которых могут содержать не один лексический и один синтаксический анализатор, а целый набор данных процедур в любой комбинации). См. раздел [«УПРАВЛЕНИЯ: командная строка»](#controls).
+
+ZUBR построен по алгоритму LALR(1)-разбора, построенному Tom Pennello и Frank DeRemer. Алгоритм описывается в статье TOPLAS Vol 4, Number 4, October 1982, Thomas J. Pennello, Frank DeRemer, Efficient Computation of LALR(1) Look-Ahead Sets. 615-649.
+
+Синтаксические анализаторы, создаваемые с помощью ZUBR, реализуют так называемый LALR(1) - разбор, являющийся модификацией одного из основных методов разбора "снизу вверх" - LR(k) - разбора (буквы L(eft) и R(ight) в обоих сокращениях означают соответственно чтение входных символов слева направо и использование правостороннего вывода. Индекс в скобках показывает число предварительно просматриваемых лексических единиц).
+
+Любой разбор по принципу "снизу вверх" (или восходящий разбор) состоит в попытке приведения всей совокупности входных данных (входной цепочки) к так называемому "начальному символу грамматики" путем последовательного применения правил вывода.
+
+Любой метод разбора требует грамматик с определёнными свойствами. В этом смысле ZUBR предполагает контекстно-свободные грамматики со свойствами LALR(1). LALR(1)-грамматики, являясь подмножеством LR(1)-грамматик, допускают при построении таблиц разбора сокращение общего числа состояний за счёт объединения идентичных состояний, различающихся только набором символов-следователей (символов, которые могут следовать после применения одного из правил вывода, если разбор по этому правилу проходил через данное состояние). Другие грамматики являются неоднозначными для принятого в ZUBR метода разбора и вызовут конфликты. Однако, если язык, описываемый данной грамматикой, в принципе допускает задание грамматики, однозначной для данного метода разбора, то ZUBR позволяет без перестройки грамматики построить анализатор, разрешающий конфликты на основе механизма приоритетов.
+
+---
+
+<a id="introduction"></a>
+
+## 1. Введение
+
+Программа ZUBR является обобщенным инструментом для построения программ, рассчитанных на определённую структуру входной информации. Такие программы явно или неявно включают в себя некоторую процедуру синтаксического анализа. Если при построении таких программ Вы намерены предоставлять пользователю относительную свободу и хотите при этом не портить собственную жизнь, программа ZUBR для Вас.
+
+Программа ZUBR предназначена для автоматического построения синтаксических анализаторов по спецификации, приготовленной пользователем. Данная спецификация включает в себя правила, описывающие структуру входной информации, код, вызываемый при разборе этих правил и процедуры ввода низкого уровня. ZUBR генерирует функцию («синтаксический анализатор», по умолчанию именуемую `zubr_parse()`) управления процессом ввода и вызовом семантических программ, соответствующих определённым правилам. Синтаксический анализатор вызывает созданную пользователем процедуру низкоуровневого ввода («лексический анализатор», по умолчанию именуемую `zubr_lex()`) для чтения из входного потока основных элементов, называемых терминальными символами, далее мы будем называть их просто терминалами. При обнаружении конструкций таких элементов, соответствующих определённым грамматическим правилам, синтаксический анализатор вызывает необходимые семантические процедуры (написанные пользователем для данного правила на языке С). Каждая семантическая процедура способна возвращать значения и пользоваться значениями, возвращаемыми другими семантическими процедурами.
+
+Таким образом, в зависимости от характера пользовательских семантических процедур, генерируемая ZUBR программа будет обеспечивать кроме анализа тот или иной вид обработки входного текста, например, его компиляцию или интерпретацию.
+
+Программа ZUBR написана на языке C. Семантические процедуры и функции ввода низкого уровня, составляющие входную спецификацию, также пишутся на C. Кроме того, многие синтаксические соглашения языка ZUBR следуют языку C.
+
+В текущем ZUBR 4.x текстовые файлы спецификаций и генерируемые текстовые файлы имеют внешнюю кодировку UTF-8. Внутри самого генератора текст представлен UCS-2 (`__mpu_char16_t`) и обрабатывается средствами LibMPUIO. Это позволяет использовать, например, русские имена терминалов и нетерминалов непосредственно в UTF-8-файле грамматики. Подробнее эта граница описана в разделе [«UTF-8 снаружи, UCS-2 внутри»](#unicode-current).
+
+### Грамматика как основа спецификации
+
+Основу входной спецификации ZUBR составляет набор грамматических правил. Правила задаются в виде, близком к нормальной форме Бэкуса ([BNF](#bnf), [НФБ](#bnf)), описывают допустимые грамматические конструкции и представлены уникальными именами. С точки зрения грамматического разбора правила рассматриваются как правила вывода (подстановки). Грамматические правила описываются в терминах некоторых исходных конструкций, которые называются лексическими единицами (символами). Например, одним из правил может быть следующее
+
+```yacc
+date : month_name day ',' year ;
+```
+
+Здесь `date`, `month_name`, `day` и `year` представляют собой структуры разбираемые в процессе ввода; предполагается, что правила вывода конструкций "month_name", "day" и "year" определены заранее каким-либо грамматическим правилом или декларированы как терминалы, которые являются низкоуровневыми структурами распознаваемыми лексическим анализатором самостоятельно. Запятая заключена в одинарные кавычки, это значит, что запятая есть литерал, который будучи прочитанным из входного потока лексическим анализатором просто передается на вход синтаксического анализатора. Двоеточие и точка с запятой просто служат как знаки пунктуации и не имеют смысла при контроле ввода. Так следующему предложению на входе
+
+```text
+July 4, 1776
+```
+
+может быть поставлено в соответствие приведённое выше правило.
+
+Здесь, следует заметить, что сопоставление набора букв `July` с символом "month_name" может быть поручено как лексическому анализатору, так и синтаксическому. В последнем случае символ `month_name` должен встретиться в левой части хотя бы одного правила.
+
+Значительная часть процесса разбора входного потока поручается лексическому анализатору (`zubr_lex()`). Эта пользовательская процедура, которая читает входной поток, разбирает структуры низкого уровня и передает распознанные символы синтаксическому анализатору. Исторически сложилось так, что элементы разбираемые лексическим анализатором называются терминальными символами ("terminal symbols"), а структуры, разбираемые синтаксическим анализатором называются нетерминальными символами ("nonterminal symbols"). Во избежание путаницы, терминальные символы принято именовать "tokens", мы будем применять также краткое название "терминалы".
+
+### Что поручать lexer, а что parser
+
+Существует проблема выбора способа разбора входных данных, которая лежит на пользователе, а именно, кому поручить разбор сложных терминальных конструкций, либо синтаксическому анализатору, либо лексическому. Например, правила
+
+```yacc
+month_name : 'J' 'a' 'n' ;
+month_name : 'F' 'e' 'b' ;
+ ...
+month_name : 'D' 'e' 'c' ;
+```
+
+могут быть использованы в предыдущем примере. Тогда, лексический анализатор должен будет распознавать на входе лишь одиночные символы и просто передавать их синтаксическому анализатору. В этом случае, символ `month_name` будет нетерминальным.
+
+Чаще, для разбора таких сравнительно простых конструкций поступают по-другому, то есть лексическому анализатору поручают распознавать "month_name" как цепочку необходимых литералов (одиночных символов) и возвращать синтаксическому анализатору лишь индикатор того, что прочитан соответствующий терминал. В этом случае, символ `month_name` является терминалом.
+
+Одиночные символы, например запятая (`','`), могут возвращаться лексическим анализатором непосредственно, а могут быть обозначены как терминалы, имеющие собственное имя (как, например, "month_name").
+
+Терминалы фактически представлены целыми числами, которые возвращает функция `zubr_lex()`. Их символическим именам соответствуют `#define`-определения препроцессора C, например,
+
+```c
+#define month_name 258
+#define COMMA 259
+```
+
+Данные определения генерируются программой ZUBR автоматически в процессе создания текста выходного файла. Кроме того,
+с помощью опции [`-I`](#opt-I) пользователь может включить в выходную программу свои собственные, приготовленные заранее, декларации.
+
+Язык описания входных спецификаций для программы ZUBR является достаточно простым и гибким средством задания грамматики входного языка будущей программы. Относительно нетрудно добавить в предыдущий пример еще одно правило
+
+```yacc
+date : month_name '/' day '/' year ;
+```
+
+и тогда входная конструкция
+
+```text
+7 / 4 / 1776
+```
+
+будет синонимом для
+
+```text
+July 4, 1776
+```
+
+Во время чтения входной информации, на вход синтаксического анализатора может поступить конструкция, не соответствующая ни одному грамматическому правилу спецификации. Такие ошибки программа ZUBR допускает, как теоретически возможные при разборе. Зарезервированное ключевое слово грамматики [error](#errors) позволяет анализатору, не только прочитать ошибочные конструкции, но и осуществить вывод по правилу. Сигнал ошибки (специальный терминал [error](#errors)), поддерживаемый программой ZUBR как часть входной спецификации, позволяет анализатору "восстановиться" и продолжить корректный разбор входной информации после пропуска ошибочных данных.
+
+Если входная спецификация содержит ошибки, ZUBR сообщает о них и прекращает генерацию. Неоднозначности грамматики рассматриваются отдельно: часть `shift/reduce` и `reduce/reduce` конфликтов допускается, диагностируется и разрешается по стандартным правилам ZUBR; многие `shift/reduce` конфликты можно устранить с помощью приоритетов и ассоциативности. Если же конструкция действительно не укладывается в выбранную грамматику LALR(1), приходится либо усиливать лексический анализ, либо перестраивать грамматику. Здесь полезно помнить старое практическое наблюдение: грамматические конструкции, слишком сложные для надежного LALR-разбора, нередко оказываются сложными и для человека, читающего язык.
+
+---
+
+<a id="bnf"></a>
+
+### 1.1. Нормальная форма Бэкуса — Наура (BNF)
+
+Например, грамматику целых чисел можно записать следующим образом:
+
+```text
+<число> ::= <чс>
+<чс> ::= <чс> <цифра> | <цифра>
+<цифра> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
+```
+
+Эта форма записи называется *нормальной формой Бэкуса* (сокращённо НФБ) или формой Бэкуса — Наура.
+Впервые была использована \[[1](#ref-1)\] для описания АЛГОЛа в сообщении о языке АЛГОЛ 60 (см. \[[2](#ref-2)\]).
+В оригинале сообщения перечислены авторы: Backus, Bauer, Green, Katz, McCarthy, Naur, Perlis, Rutishauser,
+Samelson, Vauquois, Wegstein, van Wijngaarden и Woodger, с пометкой Edited by Peter Naur.
+
+
+---
+
+<a id="spec"></a>
+
+## 2. Файл входной спецификации ZUBR
+
+На вход программы ZUBR подаётся текстовый UTF-8-файл спецификации. Исторически для таких файлов использовалось расширение `.zubr`; современные примеры проекта используют также привычное yacc-подобное расширение `.y`. Сам ZUBR расширение не интерпретирует: оно является соглашением проекта. Файл спецификации состоит из нескольких секций и фактически является инструкцией, по которой строится синтаксический анализатор.
+
+Имена, которые встречаются в описании грамматики, могут относиться либо к терминальным, либо к нетерминальным символам. Программа ZUBR требует предварительной (до первого использования) декларации терминалов. В спецификацию могут быть включены различные функции, написанные на языке C. Лексический анализатор, написанный пользователем, может быть включён непосредственно или находиться в отдельном модуле, разрабатываемой (пользователем) программы. При этом необходимо позаботиться о том, чтобы функция лексического анализа "видела" декларации терминалов, и сама, в свою очередь, была доступна синтаксическому анализатору.
+
+### Три секции файла
+
+В общем случае, файл входной спецификации состоит из трех частей (секций):
+
+- секция деклараций,
+- секция грамматических правил,
+- секция программ.
+
+Секции должны быть разделены двумя знаками процента (%%). (Символ процента (%) всегда используется в качестве управляющего.)
+
+Полный файл входной спецификации может быть представлен следующим образом.
+
+```yacc
+декларации (declarations)
+%%
+правила (rules)
+%%
+программы (programs)
+```
+
+Ядром файла спецификации и единственной его обязательной частью является секция правил. При отсутствии секции программ может быть опущена вторая группа символов "%%"; следовательно, минимальная допустимая конфигурация входного файла имеет вид:
+
+```yacc
+%%
+правила (rules)
+```
+
+При разборе файла входной спецификации программа ZUBR игнорирует пробелы, табуляции и символы перевода строки. Данные символы не следует использовать в именах и ключевых словах.
+
+Допустимы комментарии, принятые в языке C, которые начинаются с символов наклонной черты и звездочки (`/*`) и заканчиваются символами звездочки и наклонной черты (`*/`). Кроме того, разрешается использовать комментарии "конца строки", они начинаются с двух символов наклонной черты (//) и заканчиваются в конце текущей строки, причем, если такие комментарии будут встречены программой ZUBR в пользовательских семантических процедурах, они будут заменены на обычные (`/* ... */`).
+
+### Секция правил
+
+<a id="spec-rules"></a>
+
+**Секция правил** должна состоять хотя бы из одного правила. Правила записываются в форме
+
+```yacc
+A : BODY ;
+```
+
+Здесь `A` представляет собой нетерминальное имя, а `BODY` является одним или целым набором символов, которые, в свою очередь, могут быть как терминалами, так и нетерминалами, причем терминальные символы могут быть представлены литералами.
+
+Литералы состоят из одиночного символа, заключенного в одинарные кавычки (`'`). Как и в языке C, обратная косая черта (`\`) вводит escape-последовательность. Таким образом:
+
+- `'\a'` — звуковой сигнал;
+- `'\n'` — новая строка;
+- `'\r'` — возврат каретки;
+- `'\''` — одинарная кавычка (`'`);
+- `'\"'` — двойная кавычка (`"`);
+- `'\\'` — обратная косая черта (`\`);
+- `'\t'` — горизонтальная табуляция;
+- `'\v'` — вертикальная табуляция;
+- `'\b'` — забой;
+- `'\f'` — новая страница;
+- `\ddd` — числовое значение, заданное максимум тремя восьмеричными цифрами;
+- `\xdd` — числовое значение, заданное двумя шестнадцатеричными цифрами (`x` может быть и прописной — `X`).
+
+В текущем ZUBR числовые escape-последовательности литералов сохраняют историческую байтовую семантику и ограничены значением 255. Сам текст спецификации при этом читается как UTF-8 и внутри ZUBR представлен UCS-2, поэтому непосредственно записанные в кавычках символы проходят через UTF-8 → UCS-2 преобразование. Нулевой символ (`'\0'`, значение 0) по техническим причинам не может использоваться как грамматический литерал.
+
+Например, правило:
+
+```yacc
+HEAD : NAME '(' LIST ')' ;
+```
+
+определяет нетерминал "HEAD" как последовательность четырех элементов: два элемента заданы литерально ('(' и ')'), два других — именами.
+
+Допустимо задание нескольких правил, определяющих один нетерминальный символ, т.е. правил с одинаковой левой частью. Такие правила определяют конструкции, идентичные на некотором уровне. Ниже приводятся три правила, имеющие одинаковые левые части:
+
+```yacc
+A : B C D ;
+A : E F ;
+A : G ;
+```
+
+Правила с общей левой частью можно задавать в сокращенной форме, без повторения левой части, используя для разделения альтернативных определений символ (`|`). Таким образом, предыдущие правила можно переписать в виде
+
+```yacc
+A : B C D
+ | E F
+ | G
+ ;
+```
+
+Если нетерминальный символ представляет собой пустую строку, это можно записать, например, так:
+
+```yacc
+empty : ;
+```
+
+### Секция деклараций
+
+<a id="spec-declarations"></a>
+Имена, представляющие терминалы необходимо декларировать заранее, это просто сделать в **секции деклараций** следующим образом.
+
+```yacc
+%token name1 name2 ...
+```
+
+Любое имя, не определённое в секции деклараций, принимает на себя роль нетерминального символа. Каждый нетерминал должен появиться в левой части хотя бы одного грамматического правила.
+
+Имеется возможность ввести объявление:
+
+```yacc
+%ident строка
+```
+
+где строка — последовательность символов, начинающихся с двойной кавычки и заканчивающихся или двойной кавычкой или символом конца строки, который встретится первым. Такое объявление повлечет за собой вывод директивы `#ident` в начало выходного файла.
+
+### Стартовый символ
+
+Важнейшим нетерминалом является, так называемый, стартовый символ грамматики ("start symbol"). Будущий синтаксический анализатор имеет цель вывести этот стартовый символ путем свёртывания к нему всех грамматических правил спецификации. По умолчанию, стартовым является самый левый символ первого грамматического правила спецификации. Возможна также явная декларация стартового символа, для этого следует использовать ключевое слово **%start**:
+
+```yacc
+%start symbol
+```
+
+<a id="end-marker"></a>
+Конец ввода указывается синтаксическому анализатору посредством специального терминала, называемого "**end-marker**".
+
+Если терминалы, определённые выше, не содержат **end-marker** для какой-либо входной структуры данных, приводимых к стартовому символу, то функция `zubr_parse()` вернёт управление вызвавшей ее программе с извещением о завершении разбора (возвращаемое значение 0), если же **end-marker** встретится до того, как входные данные удастся привести к стартовому символу, это будет означать ошибку и функция `zubr_parse()`, посредством вызова `zubr_error()`, выдаст пользователю соответствующее сообщение.
+
+Если среди терминалов определён специальный **end-marker**, созданный пользователем, лексический анализатор должен возвращать этот символ.
+
+Обычно, и по умолчанию, **end-marker**-ом является символ конца файла (`end_of_file`). Таким образом, когда лексический анализатор вернёт 0 или `EOF` == (-1), одним словом, значение меньшее или равное нулю, синтаксический анализатор вернёт, вызвавшей его программе 0. Разумеется, это произойдет, только в том случае, если в текущем контексте **end-marker** не является ошибочным.
+
+Пока конец файла не достигнут, пользователь может многократно вызывать функцию `zubr_parse()` для разбора входных конструкций, заканчивающихся специальным (определённым им самим) **end-marker**-ом.
+
+---
+
+<a id="actions"></a>
+
+## 3. Семантические процедуры
+
+С каждым грамматическим правилом, пользователь может связать некую процедуру, выполняемую всякий раз, когда будет распознана входная конструкция, соответствующая данному правилу.
+
+Такие процедуры в дальнейшем мы будем называть семантическими (или просто, процедурами). Семантические процедуры могут возвращать значения и использовать значения, возвращаемые другими (семантическими) процедурами. Кроме того, лексический анализатор может возвращать значения, ассоциированные с определёнными терминалами, если это необходимо (речь идет не о возвращаемом значении функцией `zubr_lex()`, а о значении, передаваемом синтаксическому анализатору посредством переменной `zubr_lval`, [см. далее](#lex)).
+
+Семантическая процедура — это произвольный оператор или набор операторов, написанный на языке C и заключенный в фигурные скобки: ({) и (}). Например, записи
+
+```yacc
+A : '(' B ')'
+ {
+ hello( 1, "abc" );
+ }
+```
+
+или
+
+```yacc
+XXX : YYY ZZZ
+ {
+ printf( "a message\n" );
+ flag = 25;
+ }
+```
+
+представляют собой грамматические правила и, связанные с ними семантические процедуры.
+
+### `$$` и `$n`
+
+Для осуществления связи семантических процедур с синтаксическим анализатором используется знак доллара (`$`).
+
+Для того, чтобы вернуть какое-либо значение синтаксическому анализатору надо установить псевдопеременную `$$` в соответствующее значение. Например, процедура
+
+```yacc
+{ $$ = 1; }
+```
+
+возвращает значение, равное единице.
+
+Чтобы получить значения, возвращенные предыдущими процедурами (ранее вычисленные), либо лексическим анализатором, необходимо использовать псевдопеременные с именами `$1`, `$2`, ... , содержащие значения, возвращаемые компонентами правой части правила, и считаемые слева направо. Если есть правило
+
+```yacc
+A : B C D;
+```
+
+то `$2` содержит значение, возвращаемое частью (связанное с символом) C, а `$3` - значение, возвращаемое частью D.
+
+В качестве еще одного примера, рассмотрим правило
+
+```yacc
+expr : '(' expr ')'
+```
+
+Значение, возвращаемое данным правилом всегда порождается предыдущим значением "expr". Это может быть записано следующим образом.
+
+```yacc
+expr : '(' expr ')'
+ {
+ $$ = $2;
+ }
+```
+
+По умолчанию, значение правила порождается значением первого элемента его правой части (`$1`). Так, правило
+
+```yacc
+A : B ;
+```
+
+не требует явной декларации `{ $$ = $1 }`, так как программа ZUBR делает это автоматически.
+
+### Действия в середине правила
+
+В приведенных выше примерах, все семантические процедуры записывались в конце правил. Иногда, необходимо выполнить какую-либо процедуру до того, как правило будет полностью разобрано. Программа ZUBR дает возможность определять процедуры не только в конце, но также и в середине правил. Эти правила предполагают возврат значений, доступных через обычный механизм, использующий символ доллара, процедурам находящимся правее них. В этом случае, процедуры, присутствующие в правиле, а, следовательно, и их возвращаемые значения нумеруются также слева направо, как обычные части правила. Так, правило
+
+```yacc
+A : B
+ {
+ $$ = 1;
+ }
+ C
+ {
+ x = $2;
+ y = $3;
+ }
+ ;
+```
+
+присваивает переменной x значение, равное единице, а переменной y - значение, возвращаемое частью C.
+
+Процедуры, которые не завершают какое-либо правило, могут быть определены как пустое правило, например, предыдущее правило можно записать следующим образом.
+
+```yacc
+$ACT : /* empty */
+ {
+ $$ = 1;
+ }
+ ;
+
+A : B $ACT C
+ {
+ x = $2;
+ y = $3;
+ }
+ ;
+```
+
+### Семантические значения как структуры данных
+
+Многие приложения в процессе разбора, управляемого процедурами не выполняют вычисления сразу, а готовят внутренние структуры данных такие, например, как бинарные деревья, создаваемые в оперативной памяти и по которым в дальнейшем будут генерироваться определённые "выводы". Допустим, имеется функция (написанная на языке C), которая включает в строящееся дерево новый узел (node) и возвращает указатель (адрес памяти) на него:
+
+```c
+node( L, n1, n2 );
+```
+
+где, L- метка (содержимое) узла, а n1 и n2 - "потомки" этого узла. Тогда, запись
+
+```yacc
+expr : expr '+' expr
+ {
+ $$ = node( '+', $1, $3 );
+ }
+```
+
+обеспечит нам, при разборе, добавление к нашему дереву нового узла, соответствующего операции сложения.
+
+Пользователь может определять различные переменные и функции необходимые семантическим процедурам. Такие декларации и определения могут быть записаны на языке C, заключены в символы %{ и %} и добавлены в [секцию деклараций](#spec-declarations). Декларации, описанные таким образом, будут видны семантическим процедурам и лексическому анализатору (если, конечно, он описан в [секции программ](#spec)). Например, запись
+
+```yacc
+%{
+ int variable = 0;
+%}
+```
+
+размещенная в секции деклараций, делает доступной для всех процедур целочисленную переменную variable.
+
+Программа ZUBR использует имена переменных, начинающиеся с символов `zubr_` и `ZUBR_`. Пользователь должен избегать подобных имен в своих декларациях.
+
+Все переменные в предыдущих примерах были целыми (тип int). Обсуждение других типов данных Вы найдете в [следующих разделах](#value-types).
+
+---
+
+<a id="lex"></a>
+
+## 4. Лексический анализ
+
+Пользователь должен написать лексический анализатор самостоятельно. Лексический анализатор предназначен для чтения входного потока и передачи терминалов (с их значениями, если необходимо) синтаксическому анализатору. Лексический анализатор пишется в виде функции, возвращающей значение типа int и называемой по умолчанию `zubr_lex()`:
+
+```c
+int zubr_lex( void );
+```
+
+### `zubr_lval` и возвращаемый token
+
+<a id="zubr-lval"></a>
+Возвращаемое данной функцией значение, является номером терминала, который сообщает синтаксическому анализатору, что именно прочитано из входного потока. Если существует значение, ассоциируемое с данным терминалом, оно может быть передано синтаксическому анализатору посредством внешней переменной, именуемой по умолчанию `zubr_lval`.
+
+Номера терминалов одинаково воспринимаются (имеют одинаковые значения), как синтаксическим, так и лексическим анализаторами. Эти номера могут быть определены пользователем, или автоматически заданы программой ZUBR. В любом случае, для их задания в выходной C-программе используется механизм `#define`-определений препроцессора языка C. Это позволяет лексическому анализатору обращаться к ним по их символическим именам. Например, можно использовать (как в следующем фрагменте программы) терминал с именем DIGIT, определённый заранее в [секции деклараций](#spec-declarations) с помощью ключевого слова **[%token](#spec-declarations)**.
+
+Далее приводится часть лексического анализатора, возвращающая терминал DIGIT и его значение (через переменную `zubr_lval`).
+
+```c
+int zubr_lex( void )
+{
+ int c;
+
+ ...
+
+ c = getchar();
+
+ ...
+
+ switch( c )
+ {
+ ...
+
+ case '0':
+ case '1':
+ ...
+ case '9':
+ zubr_lval = c - '0';
+ return( DIGIT );
+
+ ...
+ }
+
+ ...
+
+} /* End of zubr_lex() */
+```
+
+Здесь подразумевается, что лексический анализатор описан в секции программ файла спецификации ZUBR и, кроме того, идентификатор DIGIT определён в секции деклараций следующим образом:
+
+```yacc
+%token DIGIT
+```
+
+Такой механизм упрощает модификацию лексического анализатора (при необходимости), так как пользователь не обязан отслеживать конкретные номера терминалов, а может использовать их по именам, встречающимся в грамматических правилах.
+
+При определении имен терминалов пользователь должен учитывать то, что не следует выбирать слова, совпадающие с ключевыми словами языка C или самого (создаваемого) синтаксического анализатора (напомним, что его имена начинаются с символов `zubr_` и `ZUBR_`), так как это может привести к ошибкам компиляции полученной программы. Имя терминала [error](#errors) зарезервировано программой ZUBR и его также не следует использовать.
+
+### Номера терминалов
+
+Номера терминалов могут быть заданы пользователем или сгенерированы программой ZUBR. Для именованных терминалов, которым номер явно не задан, текущая реализация выбирает свободные значения начиная с 257. Для односимвольного литерала его значением служит код самого символа; поскольку спецификация уже прочитана и представлена внутри ZUBR как UCS-2, здесь нет никакой выбираемой «домашней кодовой страницы».
+
+Если пользователь хочет определить собственный номер какого-либо терминала, он должен после первого вхождения терминального имени в секции деклараций записать неотрицательное число, например,
+
+```yacc
+%token DIGIT 548 IDENTIFIER IF ELSE
+```
+
+Это возможно и для литералов.
+
+Специальный [end-marker](#end-marker) имеет значение конца ввода. Пользовательский lexer должен вернуть `0` или отрицательное значение при достижении конца входного потока; обычный `EOF == -1` поэтому также воспринимается как конец ввода. Генерируемый parser приводит отрицательное значение, возвращенное lexer, к внутреннему маркеру конца.
+
+Важно различать **кодировку файла грамматики** и **кодировку потока, который разбирает уже сгенерированная программа**. ZUBR сам читает свою спецификацию в UTF-8, однако функция `zubr_lex()` принадлежит приложению. Именно пользовательский lexer решает, откуда и в какой кодировке получать исходный язык программы: через `getchar()`, LibMPUIO, память, сетевой поток и т. п. ZUBR не навязывает lexer-у способ ввода.
+
+---
+
+<a id="parse"></a>
+
+## 5. Операции разбора и файл `z.output`
+
+Программа ZUBR читает входную спецификацию и создаёт исходный текст на языке C, в конце которого размещает синтаксический анализатор (по умолчанию, функция `zubr_parse()`), написанный по грамматике, заданной пользователем в [секции правил](#spec-rules).
+
+Алгоритм, на котором базируется программа ZUBR можно найти в статье: TOPLAS Vol 4, Number 4, October 1982, Thomas J. Pennello, Frank DeRemer, Efficient Computation of LALR(1) Look-Ahead Sets. 615-649.
+
+Работа функции синтаксического анализа относительно проста и понятна, кроме основной задачи она занимается обработкой ошибок и разрешением конфликтных ситуаций.
+
+### Look-ahead и четыре действия автомата
+
+<a id="lookahead-actions"></a>
+Синтаксический анализатор, произведённый программой ZUBR, представляет собой конечный автомат, который получает терминалы через `zubr_lex()` и в зависимости от текущего состояния и входного терминала выполняет переходы. Следующий уже прочитанный, но ещё не потреблённый терминал будем называть **look-ahead**. Автомат имеет стек состояний; текущее состояние всегда находится на его вершине. В начале разбора в стеке находится состояние 0, а look-ahead ещё не задан. Parser обращается к `zubr_lex()` только тогда, когда для выбора действия действительно требуется следующий терминал.
+
+Для данного автомата допустимы всего четыре действия -
+
+- **shift** (сдвиг),
+- **reduce** (свёртка (по правилу)),
+- **accept** (нормальное завершение работы (в конце входного потока)),
+- **error** (ошибка).
+
+Шаг разбора устроен так:
+
+- parser проверяет, можно ли выбрать действие только по текущему состоянию; например, состояние может иметь свёртку по умолчанию;
+- если для выбора нужен входной терминал, а look-ahead ещё не прочитан, вызывается `zubr_lex()`;
+- по текущему состоянию и, при необходимости, look-ahead выбирается одно из действий **shift**, **reduce**, **accept** или **error**;
+- после выполнения действия цикл повторяется. Новый терминал читается не обязательно на каждом шаге: **reduce** и **goto** сохраняют текущий look-ahead.
+
+**shift** — самое распространённое действие анализатора. При сдвиге терминал, уже находящийся в **look-ahead**, считается принятым: новое состояние помещается в стек, а текущий look-ahead освобождается. Следующий терминал будет запрошен у lexer-а тогда, когда он действительно понадобится автомату. Например, в состоянии 56 одним из возможных действий может быть следующее:
+
+```text
+IF shift 34
+```
+
+Эта запись означает: если в состоянии 56 look-ahead равен `IF`, parser принимает этот терминал и помещает состояние 34 на вершину стека. Состояние 56 остаётся под ним, а look-ahead очищается; при следующей необходимости parser запросит новый терминал.
+
+**reduce** — выполняется тогда, когда анализатор готов к разбору отдельного правила, при этом, если необходимо, может быть проанализирована переменная **look-ahead**. Свёртка (**reduce**) ассоциируется с отдельным грамматическим правилом, которое в наших записях также представлено целым числом. Во избежание дальнейшей путаницы следует запомнить, что действие
+
+```text
+. reduce 18
+```
+
+означает свёртку по **правилу** 18, в то время как
+
+```text
+IF shift 34
+```
+
+говорит о переходе к **состоянию** 34.
+
+Рассмотрим действие **reduce** по правилу
+
+```yacc
+A : x y z ;
+```
+
+Процесс свёртки зависит от левой части правила (здесь, символ A) и от числа символов, стоящих в правой части (в данном случае, это число равно трем). Сначала из стека выталкивается три состояния (количество выталкиваемых состояний всегда равно количеству символов, стоящих в правой части правила). Эти три состояния были помещены в стек во время распознавания (или вывода) символов x, y и z.
+
+Итак, после выталкивания этих трех состояний в вершине стека открывается то состояние, которое анализатор имел до начала разбора этого правила. Используя это открывшееся состояние и символ, стоящий в левой части правила (A), анализатор предпринимает очередное действие и процесс повторяется снова и снова.
+
+После свёртки используется ещё одна операция — **goto**. Она не является ни сдвигом входного терминала, ни новой свёрткой и не читает lexer. После удаления из стека состояний правой части parser берёт открывшееся состояние и нетерминал из левой части правила и по таблице `goto` определяет следующее состояние. Например, запись
+
+```text
+A goto 20
+```
+
+означает: после свёртки к нетерминалу `A` parser должен перейти в состояние 20 и поместить его в стек. Содержимое **look-ahead** при этом не изменяется.
+
+Если в одном состоянии допустимы две свёртки и конфликт не разрешён приоритетами, возникает **reduce/reduce**-конфликт; по умолчанию ZUBR выбирает правило, которое раньше записано в секции правил.
+
+Если правая часть правила пуста, состояния из стека не извлекаются. После этого всё равно выполняется обычный `goto` по нетерминалу из левой части пустого правила.
+
+Свёртка включает выполнение определённой пользователем семантической процедуры. Сначала формируется семантическое значение правила, затем выполняется action, после чего parser удаляет из стеков элементы правой части и выполняет `goto` по левой части правила.
+
+Параллельно стеку состояний работает стек семантических значений. При **shift** в него помещается значение `zubr_lval`, соответствующее принятому терминалу. При свёртке значения `$1`, `$2`, … берутся из этого стека, результат правила формируется в `zubr_val`, а после `goto` именно `zubr_val` помещается в стек как значение полученного нетерминала.
+
+Итак, нам осталось рассмотреть два самых простых действия из списка возможных для анализатора.
+
+**accept** — означает о том, что достигнут конец входного потока и весь ввод удачно приведен к стартовому символу грамматики. Это действие может быть предпринято только после прочтения [`end-marker`](#end-marker)-а и говорит об удачном завершении работы анализатора.
+
+**error**, напротив, выполняется в том месте, где анализатор не может продолжить работу по заданной спецификации, и входной терминал не может быть отнесен ни к одной правильной грамматической конструкции. При этом анализатор сообщит об ошибке (посредством вызова функции [`zubr_error()`](#zubr-error)), попытается вернуться в нормальное состояние и продолжить разбор. [Вопросы восстановления после ошибок будут рассмотрены далее](#errors).
+
+### Как читать `z.output`: полный пример
+
+Рассмотрим следующую спецификацию
+
+```yacc
+%token DING DONG DELL
+%%
+rhyme : sound place
+ ;
+sound : DING DONG
+ ;
+place : DELL
+ ;
+```
+
+Если программу ZUBR запустить с опцией [-v](#opt-v), то будет создан файл `z.output`, содержащий описание работы анализатора. В начале данного файла будут приведены все грамматические правила, а в конце некоторая синтаксическая информация о количестве состояний, конфликтов и ошибок. Основное содержание файла описывает состояния автомата и перечень действий, возможных для анализатора в каждом состоянии. Следующий текст является примером описания состояний для грамматики, приведенной выше.
+
+```text
+state 0
+ $accept : _ rhyme $end
+ DING shift 3
+ . error
+ rhyme goto 1
+ sound goto 2
+
+state 1
+ $accept : rhyme _ $end
+ $end accept
+ . error
+
+state 2
+ rhyme : sound _ place
+ DELL shift 5
+ . error
+ place goto 4
+
+state 3
+ sound : DING _ DONG
+ DONG shift 6
+ . error
+
+state 4
+ rhyme : sound place _ (1)
+ . reduce 1
+
+state 5
+ place : DELL _ (3)
+ . reduce 3
+
+state 6
+ sound : DING DONG _ (2)
+ . reduce 2
+```
+
+Символ `_` обозначает текущую позицию разбора правила, номер которого задан в круглых скобках.
+
+Мы используем следующую последовательность ввода
+
+```text
+DING DONG DELL
+```
+
+для отслеживания операций, производимых анализатором.
+
+Изначально, текущим является состояние 0. Анализатор должен обратиться к входному потоку и выбрать, какое (из доступных в данном состоянии) действие предпринять. Так первый терминал есть DING и после его чтения в данном состоянии автомат может предпринять
+
+```text
+DING shift 3
+```
+
+сдвиг к состоянию 3, следовательно, состояние 3 опускается в стек и становится текущим, переменная **look-ahead** очищается. Новый терминал, DONG, читается в ячейку **look-ahead**. В состоянии 3, если следующий символ является символом DONG (**look-ahead** == DONG), может быть предпринят сдвиг к состоянию 6. Состояние 6 помещается в стек и ячейка **look-ahead** очищается. Сейчас в стеке записаны состояния 0, 3 и 6. В состоянии 6, без всякого анализа **look-ahead** символа, анализатор осуществляет свёртку по правилу
+
+```yacc
+sound : DING DONG
+```
+
+которое имеет номер 2. Два состояния, 6 и 3, выталкиваются из стека, открывая состояние 0. Исходя из описания состояния 0 (см. переход по символу sound) предпринимается действие
+
+```text
+sound goto 2
+```
+
+Состояние 2 опускается в стек и становится текущим.
+
+В состоянии 2 может быть прочитан новый символ DELL и помещен в **look-ahead**. Тогда предпринимается действие
+
+```text
+DELL shift 5
+```
+
+по которому, состояние 5 опускается в стек. Теперь в стеке записаны состояния 0, 2, 5 и содержимое переменной **look-ahead** стерто. В состоянии 5, возможно только сворачивание по правилу 3. Это правило в правой части содержит только один символ и, следовательно, только одно состояние (5) выталкивается из стека, открывается состояние 2 и становится текущим. Переход из состояния 2 по символу place (левая часть правила 3)
+
+```text
+place goto 4
+```
+
+осуществляется в состояние 4. Теперь в стеке записаны состояния 0, 2 и 4. В состоянии 4 возможно только одно действие, - свёртка по правилу 1. Существует два символа в правой части правила 1 и, следовательно, из стека извлекается два состояния, открывая состояние 0, которое становится текущим. Из состояния 0 возможен переход по rhyme (левая часть правила 1) в состояние 1. Содержимое стека, - 0,1. В состоянии 1 может быть прочитан [`end-marker`](#end-marker), обозначаемый `$end` в файле z.output. Предпринимается действие **accept** (если, конечно, прочитан [`end-marker`](#end-marker)), которое нормально завершает работу анализатора.
+
+Нетрудно видеть, что данный анализатор будет считать ошибочными следующие последовательности: DING DONG DONG, DING DONG, DING DONG DELL DELL и т.д.
+
+---
+
+<a id="conflict"></a>
+
+## 6. Неоднозначности и конфликты
+
+Набор грамматических правил (грамматика) является неоднозначным, если найдется входная строка, которая может быть разобрана по двум или более правилам (путям). Рассмотрим грамматическое правило
+
+```yacc
+expr : expr '-' expr
+```
+
+которое фактически предоставляет единственный способ формирования одного арифметического выражения из двух, разделенных знаком минус. Однако это правило не полностью определяет способ действий в случае более сложного входного потока. Например, имея на входе
+
+```text
+expr - expr - expr
+```
+
+конечное выражение можно вычислить двумя способами, либо как
+
+```text
+( expr - expr ) - expr,
+```
+
+либо как
+
+```text
+expr - ( expr - expr ).
+```
+
+(В первом случае мы говорим о "левой ассоциативности", а во втором о "правой ассоциативности".)
+
+Программа ZUBR определяет такие неоднозначности, когда пытается построить анализатор. При встрече на входе
+
+```text
+expr - expr - expr
+```
+
+перед анализатором встает проблема, которая может быть решена двумя путями.
+
+Первый способ состоит в следующем. Когда синтаксический анализатор встречает второе expr, он имеет выражение
+
+```text
+expr - expr
+```
+
+которое вычислит согласно приведённому выше правилу. После применения правила, вход свёртывается к expr (левая часть правила). Затем, прочитав заключительное
+
+```text
+- expr
+```
+
+применит это правило снова. Это называется левоассоциативной интерпретацией (оператора '-').
+
+Другой подход состоит в следующем. Если анализатор прочтет
+
+```text
+expr - expr
+```
+
+он не применит правило сразу, а продолжит чтение
+
+```text
+expr - expr - expr
+```
+
+и затем применит правило к правым трем символам, что приведет к сворачиванию их в expr и результату
+
+```text
+expr - expr
+```
+
+который затем будет свёрнут еще раз. Это называется правоассоциативной интерпретацией (оператора '-').
+
+Так, имея на входе
+
+```text
+expr - expr
+```
+
+анализатор может предпринять одно из двух легальных действий: **shift** или **reduce**. При невозможности правильного выбора между ними, мы имеем конфликт, называемый **shift**/**reduce** конфликтом. Может случиться так, что возможными окажутся сразу две свёртки. Это называется **reduce**/**reduce** конфликтом. Заметим, что никогда не может случиться **shift**/**shift** конфликт.
+
+### Правила разрешения конфликтов по умолчанию
+
+<a id="disambiguating-rules"></a>
+Когда существуют **shift**/**reduce** или **reduce**/**reduce** конфликты, программа ZUBR все равно сделает анализатор, который будет выбирать верные шаги там, где это возможно. При разрешении подобных конфликтов, анализатор, построенный программой ZUBR поступает согласно следующим двум правилам.
+
+- При **shift**/**reduce** конфликте, между сдвигом и сверткой выбирается сдвиг.
+- При **reduce**/**reduce** конфликте, выбирается свёртка по правилу, которое первым вошло в спецификацию.
+
+Программа ZUBR всегда сообщает пользователю о количестве **shift**/**reduce** и **reduce**/**reduce** конфликтов, которые будут разрешаться анализатором по приведенным выше правилам.
+
+При появлении сообщения о **reduce**/**reduce** конфликте, пользователь обязательно должен пересмотреть, заданную им грамматику.
+
+### Классический dangling `else`
+
+Однако, в ряде ситуаций, описанный способ разрешения конфликтов приводит к нужному результату. Например, правило
+
+```yacc
+stat : IF '(' cond ')' stat
+ | IF '(' cond ')' stat ELSE stat
+ ;
+```
+
+являющееся фрагментом грамматики для некоторого языка программирования, содержит **shift**/**reduce** неоднозначность. В этом правиле, IF и ELSE - терминалы (ключевые слова языка), cond - нетерминальный символ, описывающий условное (логическое) выражение и stat - нетерминальный символ, описывающий оператор языка.
+
+Первое правило может быть названо "**simple if**", а второе - "**if-else**".
+
+Эти два правила могут вызвать неоднозначность, когда на входе появится следующая структура.
+
+```text
+IF ( C1 ) IF ( C2 ) S1 ELSE S2
+```
+
+которая может быть разобрана двумя способами:
+
+<a id="dangling-else-first"></a>
+**(первый способ группирования)**
+
+```text
+IF( C1 )
+{
+ IF( C2 ) S1
+}
+ELSE
+ S2
+```
+
+или
+
+<a id="dangling-else-second"></a>
+**(второй способ группирования)**
+
+```text
+IF( C1 )
+{
+ IF( C2 ) S1
+ ELSE S2
+}
+```
+
+причем, вторая интерпретация является правильной для большинства языков программирования (**каждый `ELSE` относится к ближайшему предшествующему `IF`**).
+
+Рассмотрим ситуацию, когда на входе прочитана конструкция
+
+```text
+IF ( C1 ) IF ( C2 ) S1
+```
+
+и, следующим символом является ELSE. Здесь можно осуществить свёртку по правилу **simple if**, что даст
+
+```text
+IF ( C1 ) stat
+```
+
+а затем прочитать оставшиеся символы
+
+```text
+ELSE S2
+```
+
+и осуществить свёртку конструкции
+
+```text
+IF ( C1 ) stat ELSE S2
+```
+
+по правилу **if-else**. Это даст первую группировку ввода ([см. выше](#dangling-else-first)).
+
+С другой стороны, увидев ELSE можно осуществить сдвиг, прочитать S2 и правые символы последовательности
+
+```text
+IF ( C1 ) IF ( C2 ) S1 ELSE S2
+```
+
+свёрнуть по правилу **if-else** и лишь затем
+
+```text
+IF ( C1 ) stat
+```
+
+свёрнуть по **simple if**. Такой вывод соответствует второй группировке ввода ([см. выше](#dangling-else-second)).
+
+Итак, анализатор может предпринять одно из двух разрешенных действий, - это и есть **shift**/**reduce** конфликт. Программа ZUBR выбирает в этом случае **shift** ([см. выше: два правила разрешения конфликтов](#disambiguating-rules)).
+
+Данный конфликт возникает только тогда, когда следующим символом на входе является ELSE и имеется похожий на
+
+```text
+IF ( C1 ) IF ( C2 ) S1
+```
+
+входной поток. Конечно, может быть множество конфликтов, и каждый из них ассоциируется с входным символом и, с уже прочитанной частью входного потока. Прочитанные символы характеризуются текущим состоянием анализатора.
+
+Сообщение о конфликте, выдаваемое программой ZUBR в файл `z.output` (по опции [-v](#opt-v)) совершенно понятно пользователю. Например, по поводу разобранного ранее конфликта, в файле `z.output`, пользователь может прочитать следующую запись
+
+```text
+23: shift/reduce conflict (shift 45, reduce 18) on ELSE
+state 23
+ stat : IF ( cond ) stat _ (18)
+ stat : IF ( cond ) stat _ ELSE stat
+
+ ELSE shift 45
+ . reduce 18
+```
+
+где, первая строка определяет состояние, описывает конфликт и приводит входной символ, при появлении которого может возникнуть этот конфликт. Обычное же описание состояния содержит грамматические правила, активные на данный момент, и действия, которые может предпринять анализатор. Напомним, что символ `_` показывает текущую позицию разбора. В данном примере, в состоянии 23 анализатор имеет, в качестве уже прочитанной конструкцию
+
+```text
+IF ( cond ) stat
+```
+
+и два активных грамматических правила. При этом, анализатор может предпринять два допустимых решения. Если на входе появится ELSE, возможен сдвиг к состоянию 45. Состояние 45 имеет частью своего описания строку
+
+```text
+stat : IF ( cond ) stat ELSE _ stat
+```
+
+и при ELSE, анализатор перейдет к нему. Другое, возможное в состоянии 23, действие, - это свёртка. В нашем случае, если на входе появится символ отличный от ELSE, анализатор осуществит свёртку по правилу,
+
+```yacc
+stat : IF '(' cond ')' stat
+```
+
+имеющему номер 18.
+
+Напомним здесь, что номер, следующий за словом **shift**, является номером состояния, а номер, следующий за словом **reduce**, является номером правила. В файле `z.output`, номер правила также печатается в круглых скобках после правила, по которому может быть осуществлена свёртка.
+
+Надеемся, что данное описание файла `z.output` поможет пользователю легко разбираться с сообщениями о количестве **shift**/**reduce** конфликтов.
+
+---
+
+<a id="precedence"></a>
+
+## 7. Приоритеты
+
+Существует одна общая ситуация, когда правила, приведенные выше, для разрешения конфликтов не имеют полной информации. Это случается при разборе арифметических выражений. Должен существовать механизм задания уровней приоритетов операций, кроме того надо иметь возможность задавать информацию о левой или правой ассоциативности. Это может устранить неоднозначности грамматик.
+
+Основные грамматические правила для арифметических операций могут выглядеть следующим образом
+
+```yacc
+expr : expr OP expr
+```
+
+и
+
+```yacc
+expr : UNARY expr
+```
+
+для любых бинарных и унарных операций. Данные правила (при отсутствии, приведенных выше механизмов) могут создать множество конфликтных ситуаций при разборе. Для того, чтобы сообщить программе ZUBR о том, каким образом разрешать конфликты, в языке спецификаций существуют специальные ключевые слова.
+
+Приоритет и ассоциативность могут быть привязаны к конкретным терминалам при описании их в секции деклараций. Это может быть задано последовательностью строк, начинающихся ключевыми словами: **%left**, **%right**, **%nonassoc**, после которых даны списки терминалов. Все терминалы, представленные в одной строке, имеют одинаковые приоритеты; строки располагаются в порядке увеличения приоритета. Так, операторы
+
+```yacc
+%left '+' '-'
+%left '*' '/'
+```
+
+определяют приоритет и ассоциативность четырех арифметических операций. Плюс и минус левоассоциативны и имеют более низкий приоритет, чем звездочка и наклонная черта, которые также левоассоциативны. Ключевое слово **%right** определяет правоассоциативные операторы, а ключевое слово **%nonassoc** используется для описания операторов, таких как **.LT.** в ФОРТРАНЕ, которые не имеют ассоциативности. Так, запись
+
+```text
+A .LT. B .LT. C
+```
+
+недопустима в языке ФОРТРАН и этот оператор можно описать с помощью ключевого слова **%nonassoc** при задании входной спецификации. В качестве примера, рассмотрим описание
+
+```yacc
+%right '='
+%left '+' '-'
+%left '*' '/'
+%%
+expr : expr '=' expr
+ | expr '+' expr
+ | expr '-' expr
+ | expr '*' expr
+ | NAME
+ ;
+```
+
+по которому, входная конструкция
+
+```text
+a = b = c*d - e - f*d
+```
+
+будет разобрана следующим образом
+
+```text
+a = ( b = ( ( (c*d) - e ) - (f*d) ) ),
+```
+
+что соответствует правильному разбору (принятому в арифметике). Используя этот механизм, унарным операторам, также можно назначить приоритет.
+
+### `%prec` и унарные операции
+
+<a id="prec-directive"></a>
+Иногда, унарные и бинарные операторы, представленные одним и тем же символом, должны иметь разные приоритеты. Например, унарный и бинарный минус. Унарный минус должен иметь такой же приоритет как операция умножения, или даже больший, здесь же (в рассмотренном выше примере) унарный минус имеет меньший приоритет, чем у операции умножения. Ключевое слово **%prec** изменяет уровень приоритета для конкретного грамматического правила. Ключевое слово **%prec** должно появляться после самого правила до его завершения точкой с запятой, и за ним должно следовать имя терминала, либо литерал. Тогда приоритет операции (обозначенной каким-либо терминалом) будет изменен и сделан таким, каким обладает операция, заданная после ключевого слова **%prec**.
+
+Например, правила
+
+```yacc
+%right '='
+%left '+' '-'
+%left '*' '/'
+%%
+expr : expr '=' expr
+ | expr '+' expr
+ | expr '-' expr
+ | expr '*' expr
+ | '-' expr %prec '*'
+ | NAME
+ ;
+```
+
+позволяют использовать унарный минус с приоритетом, таким как у операции умножения. Это можно сделать несколько иначе, введя в грамматику неиспользуемый терминал:
+
+```yacc
+%right '='
+%left '+' '-'
+%left '*' '/'
+%left UNARY_MINUS
+%%
+expr : expr '=' expr
+ | expr '+' expr
+ | expr '-' expr
+ | expr '*' expr
+ | '-' expr %prec UNARY_MINUS
+ | NAME
+ ;
+```
+
+и, во многих случаях, данный способ может оказаться лучше, так как теперь унарный минус имеет более высокий приоритет, чем операция умножения.
+
+Терминалы, декларированные после ключевых слов **%left**, **%right**, **%nonassoc**, не запрещается предварительно описывать после ключевого слова **%token**, но можно этого не делать.
+
+Приоритеты и ассоциативность используются программой ZUBR для разрешения конфликтов разбора. ZUBR также пользуется двумя приведенными выше [правилами](#disambiguating-rules) разрешения **shift**/**reduce** и **reduce**/**reduce** конфликтов. Формально, правила работы программы ZUBR над разрешением конфликтов можно описать следующим образом.
+
+- Используются те приоритеты и ассоциативность, которые заданы при определении терминалов и литералов в секции деклараций.
+- Приоритет и ассоциативность операций связывается с каждым конкретным грамматическим правилом. Причем она зависит от приоритета и ассоциативности последнего вошедшего в правую часть правила, терминала или литерала. Если используется конструкция **%prec**, то она отменяет ранее установленный приоритет. Грамматические правила могут не иметь ассоциированных с ними ни приоритета ни ассоциативности.
+- Когда существует **reduce**/**reduce** или **shift**/**reduce** конфликт и входной символ или грамматическое правило не имеет приоритета и ассоциативности, тогда программа ZUBR действует по двум [правилам](#disambiguating-rules) разрешения конфликтов, приведенным выше.
+- Если есть **shift**/**reduce** конфликт и грамматическое правило и входной символ (вместе) имеют приоритеты и ассоциативность связанную с ними, конфликт разрешается выбором действия (**shift** или **reduce**), которое может быть связано с большим приоритетом, т.е. что имеет больший приоритет, правило или терминал, поступивший на вход. Если данные приоритеты одинаковы и используется ассоциативность, то при левой ассоциативности применяется **reduce**, при правой - **shift**, а при отсутствии ассоциативности (**%nonassoc**) - срабатывает действие **error**.
+
+Конфликты, разрешенные по заданным приоритетам не учитываются при выводе сообщения о количестве **shift**/**reduce** и **reduce**/**reduce** конфликтах программой ZUBR.
+
+Эта тема требует самостоятельных экспериментов и внимательного изучения файлов `z.output`.
+
+Заметим здесь, что программа ZUBR ключевые слова обрабатывает без учета регистра. Например, **%nonassoc**, **%NONASSOC**, **%NonAssoc** и **%nOnAsSoC** - воспринимаются одинаково.
+
+---
+
+<a id="errors"></a>
+
+## 8. Обработка ошибок
+
+Обработка ошибок представляет собой труднейшую область и имеет множество проблем. Например, при обнаружении ошибки, необходимо вернуть дерево разбора в предыдущее состояние, удалить или исправить записи в таблице символов и как обычно вывести соответствующее сообщение для пользователя.
+
+Очень неприятно останавливать всю работу при обнаружении ошибки. Чаще необходимо продолжить нормальное чтение входного потока до появления новых ошибок. Эту проблему мы назовем проблемой **восстановления** после ошибки. Большинство алгоритмов, при возникновении ошибки, очищают несколько последних прочитанных терминалов и пытаются снова запустить анализатор для нормального продолжения разбора.
+
+Для того, чтобы дать возможность пользователю управлять этими процессами, ZUBR поддерживает простые и разумные средства. В языке описания входных спецификаций имеется зарезервированное имя терминала error. Это имя может быть использовано в грамматических правилах. Стоит расположить это слово там, где ошибка может быть проанализирована и исправлена. Это позволит анализатору попасть в состояние, где терминал error допустим, и осуществить свёртку, при этом, естественно, [`look-ahead`](#lookahead-actions) переменная, содержащая значение error будет очищена и с ним уйдет обработанная ошибка. Если специальный символ error не определён ни в одном из правил, анализатор при появлении ошибки будет вынужден остановить весь процесс разбора.
+
+### Правила с терминалом `error`
+
+Часто правила, учитывающие возможность ошибки, задаются на уровне основных структурных единиц входного потока. Например, для пропуска ошибочных операторов, может быть использовано правило
+
+```yacc
+stat : error
+```
+
+При этом восстановление из состояния ошибки произойдет после нахождения трех терминалов (разумеется, безошибочных), которые могут следовать после оператора, например, начать новый оператор. Если точно распознать начало нового оператора невозможно, то ошибочное состояние может быть подавлено преждевременно, а обработка (якобы) нового оператора начата с середины ошибочного, что, вероятно, приведет к повторному сообщению об ошибке (на самом деле не существующей). Учитывая это, более надежного результата следует ожидать от правил вида:
+
+```yacc
+stat : error ';'
+```
+
+(предполагается, что символ ';' используется для обозначения конца операторов, разрабатываемого языка)
+
+Здесь восстановление произойдет только после нахождения ';' и двух начальных символов следующего оператора. Все символы после найденного ошибочного и до ';' будут отброшены (как принадлежащие правилу, разобрать которое не удалось).
+
+Другой способ включения правил для ошибочных операторов применим в приложениях, читающих пользовательский ввод построчно. Следующий пример является одним из возможных
+
+```yacc
+input : error '\n'
+ {
+ printf( "Reenter last line: " );
+ }
+ input
+ { $$ = $4; }
+ ;
+```
+
+При таком подходе существует одна потенциальная проблема. Анализатор должен прочитать три входных терминала прежде, чем корректно выйти из состояния ошибки. Если вновь введенная строка содержит ошибку в первых двух символах, анализатор удалит ошибочные терминалы и не выведет сообщения об ошибке. Это некорректно. Для таких случаев, есть механизм, позволяющий анализатору поверить в исправление принудительно. Оператор
+
+```c
+zubr_errok;
+```
+
+при появлении в семантической процедуре сбрасывает анализатор в нормальное состояние.
+
+Перепишем предыдущий пример, для устранения указанных недостатков.
+
+```yacc
+input : error '\n'
+ {
+ zubr_errok;
+ printf( "Reenter last line: " );
+ }
+ input
+ { $$ = $4; }
+ ;
+```
+
+### `zubr_clearin`
+
+Существует еще один оператор
+
+```c
+zubr_clearin;
+```
+
+он стирает [`look-ahead`](#lookahead-actions) символ, в котором хранится последний прочитанный терминал. Разумеется, его следует применять, если в семантической процедуре обеспечен корректный поиск нужной точки для возобновления ввода.
+
+Приведем общую форму правила с восстановительным действием
+
+```yacc
+stat : error
+ {
+ resynch();
+ zubr_errok;
+ zubr_clearin;
+ }
+ ;
+```
+
+Предполагается, что пользовательская процедура resynch() просматривает входной поток до начала очередного оператора, затем гасится состояние ошибки и стирается последний прочитанный символ (здесь он был ошибочным).
+
+---
+
+<a id="environment"></a>
+
+## 9. Окружение
+
+Когда пользователь подаёт на вход программы ZUBR спецификацию, ZUBR выдаёт программу, написанную на языке C, и по умолчанию размещает ее в файле `z_tab.c` в каталоге входной спецификации (если имя не переопределено опциями командной строки). Функция анализатора, произведенная программой ZUBR (по умолчанию) называется `zubr_parse()`; она возвращает целое значение (тип int). Функция `zubr_parse()` периодически вызывает, написанную пользователем, функцию `zubr_lex()` (см. [Лексический анализ](#lex)) для получения новых терминалов. Функция `zubr_parse()` возвращает единицу при возникновении ошибки, которую нельзя обработать (это возможно и в безошибочной ситуации по указанию пользователя (в семантической процедуре)). Когда же лексический анализатор прочитает [`end-marker`](#end-marker), и к тому времени разбор будет завершен `zubr_parse()` вернёт значение 0.
+
+Пользователь может определить любое использование полученной программы, содержащей `zubr_parse()`. Например, на языке C можно написать программу с функцией main(), которая вызывает `zubr_parse()`. Можно поступить иначе и вызвать `zubr_parse()` из какой-либо другой процедуры. Например, Вы можете написать функцию, которая принимает строку символов и, подразумевая, что данная строка содержит набор арифметических выражений, вычисляет их значения и затем возвращает переменную типа double, содержащую результат.
+
+<a id="zubr-error"></a>
+Дополнительно, пользователь должен написать процедуру с именем (по умолчанию) `zubr_error()` для печати сообщений о синтаксических ошибках (если не надо, то эти сообщения можно "похоронить" внутри неё). Вот простой пример использования кода, полученного с помощью программы ZUBR:
+
+```c
+#include <stdio.h> /* for fprintf() */
+
+int main( void )
+{
+ return( zubr_parse() );
+}
+
+void zubr_error( char *s )
+{
+ fprintf( stderr, "%s\n", s );
+}
+```
+
+Функция `zubr_error()` вызывается непосредственно из функции `zubr_parse()` с аргументом "syntax error" (при возникновении ошибки). Пользователь, зная это, может кроме вывода этой строки напечатать свое сообщение и, например, вывести номер строки во входном потоке, где обнаружена ошибка. Кроме того, внешняя целая переменная `zubr_char` содержит последний прочитанный символ, в то время как была обнаружена ошибка. Ее так же можно использовать при выводе диагностических сообщений.
+
+<a id="zubr-debug"></a>
+
+### Отладка
+
+Пользователь может определить переменную окружения `ZUBR_DEBUG`:
+
+```sh
+ZUBR_DEBUG=1
+export ZUBR_DEBUG
+```
+
+или
+
+```sh
+ZUBR_DEBUG=0
+export ZUBR_DEBUG
+```
+
+Эта переменная анализируется функцией `zubr_parse()` только в том случае, если parser был сгенерирован с опцией [`-t`](#opt-t). Сгенерированный код читает первый символ значения `ZUBR_DEBUG`: цифра от `0` до `9` задаёт runtime-значение переменной `zubr_debug`. При ненулевом значении parser выводит в `stdout` трассу чтения терминалов, переходов, свёрток и восстановления после ошибок. Поэтому `ZUBR_DEBUG=1` включает трассировку, а `ZUBR_DEBUG=0` отключает её без пересборки уже сгенерированного с `-t` parser-а.
+
+<a id="tmp-files"></a>
+
+### Временные файлы
+
+Для промежуточных данных ZUBR последовательно проверяет переменные окружения `TMPDIR`, `TEMP` и `TMP`. Если ни одна из них не задана, временные файлы создаются в текущем каталоге.
+
+Текущая реализация включает PID процесса в имя и использует три файла следующего вида:
+
+```text
+zubr_<pid>._a_
+zubr_<pid>._t_
+zubr_<pid>._u_
+```
+
+Они служат соответственно для временного хранения семантических действий, текстовых фрагментов и определения `%union`. ZUBR удаляет их при завершении работы. Включение PID в имя устраняет старую проблему фиксированных имен временных файлов при параллельных запусках.
+
+---
+
+<a id="hints"></a>
+
+## 10. Советы по подготовке входных спецификаций
+
+Эта часть содержит разнообразные советы по стилю оформления входных спецификаций, понятных другим пользователям и самому автору (по прошествии некоторого времени).
+
+<a id="style"></a>
+
+### Стиль
+
+Для того, чтобы Ваши спецификации были понятны другим пользователям и Вам самим после того, как пройдет время и Вы забудете о чем писали, можно посоветовать придерживаться следующих советов.
+
+- Использовать прописные буквы (верхний регистр) для имён терминалов и строчные — для имён нетерминальных символов. Это, кстати, согласуется с языком C, где давно принято `#define`-имена задавать большими буквами.
+- Записывать грамматические правила и семантические процедуры в разных строках. (Если придется изменять одно, не придется менять другое.)
+- При записи нескольких правил с одинаковой левой частью, левую часть записывать один раз, а за ней через вертикальную черту правые части, причем каждую правую часть начинать на отдельной строке и перед ней ставить вертикальную черту (`|`).
+- Точку с запятой писать только после последнего правила и причем на новой строке. Тогда при стремительном редактировании, Вы сможете легко добавлять новые правила (например, простым копированием текста с помощью Вашего любимого текстового редактора).
+- Записывать правые части правил и семантические процедуры с отступом от левого края, причем отступ для семантической процедуры делать больше. Не пользоваться символом табуляции `'\t'`, так как в разных редакторах его длина различна и не всегда исправляема.
+
+По поводу последнего совета приведем пример, в котором показаны отступы, лучше всего согласующиеся с текстом программ, приготовленным с помощью **ZUBR**.
+
+```text
+expr: NUMBER
+ | VAR { $$ = mem[$1]; }
+ | VAR '=' expr { $$ = mem[$1] = $3; }
+ | expr '+' expr { $$ = $1 + $3; }
+ | expr '-' expr { $$ = $1 - $3; }
+ | expr '*' expr { $$ = $1 * $3; }
+<--4-->| expr '/' expr
+<--6---->{
+<--8------->if( $3 == 0.0 ) execerror( "Division by zero", "" );
+ $$ = $1 / $3;
+ }
+ | '(' expr ')' { $$ = $2; }
+ | '-' expr %prec UNARYMINUS { $$ = -$2; }
+ ;
+%%
+```
+
+Данный тест, в выходном файле будет оформлен следующим образом.
+
+```c
+#line 23 "m_hoc.zubr"
+ { m_zubr_val.val = mem[m_zubr_vsp[0].index]; }
+ break;
+ case 7:
+#line 24 "m_hoc.zubr"
+ { m_zubr_val.val = mem[m_zubr_vsp[-2].index] = m_zubr_vsp[0].val; }
+ break;
+ case 8:
+#line 25 "m_hoc.zubr"
+ { m_zubr_val.val = m_zubr_vsp[-2].val + m_zubr_vsp[0].val; }
+ break;
+ case 9:
+#line 26 "m_hoc.zubr"
+ { m_zubr_val.val = m_zubr_vsp[-2].val - m_zubr_vsp[0].val; }
+ break;
+ case 10:
+#line 27 "m_hoc.zubr"
+ { m_zubr_val.val = m_zubr_vsp[-2].val * m_zubr_vsp[0].val; }
+ break;
+ case 11:
+#line 29 "m_hoc.zubr"
+ {
+ if( m_zubr_vsp[0].val == 0.0 ) execerror( "Division by zero", "" );
+ m_zubr_val.val = m_zubr_vsp[-2].val / m_zubr_vsp[0].val;
+ }
+ break;
+ case 12:
+#line 33 "m_hoc.zubr"
+ { m_zubr_val.val = m_zubr_vsp[-1].val; }
+ break;
+ case 13:
+#line 34 "m_hoc.zubr"
+ { m_zubr_val.val = -m_zubr_vsp[0].val; }
+ break;
+#line 470 "output.c"
+```
+
+<a id="recursion"></a>
+
+### Левосторонняя рекурсия
+
+Алгоритм анализатора поддерживает, так называемые, леворекурсивные грамматические правила. То есть правила вида
+
+```yacc
+name : name rest_of_rule ;
+```
+
+Правила, такие как
+
+```yacc
+list : item
+ | list ',' item
+ ;
+```
+
+и
+
+```yacc
+seq : item
+ | seq item
+ ;
+```
+
+часто используются для описания последовательностей и списков. В любом из этих случаев, первое правило применяется для первого, а второе - для всех остальных элементов последовательности.
+
+Допустимы также праворекурсивные правила
+
+```yacc
+seq : item
+ | item seq
+ ;
+```
+
+однако при правой рекурсии parser вынужден дольше удерживать незавершённые конструкции во внутреннем стеке. Для длинных последовательностей глубина стека поэтому может существенно вырасти и привести к переполнению. Размер стека можно задавать директивой
+
+```c
+#define ZUBR_STACKSIZE n
+```
+
+или
+
+```c
+#define ZUBR_MAXDEPTH n
+```
+
+где, n - максимальное количество записей во внутреннем стеке, которое по умолчанию равно 500. При использовании `-Bprefix` имена макросов получают соответствующий верхнерегистровый префикс, например `PREFIXZUBR_STACKSIZE` и `PREFIXZUBR_MAXDEPTH`.
+
+Для обеспечения возможности задания пустых списков следует пользоваться пустым правилом
+
+```yacc
+seq : /*empty */
+ | seq item
+ ;
+```
+
+Сочетание пустых и рекурсивных правил является удобным способом представления грамматик, однако, некорректное использование пустых правил может вызвать конфликтные ситуации из-за неоднозначности выбора нетерминала, соответствующего пустой последовательности.
+
+<a id="lex-anchor"></a>
+
+### Лексическая привязка
+
+Некоторые действия при лексическом анализе могут зависеть от контекста. Например, лексический анализатор может пропускать пробелы при нормальной работе и не удалять их при разборе строковых констант, или, например, имена, встречающиеся в декларациях должны вызывать процедуру записи соответствующих данных в таблицу символов, а при встрече имен в выражениях, таких записей делать не надо.
+
+Один из путей передачи текущего контекста лексическому анализатору состоит в создании глобальных переменных (флагов), которые лексический анализатор может проверять во время своей работы. Например,
+
+```yacc
+%{
+int dflag;
+%}
+
+... другие декларации ...
+
+%%
+
+prog : decls stats
+ ;
+
+decls : /* empty */
+ {
+ dflag = 1;
+ }
+ | decls declaration
+ ;
+
+stats : /* empty */
+ {
+ dflag = 0;
+ }
+ | stats statement
+ ;
+
+... другие правила ...
+```
+
+данная спецификация программы говорит о том, что программа может содержать ноль или более деклараций, за которыми следует ноль или более операторов. Причем флаг dflag имеет значение ноль в процессе чтения операторов и установлен в единицу, когда читаются декларации. **Исключая первый терминал первого оператора!** Этот терминал может быть увиден анализатором до того, как он может "сообщить" о конце деклараций. Но для многих случаев, это не так важно.
+
+---
+
+<a id="advanced"></a>
+
+## 11. Более мощные средства
+
+Эта часть посвящена некоторым средствам, поддерживаемым программой ZUBR, которые обеспечивают более эффективную работу по созданию приложений.
+
+<a id="simulation"></a>
+
+### Симуляция действий `error` и `accept`
+
+Действия синтаксического анализатора **[error](#lookahead-actions)** и **[accept](#lookahead-actions)** могут быть вызваны принудительно из пользовательских семантических процедур посредством макроопределений `ZUBR_ACCEPT` и `ZUBR_ERROR`. Макроопределение `ZUBR_ACCEPT` заставляет функцию `zubr_parse()` вернуть значение 0; `ZUBR_ERROR` заставляет анализатор рассматривать текущий прочитанный символ как синтаксически неверный; при этом вызывается функция [`zubr_error()`](#zubr-error), и происходит восстановление после ошибки. Этот механизм может быть использован для симуляции работы анализатора с многими [`end-marker`](#end-marker)-ами или для проверки контекстно-зависимого синтаксиса. Существует еще одно макроопределение (`ZUBR_ABORT`), которое заставляет анализатор прекратить работу и вернуть значение 1.
+
+<a id="access-values"></a>
+
+### Доступ к значениям
+
+Семантическая процедура может обращаться к значениям, возвращаемым другими процедурами, находящимися слева от текущего правила. Механизм аналогичен, - записывается знак доллара, а за ним число.
+
+```yacc
+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 во входном потоке. В нашем примере структура ввода просматривается отчетливо (см. нетерминал *adj*).
+
+<a id="value-types"></a>
+
+### Поддержка произвольных типов значений
+
+По умолчанию значения, возвращаемые семантическими процедурами и лексическим анализатором (переменная `zubr_lval`), имеют целый тип (int). Программа ZUBR поддерживает также значения других типов, включая структуры. Дополнительно, ZUBR может заключить все используемые типы значений в объединение (см. union в языке C), и анализатор сможет выбирать из него значения, соответственно необходимому на данный момент типу данных. При этом внутренний стек значений анализатора содержит записи, используемого типа **union**. Пользователь декларирует объединение и ассоциируемые с ним типы и имена для каждого терминала и нетерминальных символов, имеющих значения. Когда происходит обращение к переменным `$$` и `$n`, ZUBR автоматически использует входящее в **union** имя для приведения типов данных.
+
+Существует три механизма, которые пользователь может применять работая с различными типами значений. Первое, что должен сделать пользователь, это определить объединение (ключевое слово **%union**), и все его компоненты с именами для того, чтобы их могли использовать другие программы, такие, например, как `zubr_lex()`. Затем связать имена, входящие в объединение, с конкретными терминалами и нетерминальными символами. И наконец, там, где ZUBR не сможет автоматически определить тип значения, осуществить явное приведение типа.
+
+Для задания объединения, пользователь включает
+
+```yacc
+%union
+{
+ ... тело объединения ....
+}
+```
+
+в секцию деклараций. Это заставляет ZUBR определить тип значений, хранящихся в стеке, и переменных `zubr_lval`, `zubr_val` точно таким как тип заданного объединения. Если ZUBR запущен с опцией [-d](#opt-d), то декларация объединения скопируется еще и в файл z_tab.h (имя по умолчанию). Другой способ состоит в том, что пользователь (посредством директивы typedef языка C, в своем заголовочном файле или в [секции деклараций](#spec-declarations) (см. %{ и %})) определяет тип переменной `ZUBR_STYPE`. Данное определение может иметь следующий вид.
+
+```c
+typedef union
+{
+ ... тело объединения ...
+
+} ZUBR_STYPE;
+```
+
+Заметим здесь, что простые типы можно определять еще одним способом:
+
+```yacc
+%{
+#define ZUBR_STYPE double
+%}
+```
+
+Когда тип переменной `ZUBR_STYPE` определён, элементы объединения (по их именам) могут быть ассоциированы с различными терминалами и нетерминальными символами с помощью конструкции
+
+```text
+<name>
+```
+
+которая обозначает имя элемента объединения. При декларации терминалов посредством ключевых слов **`%token`**, **`%left`**, **`%right`**, **`%nonassoc`**; можно поступить следующим образом.
+
+```yacc
+%left <optype> '+' '-'
+```
+
+Это говорит о том, что значение, возвращаемое этими двумя терминалами, может быть выбрано из объединения посредством обращения к нему с именем optype.
+
+<a id="type-directive"></a>
+Другое ключевое слово **%type** специально предназначено для определения типа нетерминалов. Так запись
+
+```yacc
+%type <nodetype> expr stat
+```
+
+ассоциирует элемент объединения именованный как nodetype с нетерминальными символами expr и stat.
+
+Бывают случаи, когда анализатор не может автоматически приводить значения к нужному типу, например, если заранее не известен тип значения, возвращаемого семантической процедурой. Так, обращаясь к значению контекста слева (здесь, `$0`), программе ZUBR будет нелегко самостоятельно произвести приведение к нужному типу. В этом случае, тип может быть задан непосредственно с помощью имени элемента объединения, заключенного в угловые скобки и записанного после первого знака доллара. Следующий пример
+
+```yacc
+rule : three
+ {
+ $<intval>$ = 3;
+ }
+ call_func
+ {
+ func( $<intval>2, $<other>0 );
+ }
+ ;
+```
+
+демонстрирует это. Такой синтаксис прост и может быть рекомендован во многих других ситуациях.
+
+---
+
+<a id="synonyms"></a>
+
+## 12. Синонимы
+
+Некоторые ключевые слова, используемые во входной спецификации ZUBR, могут быть заменены другими символами, имеющими абсолютно такое же значение. Кроме того при оформлении спецификации можно по-разному декларировать семантические процедуры и литералы. Вот перечень допустимых "синонимов".
+
+**1**. Литералы могут быть заключены в двойные кавычки (`"`).
+
+**2**. Литерал может иметь длину более одного символа. Использование многосимвольных литералов может ввести в заблуждение тех кто недостаточно знаком с программой ZUBR. (Обычно для ввода значения, состоящего из цепочки символов используют переменную [`zubr_lval`](#zubr-lval).)
+
+**3**. Синонимы:
+
+| Основная запись | Допустимый синоним |
+|---|---|
+| `%` | `\` |
+| `%%` | `\\` |
+| `%left` | `\left`, `%<` |
+| `%right` | `\right`, `%>` |
+| `%nonassoc` | `\nonassoc`, `\binary`, `%binary`, `%2` |
+| `%token` | `\token`, `\term`, `%term`, `%0` |
+| `%prec` | `\prec`, `%=` |
+
+**4**. [Семантические процедуры](#actions) могут быть заданы следующим образом.
+
+```yacc
+={ ... тело ... }
+```
+
+Причем, если процедура представлена лишь одним оператором языка C, то фигурные скобки (в данном случае) могут быть опущены.
+
+**5**. Код, написанный на языке C и окруженный символами %{ и %}, можно задавать не только в [секции деклараций](#spec-declarations), но и в начале секции правил.
+
+---
+
+<a id="runtime-switching"></a>
+
+## 13. Подмена lexer и parser на ходу
+
+ZUBR изначально допускает проекты, в которых внутри одного процесса находятся
+несколько лексических и синтаксических анализаторов. Это полезно для языков с
+вложенными подъязыками, директивами наподобие `#lang`, встроенными выражениями
+другого синтаксиса и комбинированными конфигурационными форматами.
+
+Главное различие состоит в следующем:
+
+> `-L` задает имя **того, что parser вызывает**.
+>
+> `-P` задает имя **функции, которую ZUBR сам определяет**.
+
+Из-за этого lexer можно подменять одним присваиванием function pointer, а
+текущий выполняющийся parser — нельзя.
+
+### Что на самом деле делает `-L`
+
+Пусть lexer имеет тип:
+
+```c
+typedef int (*LEXICAL_ANALYSER)( void );
+```
+
+и имеются две реализации:
+
+```c
+int lex1( void );
+int lex2( void );
+```
+
+Определим текущий lexer как переменную:
+
+```c
+LEXICAL_ANALYSER main_lex = lex1;
+```
+
+Декларация `main_lex` должна быть видна компилятору в том translation unit, куда попал сгенерированный parser. Обычно ее помещают в общий заголовок и подключают через `%{ ... %}` или секцию программ. ZUBR не обязан знать, что это именно function pointer: для него существенно лишь корректное C-выражение `main_lex()`.
+
+Грамматику генерируем так:
+
+```sh
+zubr -Lmain_lex -Pparse1 grammar1.y
+```
+
+ZUBR вставляет в parser обычное C-выражение вызова:
+
+```c
+main_lex();
+```
+
+В C один и тот же синтаксис используется и для вызова функции, и для вызова
+через указатель на функцию. Поэтому parser не обязан знать, что `main_lex` —
+переменная.
+
+Теперь:
+
+```c
+main_lex = lex1;
+```
+
+означает, что следующий запрос терминала пойдет в `lex1()`, а:
+
+```c
+main_lex = lex2;
+```
+
+— что **следующий же** запрос терминала того же работающего parser пойдет в
+`lex2()`.
+
+Это настоящая runtime-подмена lexer.
+
+### Почему `-P` принципиально отличается от `-L`
+
+На первый взгляд может показаться, что можно определить:
+
+```c
+typedef int (*PARSER)( void );
+PARSER main_parse;
+```
+
+а обе грамматики построить с:
+
+```sh
+-Pmain_parse
+```
+
+Так делать нельзя.
+
+`-Pmain_parse` не говорит parser «вызывай переменную `main_parse`». Эта опция
+говорит ZUBR: **сгенерируй функцию с именем `main_parse`**.
+
+Если две разные грамматики обработать одинаково:
+
+```sh
+zubr -Lmain_lex -Pmain_parse grammar1.y
+zubr -Lmain_lex -Pmain_parse grammar2.y
+```
+
+оба файла будут содержать определение:
+
+```c
+int
+main_parse( void )
+{
+ /* свои таблицы и своя грамматика */
+}
+```
+
+и при компоновке возникнет конфликт двух определений одной функции.
+
+Поэтому реальные parser должны иметь **разные имена**:
+
+```sh
+zubr -Lmain_lex -Pparse1 grammar1.y
+zubr -Lmain_lex -Pparse2 grammar2.y
+```
+
+а `main_parse` создаётся уже пользовательским кодом как отдельный диспетчерский
+указатель:
+
+```c
+typedef int (*PARSER)( void );
+
+PARSER main_parse = parse1;
+```
+
+Теперь:
+
+```c
+main_parse();
+```
+
+вызовет тот parser, адрес которого находится в указателе в данный момент.
+
+### Два языка в одном процессе
+
+Полная минимальная схема:
+
+```c
+typedef int (*LEXICAL_ANALYSER)( void );
+typedef int (*PARSER)( void );
+
+int lex1( void );
+int lex2( void );
+
+int parse1( void );
+int parse2( void );
+
+LEXICAL_ANALYSER main_lex = lex1;
+PARSER main_parse = parse1;
+```
+
+Первая грамматика:
+
+```sh
+zubr -Blang1_ -Lmain_lex -Pparse1 grammar1.y
+```
+
+вторая:
+
+```sh
+zubr -Blang2_ -Lmain_lex -Pparse2 grammar2.y
+```
+
+В нормальном состоянии:
+
+```c
+main_lex = lex1;
+main_parse = parse1;
+
+main_parse();
+```
+
+получаем цепочку:
+
+```text
+main_parse -> parse1()
+ |
+ +--> main_lex() -> lex1()
+```
+
+После переключения:
+
+```c
+main_lex = lex2;
+main_parse = parse2;
+```
+
+новый вызов:
+
+```c
+main_parse();
+```
+
+дает:
+
+```text
+main_parse -> parse2()
+ |
+ +--> main_lex() -> lex2()
+```
+
+### Стек контекста `#lang` / `#endlang`
+
+Для вложенных языков удобно хранить пару текущих обработчиков:
+
+```c
+struct lang_context
+{
+ struct lang_context *chain;
+ LEXICAL_ANALYSER lex;
+ PARSER parse;
+};
+
+static struct lang_context *lang_stack;
+```
+
+При входе в новый язык сохраняется текущая пара:
+
+```c
+push_lang( main_lex, main_parse );
+
+main_lex = lex2;
+main_parse = parse2;
+```
+
+при выходе восстанавливается предыдущая:
+
+```c
+pop_lang( &main_lex, &main_parse );
+```
+
+Концептуально вложенный разбор выглядит так:
+
+```text
+parse1()
+ |
+ +--> main_lex -> lex1()
+ |
+ | обнаружен #lang language2
+ v
+ push(lex1, parse1)
+ main_lex = lex2
+ main_parse = parse2
+ |
+ v
+ main_parse()
+ |
+ v
+ parse2()
+ |
+ +--> main_lex -> lex2()
+ |
+ | #endlang
+ v
+ return/end-marker
+ |
+ v
+ pop(lex1, parse1)
+ |
+ v
+ продолжается parse1()
+```
+
+Реальный протокол `#lang`/`#endlang` зависит от lexer приложения. Один удобный
+вариант состоит в том, что lexer вложенного языка при `#endlang` возвращает
+своему parser конец входа; `parse2()` успешно завершается, после чего вызывающий
+код восстанавливает предыдущую пару lexer/parser.
+
+### Подмена lexer во время уже работающего parser
+
+Это простая часть.
+
+Если `parse1()` уже выполняется и содержит вызов:
+
+```c
+main_lex();
+```
+
+то присваивание:
+
+```c
+main_lex = lex2;
+```
+
+достаточно само по себе. При следующем запросе входного терминала `parse1()`
+вызовет уже `lex2()`.
+
+Иными словами, function pointer находится **на пути каждого будущего вызова
+lexer**.
+
+### Подмена parser во время уже работающего parser
+
+Здесь механизм другой.
+
+Предположим, стек C уже содержит:
+
+```text
+parse1()
+```
+
+Присваивание:
+
+```c
+main_parse = parse2;
+```
+
+не заменит выполняющуюся функцию `parse1()` на `parse2()`. Адрес функции был
+использован в момент вызова `main_parse()`; после этого CPU уже исполняет код
+`parse1`.
+
+Чтобы действительно перейти к другой грамматике, необходимо **совершить новый
+вызов**:
+
+```c
+main_parse = parse2;
+main_parse();
+```
+
+Теперь `parse2()` выполняется вложенно. После его возврата управление снова
+окажется в старом `parse1()`, который продолжит работу с того места, где был
+вызван вложенный parser.
+
+Это и есть фундаментальное различие:
+
+| Lexer | Parser |
+|---|---|
+| вызывается многократно во время одного разбора | сам является текущим активным вызовом |
+| `main_lex = lex2` действует со следующего token request | `main_parse = parse2` влияет только на следующий вызов через указатель |
+| не требуется новый parser frame | для перехода в другую грамматику нужен `main_parse()` / `parse2()` |
+
+### Роль `-B` при нескольких parser
+
+Уникальных имен `parse1()` и `parse2()` недостаточно. Каждый сгенерированный
+parser имеет набор служебных глобальных объектов:
+
+```text
+zubr_lval
+zubr_char
+zubr_nerrs
+zubr_debug
+zubr_ss
+zubr_vs
+...
+```
+
+Если два parser компилируются в разные объектные файлы с одинаковыми именами
+этих объектов, при линковке возникнут конфликты либо один parser начнет
+использовать состояние другого.
+
+Поэтому для нескольких одновременно линковавшихся parser рекомендуется давать
+каждой грамматике свой `-B`:
+
+```sh
+zubr -Blang1_ -Lmain_lex -Pparse1 grammar1.y
+zubr -Blang2_ -Lmain_lex -Pparse2 grammar2.y
+```
+
+При этом `-Lmain_lex` имеет приоритет над тем изменением имени lexer, которое
+обычно сделал бы `-B`: оба parser продолжают вызывать общий диспетчер
+`main_lex`.
+
+`-Pparse1` и `-Pparse2` аналогично задают явные имена parser, но `-B` продолжает
+разделять остальные глобальные объекты.
+
+Следствие для semantic value: `lex1()` должен записывать значение в semantic
+value первого parser, например:
+
+```c
+lang1_zubr_lval
+```
+
+а `lex2()` — во второй:
+
+```c
+lang2_zubr_lval
+```
+
+Если используется общий lexer-код, он должен знать, semantic value какого
+текущего parser следует заполнять. Это уже часть архитектуры приложения, а не
+механизма генерации ZUBR.
+
+---
+
+<a id="controls"></a>
+
+## УПРАВЛЕНИЯ: командная строка
+
+ZUBR управляется опциями командной строки. В текущей версии используются только опции, начинающиеся с `-`.
+
+```text
+zubr [-dlprstv] [other options] [-bFilePrefix] InputFile
+```
+
+Короткие флаги `-d`, `-l`, `-p`, `-r`, `-s`, `-t`, `-v` можно объединять, например:
+
+```sh
+zubr -lvd -o main.c main.y
+```
+
+Опции которым требуется имя или другой параметр, задаются отдельно. После всех опций должен остаться ровно один входной файл спецификации.
+
+<a id="opt-b"></a>
+### `-b[FilePrefix]`, `-b FilePrefix`
+
+Задает префикс стандартных имен выходных файлов. По умолчанию используется префикс `z`.
+
+При стандартных именах:
+
+```text
+z_tab.c основной C-файл
+z_tab.h определения token-ов при -d
+z_code.c код parser-а при -r
+z.output описание таблиц и состояний при -v
+```
+
+например, `-bcalc` преобразует их в `calc_tab.c`, `calc_tab.h`, `calc_code.c`, `calc.output`.
+
+Префикс применяется только к стандартно формируемым именам. Имена, явно заданные `-C`, `-D`, `-R`, `-V` или `-o`, не дополняются `FilePrefix`.
+
+<a id="opt-d"></a>
+### `-d`
+
+Создает заголовочный файл `FilePrefix_tab.h`. В него попадают определения терминалов, соответствующие `%token`, описание `ZUBR_STYPE` и внешняя декларация `zubr_lval` (с учетом `-B`). Заголовок предназначен прежде всего для lexer-а и других модулей, которым нужны те же номера token-ов и тип semantic value.
+
+`-d` имеет приоритет над `-D` в выборе именно стандартного заголовочного файла и отключает эффект `-s` для объектов, которые должны быть доступны из другого файла.
+
+<a id="opt-l"></a>
+### `-l`
+
+Подавляет вывод директив `#line` в генерируемый C/C++-файл. Без `-l` ZUBR использует `#line`, чтобы сообщения компилятора указывали на строки исходной грамматики, содержащие пользовательский C-код.
+
+<a id="opt-r"></a>
+### `-r`
+
+Разделяет генерируемый код на таблицы и parser. Read-only таблицы помещаются в `FilePrefix_tab.c`, а исполняемый код и изменяемые данные — в `FilePrefix_code.c`. Во втором файле для таблиц генерируются `extern`-декларации.
+
+К таблицам относятся, в частности:
+
+```c
+zubr_lhs[]
+zubr_len[]
+zubr_defred[]
+zubr_dgoto[]
+zubr_sindex[]
+zubr_rindex[]
+zubr_gindex[]
+zubr_table[]
+zubr_check[]
+zubr_name[]
+zubr_rule[]
+```
+
+`-r` имеет приоритет над `-R`, `-C`/`-o` и `-s` в тех местах, где эти режимы несовместимы.
+
+<a id="opt-t"></a>
+### `-t`
+
+Включает в генерируемый parser отладочный код. Сам вывод трассы включается во время выполнения через переменную окружения `ZUBR_DEBUG`; см. [раздел об отладке](#zubr-debug).
+
+<a id="opt-v"></a>
+### `-v`
+
+Создает `FilePrefix.output` с правилами, состояниями LALR-автомата, действиями `shift`/`reduce`/`goto`, конфликтами и итоговой статистикой. Этот файл — основной инструмент понимания того, что именно построил ZUBR.
+
+`-v` имеет приоритет над `-V` при выборе стандартного имени.
+
+<a id="opt-p"></a>
+### `-p`
+
+Меняет расширение генерируемых C-файлов с `.c` на `.cpp`. Эта опция **не превращает** сгенерированный текст в специально адаптированный C++-код; меняется только расширение имени файла.
+
+<a id="opt-s"></a>
+### `-s`
+
+По возможности задает `static` для таблиц, parser function и внутренних глобальных объектов сгенерированного parser-а, ограничивая их видимость одним translation unit. Исторически это удобно для небольших встроенных анализаторов, целиком размещаемых вместе с lexer-ом в одном сгенерированном файле.
+
+Режим `static` не может применяться там, где `-d`, `-r`, `-R` или `-D` требуют доступа к объектам из другого файла, поэтому эти опции отменяют соответствующий эффект `-s`. Если `zubr_lval` становится `static`, lexer, обращающийся к ней напрямую, должен находиться в том же translation unit.
+
+<a id="opt-B"></a>
+### `-Bname_prefix`
+
+Добавляет `name_prefix` к служебным именам parser-а, начинающимся с `zubr_`/`ZUBR_`. Для верхнерегистровых имен префикс также переводится в верхний регистр. Например, при:
+
+```sh
+zubr -Blang1_ grammar.y
+```
+
+получаются имена вида:
+
+```text
+lang1_zubr_parse
+lang1_zubr_lex
+lang1_zubr_error
+lang1_zubr_lval
+lang1_zubr_char
+LANG1_ZUBR_STYPE
+LANG1_ZUBR_DEBUG
+LANG1_ZUBR_STACKSIZE
+...
+```
+
+Сама переменная окружения **по-прежнему называется `ZUBR_DEBUG`**: `-B` меняет символы сгенерированного C-кода, а не имя environment variable.
+
+`-B` особенно важна, когда несколько сгенерированных parser-ов одновременно компонуются в одну программу. Явные `-L` и `-P` имеют приоритет над `-B` только для имен lexer/parser соответственно; остальные служебные имена продолжают получать префикс.
+
+<a id="opt-C"></a>
+### `-C[FileName]`
+
+Задает имя основного выходного C/C++-файла. Если используется просто `-C` без присоединенного имени, ZUBR строит имя из имени входного файла, заменяя расширение на `.c` или `.cpp`.
+
+<a id="opt-o"></a>
+### `-o FileName`, `-oFileName`
+
+Современная UNIX-подобная форма задания имени основного выходного файла. По смыслу она выбирает тот же `output_file_name`, что и `-C`, но, в отличие от исторической формы `-CFileName`, допускает обычный отдельный аргумент:
+
+```sh
+zubr -o main.c main.y
+```
+
+`-o` и `-C` одновременно не используются.
+
+<a id="opt-D"></a>
+### `-D[FileName]`
+
+Задает имя заголовочного файла с определениями token-ов и semantic value. Если указано только `-D`, имя строится из имени входного файла с расширением `.h`. `-D` отменяет соответствующий эффект `-s`.
+
+<a id="opt-I"></a>
+### `-Ifilename`
+
+Вместо собственных `#define` для терминалов вставляет:
+
+```c
+#include "filename"
+```
+
+как в основном выходном файле, так и в создаваемом заголовке. Опция полезна, когда несколько parser-ов должны использовать единый набор token numbers. При общем lexer-е надо отдельно решить, в `zubr_lval` какого parser-а помещать semantic value.
+
+<a id="opt-L"></a>
+### `-Lfuncname`
+
+Задает имя сущности, которую сгенерированный parser вызывает для получения следующего token вместо `zubr_lex()`.
+
+```sh
+zubr -Lmain_lex grammar.y
+```
+
+приводит к вызовам вида:
+
+```c
+main_lex();
+```
+
+`funcname` должно быть допустимым C-идентификатором. `-L` имеет приоритет над `-B` только для имени lexer. Особое следствие этой опции — возможность сделать `main_lex` переменной-указателем на функцию и переключать lexer во время работы; это подробно рассмотрено в разделе [«Подмена lexer и parser на ходу»](#runtime-switching).
+
+<a id="opt-P"></a>
+### `-Pfuncname`
+
+Задает **имя функции parser-а, которую ZUBR определяет** вместо `zubr_parse()`:
+
+```sh
+zubr -Pparse_expression grammar.y
+```
+
+Кроме того, создаётся заголовок `parse_expression.h` с декларацией этой функции. `-P` имеет приоритет над `-B` только для имени parser function и снимает `static` именно с этой функции, если одновременно задан `-s`.
+
+Это не то же самое, что `-L`: `-Pmain_parse` создаст функцию `main_parse()`, а не вызов через пользовательский указатель. Разница принципиальна для runtime-переключения parser-ов.
+
+<a id="opt-R"></a>
+### `-R[FileName]`
+
+Включает разделение таблиц и кода, как `-r`, но позволяет явно задать имя файла с исполняемым кодом. Таблицы сохраняют стандартное имя `FilePrefix_tab.c`. Если указано только `-R`, имя code file строится из имени входной спецификации с расширением `.c` или `.cpp`.
+
+`-R` имеет приоритет над `-C`/`-o` и `-s`.
+
+<a id="opt-V"></a>
+### `-V[FileName]`
+
+Задает имя файла, в который выводится информация, обычно создаваемая `-v`. Если задано только `-V`, имя строится из имени входной спецификации с расширением `.output`.
+
+<a id="opt-help"></a>
+### `--help`
+
+Печатает краткую справку по текущей командной строке.
+
+<a id="opt-version"></a>
+### `--version`
+
+Печатает версию ZUBR и завершает работу.
+
+### Приоритеты и совместное использование опций
+
+Исторические короткие режимы специально имеют определённые приоритеты. Практически полезно помнить следующее:
+
+- `-d` выбирает стандартный header и перекрывает `-D`;
+- `-v` выбирает стандартный verbose file и перекрывает `-V`;
+- `-r` выбирает стандартное разделение tables/code и перекрывает `-R` и основной output name;
+- `-R` перекрывает `-C`/`-o` для code file;
+- `-d`, `-D`, `-r`, `-R` отключают применение `-s` там, где требуются внешние связи;
+- `-L` и `-P` перекрывают действие `-B` только для соответствующего имени функции.
+
+Кодировка файлов в ZUBR 4.x не является параметром командной строки: она входит в фиксированный контракт [UTF-8 ↔ UCS-2](#unicode-current).
+
+---
+
+<a id="unicode-current"></a>
+
+## UTF-8 снаружи, UCS-2 внутри
+
+Текущая модель текста ZUBR существенно проще старой многокодировочной реализации.
+
+### Внешняя граница: UTF-8
+
+Все текстовые данные, которые сам генератор ZUBR получает извне или записывает наружу, рассматриваются как UTF-8:
+
+- файл грамматики;
+- имена файлов и параметры командной строки;
+- генерируемые `.c`, `.cpp`, `.h`, `.output`;
+- диагностический текст на внешней границе.
+
+Некорректная UTF-8-последовательность во входной спецификации не должна молча интерпретироваться как другая кодовая страница: это ошибка входного текста.
+
+Именно поэтому старую грамматику в любой прежней однобайтовой кодировке перед использованием с ZUBR 4.x следует **однократно перекодировать в UTF-8**, а не возвращать в генератор многокодировочную логику.
+
+### Внутреннее представление: UCS-2
+
+После чтения UTF-8 ZUBR переводит текст во внутреннее представление `__mpu_char16_t`, то есть UCS-2. Внутренние строки генератора, сравнение имен, таблицы символов и операции над текстом работают через LibMPU/LibMPUIO и UCS-2.
+
+В исходниках самого ZUBR строковые константы для этого представления записываются через:
+
+```c
+MPU_UCS2( "text" )
+```
+
+Макрос разворачивает строковый литерал в подходящую UCS-2 форму (`u"..."`). Пользователь грамматики не обязан писать `u"..."`: файл грамматики остается обычным UTF-8-текстом.
+
+### Русские и другие UCS-2-идентификаторы
+
+Поскольку lexer самого ZUBR получает уже UCS-2-символы, имена терминалов и нетерминалов не обязаны быть только ASCII. Например, допустима грамматика вида:
+
+```yacc
+%token <sym> НОМЕР VAR BLTIN UNDEF
+%type <inst> выражение assign
+%%
+assign:
+ VAR '=' выражение
+ ;
+
+выражение:
+ НОМЕР
+ | BLTIN '(' выражение ')'
+ | выражение '+' выражение
+ ;
+```
+
+Здесь UTF-8 относится к файлу на диске, а сравнение и хранение русских имен внутри ZUBR происходит уже в UCS-2.
+
+### Что эта модель **не** означает для пользовательского lexer-а
+
+Кодировка входной программы, которую разбирает **сгенерированный parser**, не задается ZUBR автоматически. `zubr_lex()` — пользовательская функция. Она может читать байты через libc, UCS-2 через LibMPUIO, уже токенизированный буфер или вообще не иметь текстового входа.
+
+Следовательно, надо различать две независимые границы:
+
+```text
+grammar.y --UTF-8--> ZUBR --UCS-2 internally--> generated C source
+
+program input --> user zubr_lex() --> integer token + zubr_lval --> parser
+```
+
+Первая граница фиксирована самим **ZUBR**. Вторая определяется приложением.
+
+
+### Литералы в грамматике
+
+Обычный непосредственно записанный символ в UTF-8-файле сначала преобразуется в UCS-2 и затем попадает в грамматический литерал. Числовые escape-последовательности работают в том же диапазоне: восьмеричная форма принимает от одной до шести цифр (`\dddddd`), а шестнадцатеричная — ровно четыре цифры (`\xdddd` или `\Xdddd`). Допустимы значения `0x0001`–`0xffff`, кроме суррогатного диапазона `0xd800`–`0xdfff`; значение `0` зарезервировано и как литерал недопустимо.
+
+Таким образом, в ZUBR 4.x нет отдельных режимов выбора кодировки: Unicode-путь является обычным рабочим путем программы.
+
+Однако следует помнить, если код символа велик, то и таблицы, генерируемые анализатором, тоже будут не маленькие.
+
+Например, если в вашей граматике небольшое количество токенов и вы решили использовать в качестве литералов
+русские буквы 'ё', 'Ё', например:
+
+```yacc
+%token NUMBER
+%token NAME
+
+expr:
+ NUMBER
+ | NAME
+ | 'ё' expr 'Ё'
+ | '\x0451' expr '\x0401'
+ ;
+```
+
+то в таблице окажутся токены с номерами 1105 и 1025:
+
+```text
+NUMBER = 257
+NAME = 258
+'Ё' = 1025
+'ё' = 1105
+```
+
+что, естественно, приведет к увеличению таблиц.
+
+---
+
+<a id="examples"></a>
+
+## Примеры программ
+
+В каталоге [zubr](https://ftp.radix-linux.su/zubr/) представлены примеры программ из книги \[[3](#ref-3)\],
+предназначенные для практического знакомства с ZUBR и проверки работы построенных синтаксических анализаторов.
+
+---
+
+<a id="biblio"></a>
+
+## Литература:
+
+1. <a id="ref-1"></a> J. W. Backus,
+ *The Syntax and Semantics of the Proposed International Algebraic
+ Language of the Zurich ACM-GAMM Conference*,
+ Proceedings of the International Conference on Information Processing,
+ UNESCO, Paris, 1959, pp. 125–132. [PDF][backus1959]
+
+2. <a id="ref-2"></a> P. Naur (ed.),
+ *Revised Report on the Algorithmic Language ALGOL 60*,
+ Communications of the ACM, Vol. 6, No. 1,
+ January 1963, pp. 1–17. [DOI][algol60-doi] · [PDF][algol60-pdf]
+
+3. <a id="ref-3"></a> Костельцев А. В.
+ [Построение интерпретаторов и компиляторов: Использование программ BIZON, BYACC, ZUBR](https://search.rsl.ru/ru/record/01000734042?ysclid=mujon27qcy720803550). —
+ СПб.: Наука и Техника, 2001. — 218, [1] с.: ил.; 22 см. — (Конспект программиста). — ISBN 5-94387-033-4.
+
+
+[backus1959]: https://algol60.org/history/ICIP-1959.pdf
+[algol60-doi]: https://doi.org/10.1145/366193.366201
+[algol60-pdf]: https://www.cs.tufts.edu/comp/150FP/archive/peter-naur/algol60.pdf
+
+---
+
+<a id="warranty"></a>
+
+## Без каких-либо гарантий
+
+Автор не несет какой-либо потенциальной ответственности, действительной, или сознательной за содержание и использование данного руководства, а также за последствия применения программы ZUBR.
+
+**E-mail:** <support@radix-linux.su>