Однопроходный/двухпроходный транслятор с языка математических выражений на язык деревьев вывода
Однопроходный/двухпроходный транслятор с языка математических выражений на язык деревьев вывода
Кафедра ЭВА Отчет по курсовой работе по дисциплине «Системное Программное Обеспечение» на тему «Однопроходный/двухпроходный транслятор с языка математических выражений на язык деревьев вывода. Интерпретатор языка деревьев вывода» Москва 2009 Аннотация Цель данной курсовой работы: - изучение принципов построения трансляторов - написание на языке C++ класса, реализующего следующие действия над математическими выражениями: - лексический анализ - синтаксический анализ - вычисление значения - написание транслятора с языка математических выражений на язык деревьев вывода - написание интерпретатора языка деревьев вывода Теоретическое введениеТеория построения трансляторов используется во многих областях, связанных с программным обеспечением. Важность этой темы можно проиллюстрировать на примере языка высокого уровня C++: для разработки программы на C++ требуется гораздо меньше времени, чем на языках более низкого уровня.
Формальные грамматики
Формальное определение грамматики. Форма Бэкуса-Наура Грамматика - это описание способа построения предложений некоторого языка. Иными словами, грамматика - это математическая система, определяющая язык. Фактически, определив грамматику языка, мы указываем правила порождения цепочек символов, принадлежащих этому языку. Таким образом, грамматика - это генератор цепочек языка. Правило (или продукция) - это упорядоченная пара цепочек символов (?, ?). В правилах важен порядок цепочек, поэтому их чаще записывают в виде ? > ? (или ?::= ?). Такая запись читается как «? порождает ?» или «? по определению есть ?». Грамматика языка программирования содержит правила двух типов: первые (определяющие синтаксические конструкции языка) довольно легко поддаются формальному описанию; вторые (определяющие семантические ограничения языка) обычно излагаются в неформальной форме. Поэтому любое описание (или стандарт) языка программирования обычно состоит из двух частей: вначале формально излагаются правила построения синтаксических конструкций, а потом на естественном языке дается описание семантических правил. Язык, заданный грамматикой G, обозначается как L(G). Две грамматики G и G' называются эквивалентными, если они определяют один и тот же язык: L(G) = L(G'). Две грамматики G и G' называются почти эквивалентными, если заданные ими языки различаются не более чем на пустую цепочку символов: Формально грамматика G определяется как четверка G (VT, VN, P, S), где: VT - множество терминальных символов или алфавит терминальных символов; VN - множество нетерминальных символов или алфавит нетерминальных символов; Р - множество правил (продукций) грамматики, вида , где , S - целевой (начальный) символ грамматики . Алфавиты терминальных и нетерминальных символов грамматики не пересекаются: . Целевой символ грамматики - это всегда нетерминальный символ. Множество называют полным алфавитом грамматики G. Множество терминальных символов VT содержит символы, которые входят в алфавит языка, порождаемого грамматикой. Как правило, символы из множества VT встречаются только в цепочках правых частей правил. Множество нетерминальных символов VN содержит символы, которые определяют слова, понятия, конструкции языка. Каждый символ этого множества может встречаться в цепочках как левой, так и правой частей правил грамматики, но он обязан хотя бы один раз быть в левой части хотя бы одного правила. Во множестве правил грамматики может быть несколько правил, имеющих одинаковые левые части, вида: . Тогда эти правила объединяют вместе и записывают в виде: . Одной строке в такой записи соответствует сразу n правил. Такую форму записи правил грамматики называют формой Бэкуса-Наура. Форма Бэкуса-Наура предусматривает, как правило, также, что нетерминальные символы берутся в угловые скобки: < >. Пример грамматики, определяющей язык целых десятичных чисел со знаком: С({0,1,2. 3,4.5. 6,7.8.9.-,+}, {<число>,<чс>.<цифра>}, Р,<число>) P: <<число> -> <чс> | +<чс> | -<чс> <<чс> -> <цифра> | <чс><цифра> <<цифра> ->0|1|2|3|4|5|6|7|8|9 Рассмотрим составляющие элементы грамматики G: · множество терминальных символов VT содержит двенадцать элементов: десять десятичных цифр и два знака; · множество нетерминальных символов VN содержит три элемента: символы <число>, <чс> и <цифра>; · множество правил содержит 15 правил, которые записаны в три строки (то есть имеется только три различных правых части правил); · целевым символом грамматики является символ <число>. Названия нетерминальных символов не обязаны быть осмысленными. Это сделано для удобства понимания правил грамматики человеком. Например, в программе на языке Pascal можно изменить имена идентификаторов, и при этом не изменится смысл программы. Для терминальных символов это неверно. Набор терминальных символов всегда строго соответствует алфавиту языка, определяемого грамматикой. Принцип рекурсии в правилах грамматики Особенность рассмотренных выше формальных грамматик в том, что они позволяют определить бесконечное множество цепочек языка с помощью конечного набора правил. Приведенная выше в примере грамматика для целых десятичных чисел со знаком определяет бесконечное множество целых чисел с помощью 15 правил. В такой форме записи грамматики возможность пользоваться конечным набором правил достигается за счет рекурсивных правил. Рекурсия в правилах грамматики выражается в том, что один из нетерминальных символов определяется сам через себя. Рекурсия может быть непосредственной (явной) - тогда символ определяется сам через себя в одном правиле, либо косвенной (неявной) - тогда то же самое происходит через цепочку правил. В рассмотренной выше грамматике G непосредственная рекурсия присутствует в правиле: <чс> <чс><цифра>. Чтобы рекурсия не была бесконечной, для участвующего в ней нетерминального символа грамматики должны существовать также и другие правила, которые определяют его, минуя его самого, и позволяют избежать бесконечного рекурсивного определения (в противном случае этот символ в грамматике был бы просто не нужен). Такими правилами являются <чс><цифра> - в грамматике G. В теории формальных языков более ничего сказать о рекурсии нельзя. Но чтобы полнее понять смысл рекурсии, можно прибегнуть к семантике языка - в рассмотренном выше примере это язык целых десятичных чисел со знаком. Рассмотрим его смысл. Если попытаться дать определение тому, что же является числом, то начать можно с того, что любая цифра сама по себе есть число. Далее можно заметить, что любые две цифры - это тоже число, затем - три цифры и т.д. Если строить определение числа таким методом, то оно никогда не будет закончено (в математике разрядность числа ничем не ограничена). Однако можно заметить, что каждый раз, порождая новое число, мы просто дописываем цифру справа (поскольку привыкли писать слева направо) к уже написанному ряду цифр. А этот ряд цифр, начиная от одной цифры, тоже в свою очередь является числом. Тогда определение для понятия «число» можно построить таким образом: «число - это любая цифра либо другое число, к которому справа дописана любая цифра». Именно это и составляет основу правил грамматики G и отражено в правилах <чс> <цифра> | <чс><цифра>. Другие правила в этой грамматике позволяют добавить к числу знак и дают определение понятию «цифра». Принцип рекурсии - важное понятие в представлении о формальных грамматиках. Так или иначе, явно или неявно рекурсия всегда присутствует в грамматиках любых реальных языков программирования. Именно она позволяет строить бесконечное множество цепочек языка. Как правило, в грамматике реального языка программирования содержится не одно, а целое множество правил, построенных с помощью рекурсии. Задача разбора Для каждого языка программирования важно не только уметь построить текст программы на этом языке, но и определить принадлежность имеющегося текста к данному языку. Именно эту задачу решают компиляторы в числе прочих задач (компилятор должен не только распознать исходную программу, но и построить эквивалентную ей результирующую программу). В отношении исходной программы компилятор выступает как распознаватель, а человек, создавший программу на некотором языке программирования, выступает в роли генератора цепочек этого языка. Грамматики и распознаватели - два независимых метода, которые реально могут быть использованы для определения какого-либо языка. Однако при создании компилятора для некоторого языка программирования возникает задача, которая требует связать между собой эти методы задания языков. Разработчиков компиляторов интересуют, прежде всего, синтаксические конструкции языков программирования. Для этих конструкций доказано, что задача разбора для них разрешима. Более того, для них найдены формальные методы ее решения. Поскольку языки программирования не являются чисто формальными языками и несут в себе некоторый смысл (семантику), то задача разбора для создания реальных компиляторов понимается несколько шире, чем она формулируется для чисто формальных языков. Компилятор должен не просто установить принадлежность входной цепочки символов заданному языку, но и определить ее смысловую нагрузку. Для этого необходимо выявить те правила грамматики, на основании которых цепочка была построена. Если же входная цепочка символов не принадлежит заданному языку - исходная программа содержит ошибку, - разработчику программы не интересно просто узнать сам факт наличия ошибки. В данном случае задача разбора также расширяется: распознаватель в составе компилятора должен не только установить факт присутствия ошибки во входной программе, но и по возможности определить тип ошибки и то место во входной цепочке символов, где она встречается. Виды рекурсий и разбора Правила, имеющие вид 1) T-> T * A 2) T-> A * T Называются леворекурсивными и праворекурсивными соответственно. Разбор входной программы может быть как нисходящим так и восходящим. При нисходящем разборе недопустима левая рекурсия в правилах, т. к. она приводит к бесконечному зацикливанию. Левую рекурсию можно преобразовать в эквивалентую праворекурсивную форму следующим образом: T-> A M M-> * A | M При восходящем разборе в правилах недопустима правая рекурсия. Аналогичным образом преобразовываются правила с правой рекурсией в правила с левой рекурсией. Классификация языков и грамматик Чем сложнее язык программирования, тем выше вычислительные затраты компилятора на анализ цепочек исходной программы, написанной на этом языке, а следовательно, сложнее сам компилятор и его структура. Для некоторых типов языков в принципе невозможно построить компилятор, который бы анализировал исходные тексты на этих языках за приемлемое время на основе ограниченных вычислительных ресурсов (именно поэтому до сих пор невозможно создавать программы на естественных языках, например на русском или английском). Классификация грамматик по Хомскому Согласно классификации, предложенной американским лингвистом Ноамом Хомским, формальные грамматики классифицируются по структуре их правил. Если все без исключения правила грамматики удовлетворяют некоторой заданной структуре, то такую грамматику относят к определенному типу. Хомский выделил четыре типа грамматик. Тип 0: грамматики с фразовой структурой На структуру их правил не накладывается никаких ограничений: для грамматики вида G (VT, VN, P, S), правила имеют вид: , где . Это самый общий тип грамматик. В него подпадают все без исключения формальные грамматики, но часть из них, к общей радости, может быть также отнесена и к другим классификационным типам. Дело в том, что грамматики, которые относятся только к типу 0 и не могут быть отнесены к другим типам, являются самыми сложными по структуре. Практического применения грамматики, относящиеся только к типу 0, не имеют. Тип 1: контекстно-зависимые (КЗ) и неукорачивающие грамматики В этот тип входят два основных класса грамматик: Контекстно-зависимые грамматики G (VT, VN, P, S), имеют правила вида: , где Неукорачивающие грамматики G (VT, VN, P, S), имеют правила вида: , где Структура правил КЗ-грамматик такова, что при построении предложений заданного ими языка один и тот же нетерминальный символ может быть заменен на ту или иную цепочку символов в зависимости от того контекста, в котором он встречается. Именно поэтому эти грамматики называют «контекстно-зависимыми». Цепочки и в правилах грамматики обозначают контекст ( - левый контекст, а - правый контекст), в общем случае любая из них (или даже обе) может быть пустой. Неукорачивающие грамматики имеют такую структуру правил, что при построении предложений языка, заданного грамматикой, любая цепочка символов может быть заменена на цепочку символов не меньшей длины. Отсюда и название «неукорачивающие». Доказано, что эти два класса грамматик эквивалентны. При построении компиляторов такие грамматики не применяются, поскольку синтаксические конструкции языков программирования, рассматриваемые компиляторами, имеют более простую структуру и могут быть построены с помощью грамматик других типов. Что касается семантических ограничений языков программирования, то с точки зрения затрат вычислительных ресурсов их выгоднее проверять другими методами, а не с помощью контекстно-зависимых грамматик. Тип 2: контекстно-свободные (КС) грамматики Контекстно-свободные (КС) грамматики G (VT, VN, P, S), имеют правила вида:, где . Такие грамматики также иногда называют неукорачивающими контекстно-свободными (НКС) грамматиками (видно, что в правой части правила у них должен всегда стоять как минимум один символ). Существует также почти эквивалентный им класс грамматик - укорачивающие контекстно-свободные (УКС) грамматики G (VT, VN, P, S), , правила которых могут иметь вид: , где. Разница между этими двумя классами грамматик заключается лишь в том, что в УКС-грамматиках в правой части правил может присутствовать пустая цепочка (X), а в НКС-грамматиках - нет. Доказано, что эти два класса грамматик почти эквивалентны. КС-грамматики широко используются при описании синтаксических конструкций языков программирования. Синтаксис большинства известных языков программирования основан именно на КС-грамматиках. Внутри типа КС-грамматик кроме классов НКС и УКС выделяют еще целое множество различных классов грамматик, и все они относятся к типу 2. Тип 3: регулярные грамматики К типу регулярных относятся два эквивалентных класса грамматик: леволинейные и праволинейные. Леволинейные грамматики G (VT, VN, P, S), могут иметь правила двух видов: или , где . В свою очередь, праволинейные грамматики G (VT, VN, P, S), могут иметь правила тоже двух видов: или , где. Эти два класса грамматик эквивалентны и относятся к типу регулярных грамматик. Регулярные грамматики используются при описании простейших конструкций языков программирования: идентификаторов, констант, строк, комментариев и т.д. Эти грамматики исключительно просты и удобны в использовании, поэтому в компиляторах на их основе строятся функции лексического анализа входного языка. Соотношения между типами грамматик Типы грамматик соотносятся между собой особым образом. Из определения типов 2 и 3 видно, что любая регулярная грамматика является КС-грамматикой, но не наоборот. Также очевидно, что любая грамматика может быть отнесена к типу 0, поскольку он не накладывает никаких ограничений на правила. В то же время существуют укорачивающие КС-грамматики (тип 2), которые не являются ни контекстно-зависимыми, ни неукорачивающими (тип 1), поскольку могут содержать правила вида , недопустимые в типе 1. Одна и та же грамматика в общем случае может быть отнесена к нескольким классификационным типам (например, как уже было сказано, все без исключения грамматики могут быть отнесены к типу 0). Для классификации грамматики всегда выбирают максимально возможный тип, к которому она может быть отнесена. Сложность грамматики обратно пропорциональна номеру типа, к которому относится грамматика.
Трансляторы
Формальное определение транслятора Транслятор - это программа, которая переводит программу на исходном (входном) языке в эквивалентную ей программу на результирующем (выходном) языке. С точки зрения преобразования предложений входного языка в эквивалентные им предложения выходного языка транслятор выступает как переводчик. Результатом работы транслятора будет результирующая программа, но только в том случае, если текст исходной программы является правильным - не содержит ошибок с точки зрения синтаксиса и семантики входного языка. Если исходная программа неправильная (содержит хотя бы одну ошибку), то результатом работы транслятора будет сообщение об ошибке. Этапы трансляции. Общая схема работы транслятора На рис. 2.1 представлена общая схема работы компилятора. Из нее видно, что в целом процесс компиляции состоит из двух основных этапов - анализа и синтеза. На этапе анализа выполняется распознавание текста исходной программы, создание и заполнение таблиц идентификаторов. Результатом его работы служит некое внутреннее представление программы, понятное компилятору. На этапе синтеза на основании внутреннего представления программы и информации, содержащейся в таблице идентификаторов, порождается текст результирующей программы. Результатом этого этапа является объектный код. Кроме того, в составе компилятора присутствует часть, ответственная за анализ и исправление ошибок, которая при наличии ошибки в тексте исходной программы должна максимально полно информировать пользователя о типе ошибки и месте ее возникновения. В лучшем случае компилятор может предложить пользователю вариант исправления ошибки. Эти этапы, в свою очередь, состоят из более мелких этапов, называемых фазами компиляции. Состав фаз компиляции на рис. 2.1 приведен в самом общем виде, их конкретная реализация и процесс взаимодействия могут, конечно, различаться в зависимости от версии компилятора. Компилятор в целом, с точки зрения теории формальных языков, выполняет две основные функции: - распознавание языка исходной программы - генерация результирующей программы на заданном языке. Далее дается перечень основных фаз (частей) компиляции и краткое описание их функций. Лексический анализатор (сканер) - это часть компилятора, которая читает литеры программы на исходном языке и строит из них слова (лексемы) исходного языка. На вход лексического анализатора поступает текст исходной программы, а выходная информация передается для дальнейшей обработки компилятором на этапе синтаксического разбора. С теоретической точки зрения лексический анализатор не является обязательной, необходимой частью компилятора. Однако существуют причины, которые определяют его присутствие практически во всех компиляторах. Синтаксический анализатор - это основная часть компилятора на этапе анализа. Она выполняет выделение синтаксических конструкций в тексте исходной программы, обработанном лексическим анализатором. На этой же фазе компиляции проверяется синтаксическая правильность программы. Синтаксический разбор играет роль распознавателя текста входного языка программирования. Семантический анализатор - это часть компилятора, проверяющая правильность текста исходной программы с точки зрения семантики входного языка. Кроме непосредственно проверки семантический анализ должен выполнять преобразования текста, требуемые семантикой входного языка (например, такие, как добавление функций неявного преобразования типов). В различных реализациях компиляторов семантический анализ может частично входить в фазу синтаксического разбора, частично - в фазу подготовки к генерации кода. Подготовка к генерации кода - это фаза, на которой компилятором выполняются предварительные действия, непосредственно связанные с синтезом текста результирующей программы, но еще не ведущие к порождению текста на выходном языке. Обычно в эту фазу входят действия, связанные с идентификацией элементов языка, распределением памяти и т.п. Генерация кода - это фаза, непосредственно связанная с порождением команд, составляющих предложения выходного языка и в целом текст результирующей программы. Это основная фаза на этапе синтеза результирующей программы. Кроме непосредственного порождения текста результирующей программы генерация обычно включает в себя также оптимизацию - процесс, связанный с обработкой уже порожденного текста. Иногда оптимизацию выделяют в отдельную фазу компиляции, так как она оказывает существенное влияние на качество и эффективность результирующей программы. Проход транслятора - процесс последовательного чтения компилятором данных из памяти, их обработки и помещёния результата в память. В компиляторе может быть реализовано несколько проходов, например проходы лексического и синтаксического анализатора. В некоторых случаях проходы могут быть объединены в один проход. Интерпретаторы Интерпретатор - программа, воспринимающая исходную программу на входном (исходном) языке и выполняющая ее. Интерпретатор, также как и транслятор, анализирует текст исходной программы, но он не порождает результирующую программу, а сразу выполняет исходную в соответствии с ее смыслом, заданным семантикой ее языка. Lex и YaccОбзор генераторов кодаСистемы GNU/Linux поставляются с несколькими программами для написания программ. Возможно наиболее популярны: · Flex, генератор лексического анализатора · Bison, генератор синтаксического анализатора · Gperf, развитый генератор хэш-функции Эти программы генерируют тексты для языка C. Вы можете удивиться, почему они реализованы в виде генераторов кода, а не в виде функций. Тому есть несколько причин: · Входные параметры для этих функций являются очень сложными и их непросто выразить в виде, корректном для C-кода. · Эти программы вычисляют и генерируют много статических таблиц преобразования для операции, следовательно, лучше сгенерировать эти таблицы один раз перед компиляцией, чем при каждом запуске программы. · Многие аспекты функционирования этих систем настраиваются произвольным кодом, помещаемым на отдельные позиции. Этот код может впоследствии использовать переменные и функции, являющиеся частью сгенерированной структуры, построенной генератором кода, без необходимости определять и извлекать все переменные вручную. Каждое из этих инструментальных средств предназначено для создания конкретного типа программ. Bison используется для генерирования синтаксических анализаторов; Flex - для генерирования лексических анализаторов. Другие средства посвящены, в основном, автоматизации конкретных аспектов программирования. Например, интегрирование методов доступа к базе данных в императивные языки программирования часто является рутинной работой. Для ее облегчения и стандартизации предназначен Embedded SQL - система метапрограммирования, используемая для простого комбинирования доступа к базе данных и C. Хотя существует немало доступных библиотек, позволяющих обращаться к базам данных в C, использование такого генератора кода как Embedded SQL делает комбинирование C и доступа к базе данных намного более легким путем объединения SQL-сущностей в C в качестве расширения языка. Многие реализации Embedded SQL, однако, в основном являются простыми специализированными макропроцессорами, генерирующими обычные C-программы. Тем не менее, использование Embedded SQL делает для программиста доступ к базе данных более естественным, интуитивным и свободным от ошибок по сравнению с прямым использованием библиотек. При помощи Embedded SQL запутанность программирования баз данных маскируется макроязыком Совместное использование Lex и YaccДо 1975 года процесс написания компиляторов занимал очень много времени. Затем Lesk[1975] и Johnson[1975] опубликовали труды по lex и yacc. Эти утилиты сильно упростили написание компиляторов. Детали реализации могут быть найдены у Aho[1986]Шаблоны кода помещаются на вход Lex. Lex читает шаблоны и генерирует C код для лексического анализатора или сканера.Лексический анализатор ищет совпадение строк во входных данных, основываясь на заданных шаблонах, и преобразует строки в токены.Токены являются числовым представлением строк упрощающим обработку.Когда лексический анализатор находит идентификаторы во входном потоке, они вносятся в таблицу символов. Таблица символов также может содержать другую информацию такую, как тип данных (целый или вещественный) и место переменной в памяти. Все последующие ссылки на идентификаторы ссылаются на соответствующий индекс таблицы символов.Код грамматики подаются на вход yacc. Yacc читает грамматику и генерирует C код для синтаксического анализатора или разборщика. Синтаксический анализатор использует грамматические правила, которые позволяют ему анализировать токены из лексического анализатора и создать синтаксическое дерево. Синтаксическое дерево устанавливает иерархичскую структуру токенов. Например, оператор precedence и ассоциативности (associativity) показаны в синтаксическом дереве. Следующий шаг, генерация кода, осуществляется с помощью обхода синтаксического дерева. Некоторые компиляторы создают машинный код, когда как некоторые - программу на языках ассемблера.Команды для создания компилятора, bas.exe, приведены ниже:yacc - d bas.y # create y.tab.h, y.tab.clex bas.l # create lex.yy.ccc lex.yy.c y.tab.c - obas.exe # compile/linkYacc читает грамматические описания в bas.y и генерирует синтаксический анализатор (parser), который включает функцию yyparse, в файле y.tab.c. Файл bas.y содержит в себе объявления токенов. Включение опции - d ведет к тому, что yacc генерирует определения для токенов и помещает их в файл y.tab.h. Lex читает шаблоны, описанные в bas.l, включающие файл y.tab.h и генерирует лексический анализатор, который включает функцию yylex, в файле lex.yy.c. Наконец, лексический анализатор (lexer) и синтаксический анализатор (parser) компилируются и компонуются вместе, образуя исполняемый файл bas.exe. Из main, мы вызываем yyparse, чтобы запустить компилятор. Функция yyparse автоматически вызывает yylex, чтобы получить каждый токен.Lex Theory Первая фаза в компиляторе - чтение входного исходного файла и его преобразование в токены. Используя регулярные выражения, мы можем специфицировать шаблоны для лексического анализа, и это позволит сгенерировать код, который позволит сканировать и найти совпадающие строки в исходном коде. Каждому шаблону специфицированному в исходном коде для лексического анализа, сопоставляется ассоциированное действие. Обычно действие возвращает токен, который представляет совпадающую строку, в последующем используемую синтаксическим анализатором. Сначала просто печатаем совпавшую строку, если возвращается значение токена. Далее следует представление простого шаблона, составляющего регулярное выражение, которое ищет идентификаторы. Lex читает этот шаблон и создает C код для лексического анализатора, который ищет идентификаторы. letter (letter|digit)* Этот шаблон ищет строку символов, которая начинается с единичного символа, следующим за нулем или больше символов или цифр. Этот пример хорошо иллюстрирует, операции, разрешенные а регулярных выражениях: * повторение, представленное оператором «*» (repetition) * чередование, представленное оператором «|» (alternation) * объединение (concatenation) Любое регулярное выражение может быть представлено автоматом с конечным числом состояний (finite state automaton, FSA). Мы можем представить FSA, использующее состояния и переходы между состояниями. Существует одно начальное состояние и одно, или больше, конечных состояний или разрешенных состояний. На рис. 3, состояние 0 - является начальным состоянием, а состояние 2 - разрешенным состоянием. Когда происходит чтение символа, осуществляется переход из одного состояния в другое. Когда читается первый символ, осуществляется переход в состояние 1. Автомат остается в состоянии 1, пока читаются буквы (letters) или цифры (digits). Когда осуществляется чтение иного символа, кроме буквы или символа, осуществляется переход в состояние 2, разрешенное состояние. Любой FSA может быть представлен с помощью компьютерной программы. Например, этот автомат с 3 мя состояниями программируется следующим образом: start: goto state0 state0: read c if c = letter goto state1 goto state0 state1: read c if c = letter goto state1 if c = digit goto state1 goto state2 state2: accept string Это техника, используемая lex. Регулярные выражения транслируются с помощью lex в компьютерную программу, которая реализует FSA. Используя следующий входной символ и текущее состояние, следующее состояние определяется с помощью индексирования в сгенерированной компьютером таблице состояний. Теперь становится легко понять ограничения в lex. Например, lex не может быть использован. Таблица 1. Элементарные шаблоны (Pattern Matching Primitives) |
Метасимвол (Metacharacter) | Совпадения (Matches) | | . | Любой символ, кроме перевода строки | | \n | Символ перевода строки | | * | 0 или более копий предшествующих выражений | | + | 1 или более копий предшествующих выражений | | ? | 0 или 1 копия предшествующих выражений | | ^ | Начало строки | | $ | Конец строки | | a|b | a или b | | (ab)+ | Одна или более копий ab (группировка, grouping) | | «a+b» | литерал «a+b» (C escapes still work) | | [] | Класс символов | | |
Таблица 2. Примеры шаблонов выражений (Pattern Matching Examples) |
Выражение (Expression) | Совпадения (Matches) | | abc | abc | | abc* | ab abc abcc abccc… | | abc+ | abc abcc abccc… | | a(bc)+ | abc abcbc abcbcbc… | | a(bc)? | a abc | | [abc] | Одно из: a, b, c | | [a-z] | Любой символ, a-z | | [a\-z] | Одно из: a, -, z | | [-az] | Одно из: -, a, z | | [A-Za-z0-9]+ | Один или более символов алфавита или цифр | | [\t\n]+ | Пробельные символы | | [^ab] | Все, кроме: a, b | | [a^b] | Одно из: a, ^, b | | [a|b] | Одно из: a, |, b | | a|b | Одно из: a, b | | |
Регулярные выражения в lex составляются из метасимволов (Таблица 1). Примеры совпадения шаблонов показаны в таблице 2. При использовании класса символов, обычные операторы теряют свое назначение. Следующие два оператора разрешены в классе символов: дефис («-», hyphen) и циркумфлекс («^», circumflex). При использовании между двумя символами дефиса, представляется диапазон символов. Циркумфлекс, при использовании его как первого символа, отрицает выражение. Если два шаблона совпадают с некоторой строкой, используется наиболее шаблон, по которому найдена наиболее длинная строка, в случае, если длина одинакова, используется первый шаблон. … definitions… %% … rules… %% … subroutines… Входные данные lex делятся на 3 секции, с символами%%, разделяющими секции. Это проиллюстрировано в примере. Первый пример - это наименьший возможный файл lex: %% Входные данные копируются выходные по одному символу за раз. Первый разделитель%% требуется всегда, так как всегда должна быть секция правил. Если не специфицировать ни одного правила, тогда действие по умолчанию - совпадение всего и копирование в выходные данные. По умолчанию для входных и выходных данных используются stdin и stdout, соответственно. Вот некоторый пример, с использованием кода по умолчанию: %% /* Совпадение всего, кроме символа новой строки */ ECHO; /* Совпадение символа перевода строки */ \n ECHO; %% int yywrap(void) { return 1; } int main(void) { yylex(); return 0; } Два шаблона специфицированы в секции правил. Каждый шаблон должен начинаться в первом столбце, то есть должен следовать за пробельным символом. Опциональное действие ассоциируется с шаблоном. Действие может быть единичным выражением на языке C или множественным, заключенным в скобки. Все, не начинающееся с первого столбца, дословно копируется в генерируемый C файл. Можно использовать это для спецификации комментариев в lex файле. В этом примере есть 2 выражения: «.» и «\n» с действием ECHO, ассоциированным с каждым шаблоном. Несколько макросов и переменных определены в lex. ECHO - это макрос, который пишет код, совпадающий с шаблоном. Это действие по умолчанию для каждой несовпадающей строки. Обычно ECHO определяется как
Страницы: 1, 2
|