Как написать парсер pascal
Pers.narod.ru. Обучение. Пишем парсер арифметических выражений на Паскале и применяем его для построения интерполяционного сплайна
Написать парсер — задача несложная. Гораздо сложней написать хороший парсер, но мы пока такой цели и не ставим. В нашем учебном примере ограничимся разбором и арфиметической оценкой строки на Паскале, зависящей от единственного аргумента, обозначенного x. Писать парсер мы будем тоже на Паскале, поэтому обойдемся только стандартными средствами этого языка. По-хорошему стоило бы сделать всё на Си через структуры и указатели (см., например, парсер выражений из этого моего архива, 4 Кб).
В чем, собственно состоит задача?
У нас есть выражение, записанное по правилам языка Паскаль, например, такое:
Мы хотим сделать с ним всего 2 вещи:
- Проверить формальную синтаксическую правильность этого выражения.
- Вычислить выражение для любого значения x.
Желательно также, чтоб парсер не был слишком капризен к лишней паре скобок, умел понимать числа в любом формате и вылавливал элементарные математические ошибки вроде деления на ноль. Для удобства стоит оформить парсер в виде отдельного модуля, чтоб потом подключать его к своим программам. Поддержка вычислений вида 1+++x — также не проблема, раз Паскаль такое принимает, а 1+-x и -x+1 тем более могут понадобиться.
Будем действовать самым очевидным образом — сначала разделим строку на отдельные лексемы, потом перепишем эти лексемы в массив, проверим правильность цепочки лексем, наконец, после этого будем вычислять выражение по правилам языка, постепенно сокращая массив, пока на выходе не останется единственное число — результат расчета.
Под лексемой я имею в виду «минимальную единицу» выражения — скобку, знак операции, имя функции или число.
В интерфейсной части модуля опишем следующие глобальные данные:
Все остальные лексемы, такие как скобки и знаки операций, будем держать прямо в программе. В строке консоли всего 80 символов, поэтому максимальное число лексем принимаем с запасом равным этому значению. Лексемам будут соответствовать типы, которых намечается шесть:
- (,) — открывающая и закрывающая скобки;
- f — стандартная функция;
- n — число, а также функция без аргументов pi;
- x — аргумент нашего выражения x;
- o — арифметическая операция.
При анализе выражения понадобятся также отдельные метки начала (S) и конца (E) выражения — некоторые лексемы меняют правила, оказываясь в начале или конце выражения. Все эти метки будут помещаться в массив types. Поскольку мы работаем изначально со строкой, получаемые нами числа придется возвращать также в строковые массивы что чревато большими потерями точности при использовании стандартной процедуры str. Поэтому все числа, получаемые при «упаковке» массива лексем будем дополнительно копировать в вещественный массив numbers и брать их для расчета именно оттуда.
Формально правильное выражение не обязательно способно вычислиться — при расчете могут всплыть деление на ноль, извлечение корня из отрицательного числа и другие неприятности. Поэтому введем строку runerror, куда будем писать арифметические ошибки вычисления.
Самое первое, что следует сделать со строкой выражения — убрать лишние пробелы и привести все символы к одному регистру. Следующие 2 функции это и делают, на всякий случай lowcase работает и с кириллицей DOS, понимаемой Паскалем.
Основная функция парсера parse проконтролирует, не пуста ли оставшаяся строка, вызовет функцию проверки и разбора выражения check, и наконец, если выражение верно, вызовет функцию eval, выполняющую расчет. Можно вызывать check и eval независимо друг от друга, если нужно только проверить выражение или вычислить без проверки. Выглядеть функция parse будет следующим образом:
Параметр s передает строку с выражением, x — аргумент, так как все расчеты делаются в вещественных числах, функция вернет значение типа real, параметр e служит для передачи вызывающей программе сообщений об ошибках.
Остаются check и eval. Проверку формальной правильности выражения проще всего разбить на несколько этапов:
- проверить парность и правильное вложение круглых скобок;
- излечь из строки все, какие возможно, числа;
- извлечь знаки операций, стандартные функции и аргумент x.
На этапе 2 нужно внимательно учесть, что знаки «+» и «-» могут обозначать как операцию сложения или вычитания, так и знак числа или аргумента x.
Этот алгоритм расставит метки типов в строку s0 той же размерности, что исходная строка s, содержащая выражение. Для приведенного выше выражения, например, получится:
Для исключения лишних цепочек плюсов и минусов вызывается функция replace_strings, делающая строковые замены:
Однако, выражения типа -x+1 после нее остаются, почему и необходим дополнительный контроль (блок «<лишние + и - из чисел>«).
Функция oper, которая будет вычислять выражения, тоже может получить подобные конструкции после сокращения цепочки лексем, так что постараемся в нее тоже не забыть вставить блок для дополнительного контроля знаков «+» и «-«.
Все неверные лексемы мы помечали подчеркиванием в s0, осталось выбрать их и сообщить, если есть ошибка (продолжение функции check):
Теперь все лексемы помечены буквами типов.
На втором этапе своей работы функция check проверяет, выполняются ли формальные правила следования лексем друг за другом, и заодно наполняет данными массивы lex, types и numbers. Все эти действия выполняются путем вызова подпрограммы get_lexem_arrays:
В свою очередь, get_lexem_arrays вызывает функцию check_rool, передавая ей типы и строковые значения каждой пары соседних лексем. Чтобы отследить начало и конец цепочки, они помечаются специальными лексемами S и E. В check_rool заложены правила следования двух соседних лексем, поэтому ее логика довольно наглядна.
Итак, если все нормально, функция check вернула истину, и можно вызывать функцию eval для вычисления.
Пока в выражении есть нераскрытые скобки, eval будет раскрывать их, обращаясь к рекурсивной функции skobka. Последняя функция, в свою очередь, вызовет функции обслуживания стандартных подпрограмм и арифметических выражений. Попутно функция eval смотрит, не возникло ли глобальной ошибки выполнения runerror и, в случае чего, возвращает значение «ложь», показывающее, что при данном x посчитать формулу нельзя. Когда скобок не осталось, достаточно оценить последнее арифметическое выражение, в которое превратилась цепочка расчетов.
Для оценки количества оставшихся скобочных выражений (непарные и неправильные скобки мы исключили ранее), не мудрствуя лукаво, вызывается функция kol_items.
Функция skobka имеет следующий вид:
Кроме обработки скобок, она вызывает подпрограмму для вычисления стандартных функций func:
и подпрограмму поиска арифметических выражений find_oper:
Если в самых внутренних скобках удалось найти подходящее выражение, оно вычисляется функцией oper.
Эта функция «доводит» свое выражение до одного значения. «Тонкая» операция исключения лишнего + и -, объявившегося перед числом после сокращения цепочки лексем, возложена на процедуру pack_number_sign:
Непосредственный расчет выражения вида A+B делает подпрограмма do_oper:
После вычислений цепочку лексем необходимо «паковать», исключая уже обработанное:
Заодно pack_lexems борется с неточным сохранением чисел стандартной процедурой str, дублируя эти числа в массив numbers.
Наконец, функция number_skob нужна для обработки оставшегося в скобках единственного аргумента
а функция number — просто аргумента без скобок:
При этом функция берет ранее сохраненные в массиве numbers значения.
Остается соединить все в один модуль и получится файл parser.pas, который можно скачать по этой ссылке:
парсер выражений на Паскале (архив *.zip со всеми файлами .pas из статьи и скомпилированным parser.tpu, 12 Кб).
Для тестирования модуля напишем маленький файл parse_me.pas.
Надеюсь, Вы не забыли, что для подключения модуля его нужно иметь скомпилированным в одной из папок, прописанных в настройке EXE & TPU Directory меню Options.Directories Турбо Паскаля. Скомпилировать исходник модуля на диск можно, если открыть в редакторе файл parser.pas, в меню Compile.Destination выставить значение Disk и выполнить команду меню Compile.Build.
Вместо содержимого строки s и выражения, присвоенного переменной rr, можно поставить и любое другое выражение, например,
тоже прекрасно сработает. Кстати, и
теперь наш парсер не смутит.
На практике удобнее один раз проверить с помощью check правильность заданной пользователем функции, а потом уже вычислять ее для разных значений x с помощью eval. Правда, тогда придется снабдить наш модуль дополнительной функцией, возвращающей вызывающей программе данные разбора, такие как переменная clex, массивы lex и types — мы-то все делаем в глобальных переменных и массивах. Но эту задачу я возлагаю на Вас, а в примере ниже буду просто предварительно проверять правильность введенной функции с помощью check, а потом вызывать parse, которая повторит всю ее трудоемкую работу. Лучше было бы вызвать eval напрямую.
Чтобы не быть голословными, давайте построим любимый мной с детства интерполяционный сплайн. Это такая гладкая кривая с неперерывной второй производной, проходящая через набор заданных точек.
Далее приводится листинг файла spline.pas, содержащего подпрограммы для построения интерполяционного сплайна, а также подключающего парсер для того, чтобы построить сплайн по таблице значений произвольной функции. В программе не делаются все проверки корректности, их легко добавить. Обратите внимание на вызов парсера из функции f, а также на ввод строки s в главной программе модуля.
Вот скриншот тестового прогона программы spline.pas:
Если программа найдет ошибку времени исполнения, соответствующее значение f(x) будет вычислено как 0. Например, такое произойдет в первой точке при вводе функции 1-2/x, интервала от 0 до 1 и шага 0.5.
Источник статьи: http://pers.narod.ru/study/pas_parser.html
Парсинг в PascalABC.NET
У меня вопрос: нет ли в PascalABC.Net оператора, процедуры или функции, которая «парсила» бы строку подобно оператору PARSE в языке REXX, вот так (пример исходного кода для REXX):
В итоге получаем результат:
Имя: Вася
Фамилия: Иванов
Получаем (добавляется запятая):
Имя: Вася,
Фамилия: Иванов
Так вот, возвращаясь к теме: нет ли подобного в PascalABC.NET? Если нет, то буду очень благодарен, если кто-то предоставит функцию/процедуру.
В чем разница PascalABC.net и PascalABC
Скажите в чем разница PascalABC.net И PascalABC. Помню когда-то давно программы работающие в.
Установится ли PascalABC.NET на систему, где установлен только .NET 4.6.1?
Привет всем. Вопрос, собственно, озвучен выше. Конкретнее: есть компьютер со свежеустановленной.
Литература по PascalABC.NET
Очень часто новички (особенно те, которые леняться юзать поиск) спрашивают: а существует ли.
PascalABC.NET и Win ХР
Здравствуйте! Подскажите PascalABC.NET совместим с Win ХР? Спасибо.
Решение
Заказываю контрольные, курсовые, дипломные и любые другие студенческие работы здесь.
Объекты в PascalABC.NET
Добрый вечер, уважаемые программисты. Сразу к делу. Я пишу одну программу в которой должно быть не.
Возможности PascalABC.NET
Возможно ли в PascalABC.NET создать web-приложение, например с помощью доп.программ? Если да, то.
Звуки в PascalABC.net
Здравствуйте. Подскажите пожалуйста как реализовать звуки в PascalABC.net ведь в нем нет.
VK.API на PascalABC.NET
Доброго времени суток, написал модуль для PascalABC.NET, с помощью которого можно использовать.
Источник статьи: http://www.cyberforum.ru/pascalabc-net/thread2023796.html
Построение синтаксического дерева программы на языке Паскаль
Содержание
Общие замечания
Для создания компилятора важно понимать следующее. Компилятор имеет несколько внутренних представлений. Вначале текст программы разбирается им в так называемое синтаксическое дерево. Синтаксическое дерево содержит представленный в виде дерева текст программы. При этом могут проверяться семантические ошибки: например, описание двух переменные с одним именем.
Для узлов синтаксического дерева используется библиотека SyntaxTree.dll, заимствованная из проекта PascalABC.NET. Ее необходимо заменить собственной библиотекой (. )
Рассмотрим создание простого языка, являющегося подмножеством языка Pascal.
Что входит в парсер подмножества Паскаля
- Папка DLL, содержащая dll из комплекта PascalABC.NET, необходимые для создания парсера.
- Папка GPLex_GPPG, содержащая генератор компиляторов GPLex+GPPG и документацию к нему.
- Файл generateParserScanner.bat для компиляции файлов .lex и .y в файлы .cs перед компиляцией проекта.
Далее разберем последовательно, как создать парсер.
Лексический анализатор GPLex и синтаксический анализатор GPPG
Итак, наша задача — разобрать текст программы и по нему получить синтаксическое дерево программы. Такой построитель синтаксического дерева программы будем называть парсером или синтаксическим анализатором. Для работы синтаксического анализатора необходим также сканер или лексический анализатор, который разбивает программу на лексемы — неделимые слова. Например, в языке Паскаль лексемами являются ключевые слова, идентификаторы, числовые и строковые константы, знаки операций (:= <> и т.д.).
Синтаксические анализаторы обычно создаются с помощью специальных программ, называемых генераторами компиляторов. Одним из наиболее известных генераторов компиляторов является Yacc (Yet Another Compiler of Compilers) — и в паре с ним работает генератор лексических анализаторов Lex.
Наиболее полной реализацией Yacc-Lex для .NET, генерирующей код парсера на C#, является GPLex+GPPG, разработанный Queensland University of Technology, Австралия. Вот странички продуктов: GPLex и GPPG. Однако рекомендуется скачать исполнимые файлы отсюда — они содержат небольшие модификации, связанные с корректной русификацией.
Создание лексического анализатора
Класс Scanner
Создаваемый в результате компиляции .lex-файла класс Scanner содержит несколько важных методов и свойств. Рассмотрим их подробнее.
- Функция int yylex() возвращает уникальный номер лексемы. Все лексемы хранятся в перечислимом типе Tokens. По-существу, возвращается номер константы в перечислимом типе Tokens — например, для лексемы ID возвращается (int)Tokens.ID
- Помимо уникального номера некоторые лексемы должны возвращать дополнительные значения. Таким значением для идентификатора является его строковое представление, для INTNUM — целое число и т.д. Дополнительное значение для некоторых лексем возвращается в свойстве yylval, которое имеет тип ValueType и содержит ряд полей различных типов — для каждой лексемы может быть предусмотрено своё поле. Например, для лексемы ID предусмотрено поле string sVal, а для лексемы INTNUM — поле iVal.
Объяснение того, как это поле сопоставляется с типом лексемы, отложим до знакомства с содержимым файла .y
- Свойство yytext возвращает строковое представление текущей лексемы
- Свойство yylloc содержит положение лексемы в тексте, задаваемое типом LexLocation из пространства имен QUT.Gppg. Он хранит координаты начала и конца лексемы
Файл .lex
Лексический анализатор обычно создается в файле с расширением .lex. Он имеет вид:
Рассмотрим файл лексического анализатора Oberon00.lex:
После обработки генератором лексических анализаторов GPLex мы получим одноименный cs-файл Oberon00.cs, содержащий класс Scanner лексического анализатора.
Разберем содержимое .lex-файла подробнее.
Секция определений
Вначале рассмотрим первую часть, содержащую определения лексем:
Строка %namespace GPPGParserScanner означает, что класс сканера будет помещен в пространство имен GPPGParserScanner
Строка %using SimplePascalParser; приводит к генерации соответствующей строки в cs-файле. Пространство имен SimplePascalParser полностью находится во вспомогательном файле SimplePascalParserTools.cs, который содержит различные глобальные описания и подпрограммы (подробнее содержимое этого файла будет объяснено позже).
Далее идёт описание некоторых лексем в виде регулярных выражений. Например, целое число INTNUM представляет собой последовательность одной или более цифр
Секция правил
Вторая секция (между первым и вторым %%) является основной и содержит действия, которые необходимо выполнить, когда распознана та или иная лексема.
Здесь приведены действия в случае если встречена та или иная лексема. Большинство действий просто возвращает порядковый номер лексемы в перечислимом типе Tokens.
Последняя лексема — INVISIBLE — символ с кодом 1 — необязательна и является вспомогательной для компилятора выражений языка (необходим для поддержки IntelliSense).
В случае лексемы INTNUM в поле yylval.iVal дополнительно возвращается целое число, соответствующее лексеме: int.Parse(yytext). Обратим внимание, что здесь нет никакой защиты от неправильного преобразования (например, в случае очень длинного целого). Причина — чтобы пока не усложнять изложение обработкой ошибок.
В случае лексемы ID вначале проверяется, не является ли она ключевым словом, и если является, то возвращается целое, соответствующее лексеме ключевого слова (например, для BEGIN возвращается (int)Tokens.BEGIN), а если не является, то возвращается (int)Tokens.ID и параллельно в поле yylval.sVal возвращается строковое представление идентификатора. Именно на этом уровне мы можем задать, чувствителен ли наш язык к регистру (например, преобразованием всех идентификаторов к UpperCase).
Секция правил завершается следующим кодом:
Этот код позволяет для каждой лексемы получать её местоположение в тексте. Вникать в этот код не надо — он просто необходим.
Секция пользовательского кода
Наконец, в секции пользовательского кода содержится единственный переопределенный в классе Scanner метод
Он вызывается всякий раз когда происходит синтаксическая ошибка. Здесь мы пользуемся услугами статического класса PT, который находится в файле SimplePascalParserTools.cs. PT.CreateErrorString(args) формирует строку ошибки, а PT.AddError(errorMsg,yylloc); добавляет ошибку в список ошибок компиляции PascalABC.NET. Здесь интересно то, что в случае автоматического вызова yyerror в args попадают токены, которые ожидались после данного.
Генерация кода лексического анализатора
Для генерации кода лексического анализатора следует выполнить команду:
Здесь gplex.exe — генератор лексических анализаторов, хранящийся в папке GPLex_GPPG, параметр /unicode указывает на то, что созданный лексический анализатор будет распознавать файлы в кодировке unicode (заметим, что он будет распознавать и однобайтные кодировки).
Создание синтаксического анализатора
Общая информация
Синтаксический анализатор — ядро нашего компилятора. Он строится по .y — файлу, в котором записываются правила грамматики языка и действия, которые выполняются по этим правилам. Каждое из действий создает соответствующий узел в синтаксическом дереве. Все узлы синтаксического дерева представляют собой иерархию классов с общим предком syntax_tree_node и описаны во внешней библиотеке SyntaxTree.dll.
Для понимания дальнейшего следует иметь некоторое начальное представление о грамматиках языков программирования, о том, как они порождают языки и об автоматах, которые проверяют, принадлежит ли данная цепочка символов языку, порождаемому грамматикой. Более конкретно, следует представлять, что такое контекстно-свободная грамматика, что Yacc-Lex системы работают с так называемыми LL(k) грамматиками, являющимися достаточно широким подмножеством контекстно-свободных грамматик.
В процессе построения грамматик придется сталкиваться с недостижимыми и циклическими продукциями грамматик, неоднозначными грамматиками. Необходимо уметь исправлять подобные ситуации хотя бы методом проб и ошибок. Особенно необходимо понимать, что Shift-Reduce — конфликты грамматик допустимы и разрешаются в пользу более длинной правой части продукции, а Reduce-Reduce — конфликты недопустимы и свидетельствуют об ошибках проектирования грамматики.
Соответствующую информацию можно прочитать в книге Ахо «Компиляторы: принципы, технологии, инструменты».
Некоторые классы синтаксического дерева
Как уже было сказано, синтаксическое дерево состоит из объектов классов — наследников syntax_tree_node. Перечислим некоторые важные классы — наследники syntax_tree_node, которые будут нами использованы для реализации парсера Паскаля:
Каждый класс имеет свои поля, все необходимые поля заполняются обычно в конструкторе класса. В базовом классе syntax_tree_node имеется поле source_context типа SourceContext, определяющее местоположение конструкции, соответствующей синтаксическому узлу, в тексте программы. Именно поэтому каждый конструктор синтаксического дерева содержит последним параметром объект класса SourceContext.
Правила грамматики
Правила грамматики содержат терминальные символы (терминалы), которые распознаются лексическим анализатором, и нетерминальные символы (нетерминалы). Каждое правило грамматики выражает нетерминал через другие терминалы и нетерминалы. Например, для оператора присваивания имеется следующее правило:
Здесь нетерминал AssignOperator выражается через терминалы ID и ASSIGN и нетерминал expr, который будет определен в другом правиле.
Нетерминал, с которого начинается разбор, называется стартовым символом грамматики. Этот нетерминал описывает всю программу. В нашей грамматике это module, а соответствующее стартовое правило имеет вид:
В конечном итоге все нетерминалы выражаются через терминалы, образующие основную программу. Соответствующий процесс получения программы из стартового символа называется выводом, а правила грамматики — правилами вывода.
В каждом правиле может быть несколько альтернатив, разделяемых символом |:
Правила могут быть рекурсивными. Например, правило для фактических параметров имеет вид:
Обратим внимание, что данное правило является леворекурсивным, поскольку рекурсивное вхождение нетерминала factparams в правую часть правила находится с левой стороны. Для рассматриваемых нами генераторов компиляторов леворекурсивные правила являются предпочтительнее праворекурсивных.
Формат .y-файла
.y-файл имеет такой же формат, как и .lex:
Раздел определений .y-файла
Общий вид
Рассмотрим раздел определений файла SimplePascal.y
Начальные строки
Он состоит из нескольких неравнозначных частей. Первая часть заключена в символы % < %>и содержит код на C#, который будет вставлен в создаваемый класс Parser:
Здесь root — корневой узел синтаксического дерева программы, он явно описан как поле класса GPPGParser. Описываемый во второй строке конструктор носит технический характер — его просто необходимо в этом месте написать. Думаю, что эта строчка связана с неудачным проектированием генератора парсеров GPPG — разработчик забыл в генерируемый класс включить этот нужный конструктор.
означает, что компиляция .y-файла с помощью gppg.exe будет осуществляться в файл oberon00yacc.cs
говорит о том, что класс парсера будет иметь имя GPPGParser
означают подключение в коде парсера соответствующих пространств имен (точки с запятой в конце строк не нужны в отличие от лексического анализатора — видимо, это ошибка проектирования gppg), а строка
означает, что класс парсера будет помещен в пространство имен GPPGParserScanner.
означает, что стартовым в нашей грамматике является нетерминал module (обычно эту строку можно не писать — считается, что стартовым является нетерминал в левой части первого правила).
Описание используемых терминалов
Все используемые терминалы должны быть описаны в секции определений следующим образом:
Именно по этим определениям формируется перечислимый тип Tokens, который встречался в .lex-файле.
Кроме этого, некоторым терминалам и нетерминалам можно задавать тип.
Типы терминалов и нетерминалов
Большинство нетерминалов и некоторые терминалы должны иметь тип. Например, терминал ID имеет тип string, а терминал INTNUM — тип int. Для задания этих типов используют структуру-объединение вида
Если с терминалом или нетерминалом необходимо связать значение некоторого типа, то поле этого типа описывается в структуре union. Например, чтобы связать с терминалом ID значение строкового типа, необходимо описать в структуре union поле
После этого необходимо описать типизированный терминал следующим образом:
Аналогично чтобы связать с нетерминалом Assignment тип statement, необходимо описать в структуре union поле
и после этого связать поле st с нетерминалом Assignment следующим образом:
Полный код описаний для типов терминалов и нетерминалов, а также полей структуры union имеет вид:
Приоритеты операций языка
Чтобы не задавать приоритеты операций в грамматике, в Yacc-системах используется секция задания приоритетов операций. Она имеет вид:
Операции задаются в порядке от самого низкого приоритета до самого высокого.
После такого задания приоритетов операций в грамматике выражений можно писать:
и это не вызовет неоднозначности грамматики.
Секция правил грамматики
Попробуем проследить, как распознаются правила грамматики.
Правило Assignment
Рассмотрим вначале правило для оператора присваивания:
Здесь формируется узел синтаксического дерева для оператора присваивания. assign — это класс — наследник syntax_tree_node, который хранит всю синтаксическую информацию об операторе присваивания: идентификатор в левой части, выражение в правой части, тип оператора присваивания (в данном случае обычное присваивание, есть ещё += *= и т.д.), а также позиция конструкции присваивания в тексте программы. В данной записи $$ обозначает нетерминал в левой части, $1 — первый символ (нетерминал или терминал) в правой части правила, $2 — второй символ в правой части правила и т.д. Таким образом, $$ соответствует символу Assignment, $1 — символу ident, а $3 — символу expr. Очень важно, что если типы соответствующих символов были прописаны в секции описаний, то выражения $$, $1, $2 и т.д. имеют ровно эти типы. Узнаем в разделе описаний типы Assignment, ident и expr:
Теперь заглянём в структуру union:
Таким образом, можно сделать вывод, что $$ имеет тип statement, $1 — тип ident, а $3 — тип expression. Если окажется, что для какого-то нетерминала или терминала не определен тип, то считается, что соответствующий символ $. имеет тип Object.
Наконец, обратим внимание на символ @$. Он соответствует положению в тексте символа Assignment из левой части правила. Тип символа @$ — LexLocation (это стандартный тип библиотеки GPPG), неявно преобразующийся в тип SourceContext (это тип библиотеки PascalABC.NET; можно для простоты считать, что @$ имеет тип SourceContext). Аналогично @1 — это SourceContext для первого символа в правой части, @2 — для второго и т.д.
Как правило, в большинстве узлов @$ будет передаваться в качестве последнего параметра конструктора.
Правило StatementSequence
Правило для StatementSequence имеет вид:
Здесь формируется список операторов, которые в программе разделены точкой с запятой. Обратим внимание, что за счёт леворекурсивности этого правила первым заканчивается разбор первого оператора Statement — в этот момент мы и создаём statement_list вызовом конструктора. В рекурсивной части считается, что statement_list уже создан, и мы добавляем в него следующий Statement с помощью метода Add класса statement_list.
Во втором правиле есть и ещё одна тонкость, рассмотрим её подробнее — она часто встречается. Рассмотрим ещё раз второе правило внимательнее:
Здесь надо понимать, что на момент разбора этого правила StatementSequence в левой части ещё не определен, а StatementSequence в правой части, напротив, определен на предыдущих шагах. Поэтому мы вначале к переменной $1, связанной со StatementSequence в правой части и имеющей тип statement_list, добавляем Statement, хранящийся в $3, после чего инициализируем StatementSequence в левой части, присваивая ему StatementSequence из правой части: $$ = $1. За счёт ссылочной модели объектов в C# здесь можно было бы поступить и иначе:
— всё равно после присваивания $$ = $1 переменные $$ и $1 указывают на один объект.
Правила Statement
Проследим далее за правилами Statement:
Здесь нет действий в <>, поэтому по умолчанию всегда подразумевается действие $$ := $1, что нам и надо.
Рассмотрим ещё несколько правил, в которых есть ранее не встречавшиеся моменты.
Правило ident
Чтобы не преобразовывать всякий раз строковый ID в узел синтаксического дерева ident, введено правило:
Правило для унарного минуса
Правило для унарного минуса имеет вид:
Ключевое слово %prec меняет в рамках одного правила приоритет операции MINUS и делает его таким же, как и у UMINUS. Заметим, что терминал UMINUS — фиктивный — он не может возникнуть при лексическом разборе, и задаётся только в секции приоритетов операций — последним и, значит, самым приоритетным.
Правило для всей программы
Вся программа на Паскале представляет собой модуль:
По этому правилу производятся следующие действия:
Формируется синтаксический узел для всей программы (класс program_module) и присваивается переменной root.