Лексический анализ. Лексический разбор слова - что это такое? Примеры

программа прежде всего разбивается на последовательность строк, или, как принято говорить, лексем (lexeme) . Множество лексем разбивается на непересекающиеся подмножества (лексические классы). Лексемы попадают в один лексический класс , если они неразличимы с точки зрения синтаксического анализатора. Например, во время синтаксического анализа все идентификаторы можно считать одинаковыми.

Размеры лексических классов различны. Например, лексический класс идентификаторов, вообще говоря, бесконечен. С другой стороны, есть лексические классы, состоящие только из одной лексемы, например, подмножество , состоящее из лексемы if . В большинстве языков программирования имеются следующие лексические классы: ключевые слова ( по одному на каждое ключевое слово ), идентификаторы, строковые литералы, числовые константы . Каждому подмножеству сопоставляется некоторое число, называемое идентификатором лексического класса (token) или, короче, лексическим классом .

Пример . Рассмотрим оператор языка Pascal const pi = 3.1416; Этот оператор состоит из следующих лексем:

  • сonst - лексический класс Const_LC
  • pi - лексический класс Identifier_LC
  • = - лексический класс Relation_LC
  • 3.1416 - лексический класс Number_LC
  • ; - лексический класс Semicolon_LC

Лексический анализ различных языков программирования

Некоторые языки обладают особенностями, существенно затрудняющими лексический анализ . Такие языки, как Фортран и Кобол, требуют размещения конструкций языка в фиксированных позициях входной строки. Такое размещение лексем могло быть очень важным при выяснении корректности программы. Например, при переносе строки в Коболе необходимо поставить специальный символ в 6-й колонке, иначе следующая строка будет разобрана неправильно. Основной тенденцией современных языков программирования является свободное размещение текста программы.

От одного языка к другому варьируются правила использования символов языка, в частности, пробелов. В некоторых языках, таких как Алгол 68 и Фортран, пробелы являются значащими только в строковых литералах. Рассмотрим популярный пример, иллюстрирующий потенциальную сложность распознавания лексем в Фортране. В операторе DO 5 I = 1.25 мы не можем определить, что DO не является ключевым словом до тех пор, пока не встретим десятичную точку.

С другой стороны, в операторе DO 5 I = 1,25 мы имеем семь лексем: ключевое слово DO , метку 5 , идентификатор I , оператор = , константу 1 , запятую и константу 25 . Причем, до тех пор пока мы не встретим запятую, мы не можем быть уверены в том, что DO - это

При разборе такого оператора необходимо постоянно переключаться с режима " THEN , ELSE как ключевые слова" на трактовку " THEN , ELSE как идентификаторы", и обратно.

В последней главе я оставил вас с компилятором который должен почти работать, за исключением того, что мы все еще ограничены односимвольными токенами. Цель этого урока состоит в том, чтобы избавиться от этого ограничения раз и навсегда. Это означает, что мы должны иметь дело с концепцией лексического анализатора (сканера).

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

Единственная причина, на самом деле, имеет отношение к ключевым словам. Это факт компьютерной жизни, что синтаксис ключевого слова имеет ту же самую форму, что и синтаксис любого другого идентификатора. Мы не можем сказать пока не получим полное слово действительно ли это ключевое слово. К примеру переменная IFILE и ключевое слово IF выглядят просто одинаковыми до тех пор, пока вы не получите третий символ. В примерах до настоящего времени мы были всегда способны принять решение, основанное на первом символе токена, но это больше невозможно когда присутствуют ключевые слова. Нам необходимо знать, что данная строка является ключевым словом до того, как мы начнем ее обрабатывать. И именно поэтому нам нужен сканер.

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

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

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

Лексический анализ

Лексический анализ – это процесс сканирования потока входных символов и разделения его на строки, называемые лексемами. Большинство книг по компиляторам начинаются с этого и посвящают несколько глав обсуждению различных методов построения сканеров. Такой подход имеет свое место, но, как вы уже видели, существуют множество вещей, которые вы можете сделать даже никогда не обращавшись к этому вопросу, и, фактически, сканер, который мы здесь закончим, не очень будет напоминать то, что эти тексты описывают. Причина? Теория компиляторов и, следовательно, программы следующие из нее, должны работать с большинством общих правил синтаксического анализа. Мы же не делаем этого. В реальном мире возможно определить синтаксис языка таким образом, что будет достаточно довольно простого сканера. И как всегда KISS – наш девиз.

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

Зачем необходимо разделение? Ответ имеет и теоретическую и практическую основы.

В 1956 Ноам Хомский определил «Иерархию Хомского» для грамматик. Вот они:

Тип 0. Неограниченные (например Английский язык)

Тип 1. Контекстно-зависимые

Тип 2. Контекстно-свободные

Тип 3. Регулярные.

Некоторые характеристики типичных языков программирования (особенно старых, таких как Фортран) относят их к Типу 1, но большая часть всех современных языков программирования может быть описана с использованием только двух последних типов и с ними мы и будем здесь работать.

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

Аналогично грамматики Типа 2 (контекстно-свободные) всегда могут быть анализированы с использованием магазинного автомата (конечный автомат, дополненный стеком). Мы также реализовывали эти машины. Вместо реализации явного стека для выполнения работы мы положились на встроенный стек связанный с рекурсивным кодированием и это фактически является предочтительным способом для нисходящего синтаксического анализа.

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

::= [ | ]*

Так как требуется различные виды абстрактных машин для анализа этих двух типов грамматик, есть смысл отделить эти низкоуровневые функции в отдельный модуль, лексический анализатор, который строится на идее конечного автомата. Идея состоит в том, чтобы использовать самый простой метод синтаксического анализа, необходимый для работы.

Имеется другая, более практическая причина для отделения сканера от синтаксического анализатора. Мы хотим думать о входном исходном файле как потоке символов, которые мы обрабатываем справа налево без возвратов. На практике это невозможно. Почти каждый язык имеет некоторые ключевые слова типа IF, WHILE и END. Как я упомянул ранее, в действительности мы не можем знать является ли данная строка ключевым словом до тех пор пока мы не достигнем ее конца, что определено пробелом или другим разделителем. Так что мы должны хранить строку достаточно долго для того, чтобы выяснить имеем мы ключевое слово или нет. Это ограниченная форма перебора с возвратом.

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

Конечные автоматы и альтернативы

Я упомянул, что регулярные выражения могут анализироваться с использованием конечного автомата. В большинстве книг по компиляторам а также в большинстве компиляторов, вы обнаружите, что это применяется буквально. Обычно они имеют настоящую реализацию конечного автомата с целыми числами, используемыми для определения текущего состояния и таблицей действий, выполняемых для каждой комбинации текущего состояния и входного символа. Если вы пишите «front end» для компилятора, используя популярные Unix инструменты LEX и YACC, это то, что вы получите. Выход LEX – конечый автомат, реализованный на C плюс таблица действий, соответствующая входной грамматике данной LEX. Вывод YACC аналогичен... исскуственный таблично-управляемый синтаксический анализатор плюс таблица, соответствующая синтаксису языка.

Однако это не единственный вариант. В наших предыдущих главах вы много раз видели, что возможно реализовать синтаксические анализаторы специально не имея дела с таблицами, стеками и переменными состояния. Фактически в пятой главе я предупредил вас, что если вы считает себя нуждающимся в этих вещах, возможно вы делаете что-то неправильно и не используете возможности Паскаля. Существует в основном два способа определить состояние конечного автомата: явно, с номером или кодом состояния и неявно, просто на основании того факта, что я нахожусь в каком-то определенном месте кода (если сегодня вторник, то это должно быть Бельгия). Ранее мы полагались в основном на неявные методы, и я думаю вы согласитесь, что они работают здесь хорошо.

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

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

Эксперименты по сканированию

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

Давайте начнем с двух определений, наиболее часто встречающихся в настоящих языках программирования:

::= [ | ]*

]+

(Не забудьте, что "*" указывает на ноль или более повторений условия в квадратных скобках, а "+" на одно и более.)

Мы уже работали с подобными элементами в третьей главе. Давайте начнем (как обычно) с пустого Cradle. Не удивительно, что нам понадобится новая процедура распознавания:

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

{ Get an Identifier }

function GetName: string;

while IsAlNum(Look) do begin

x:= x + UpCase(Look);

{ Get a Number }

function GetNum: string;

while IsDigit(Look) do begin

(Заметьте, что эта версия GetNum возвращает строку, а не целое число, как прежде).

Вы можете легко проверить что эти подпрограммы работают, вызвав их из основной программы:

WriteLn(GetName);

Эта программа выведет любое допустимое набранное имя (максимум восемь знаков, потому что мы так сказали GetName). Она отвергнет что-либо другое.

Аналогично проверьте другую подпрограмму.

Пробел

Раньше мы также работали с вложенными пробелами, используя две подпрограммы IsWhite и SkipWhite. Удостоверьтесь, что эти подпрограммы есть в вашей текущей версии Cradle и добавьте строку:

в конец GetName и GetNum.

Теперь давайте определим новую процедуру:

{ Lexical Scanner }

Function Scan: string;

if IsAlpha(Look) then

else if IsDigit(Look) then

Мы можем вызвать ее из новой основной программы:

{ Main Program }

until Token = CR;

(Вы должны добавить описание строки Token в начало программы. Сделайте ее любой удобной длины, скажем 16 символов).

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

Конечные автоматы

Подпрограмма анализа типа GetName действительно реализует конечный автомат. Состояние неявно в текущей позиции в коде. Очень полезным приемом для визуализации того, что происходит, является синтаксическая диаграмма или «railroad-track» диаграмма. Немного трудно нарисовать их в этой среде, поэтому я буду использовать их очень экономно, но фигура ниже должна дать вам идею:

Как вы можете видеть, эта диаграмма показывает логические потоки по мере чтения символов. Начинается все, конечно, с состояния «start» и заканчивается когда найден символ, отличный от алфавитно-цифрового. Если первый символ не буква, происходит ошибка. Иначе автомат продолжит выполнение цикла до тех пор, пока не будет найден конечный разделитель.

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

Из-за сложностей представления «railroad-track» диаграмм в этой среде я буду продолжать придерживаться с этого времени синтаксических уравнений. Но я настоятельно рекомендую вам диаграммы для всего, что включает синтаксический анализ. После небольшой практики вы можете начать видеть, как написать синтаксический анализатор непосредственно из диаграммы. Параллельные пути кодируются в контролирующие действия (с помощью операторов IF или CASE), последовательные пути – в последовательные вызовы. Это почти как работа по схеме.

Мы даже не обсудили SkipWhite, которая была представлена раньше, но это также простой конечный автомат, как и GetNum. Так же как и их родительская процедура Scan. Маленькие автоматы образуют большие автоматы.

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

Новые строки

Продвигаясь прямо вперед, давайте модифицируем наш сканер для поддержки более чем одной строки. Как я упомянул последний раз, наиболее простой способ сделать это – просто обработать символы новой строки, возврат каретки и перевод строки, как незаполненное пространство. Фактически это способ, используемый подпрограммой iswhite из стандартной библиотеки C. Прежде мы не этого делали. Я хотел бы сделать это теперь, чтобы вы могли почувствовать результат.

Чтобы сделать это просто измените единственную выполнимую строку в IsWhite:

IsWhite:= c in [" ", TAB, CR, LF];

Мы должны дать основной программы новое условие останова, так как она никогда не увидит CR. Давайте просто используем:

until Token = ".";

ОК, откомпилируйте эту программу и запустите ее. Попробуйте пару строк, завершаемых точкой. Я использовал:

for all good men.

Эй, что случилось? Когда я набрал это, я не получил последний токен, точку. Программа не остановилась. Более того, когда я нажал клавишу "enter" несколько раз, я все равно не получил точку.

Если вы все еще не можете выбраться из вашей программы, вы обнаружите, что набор точки в новой строке прервет ее.

Что здесь происходит? Ответ в том, что мы зависаем в SkipWhite. Короткий осмотр этой подпрограммы покажет, что пока мы печатаем пустые строки, мы просто продолжаем выполнение цикла. После того, как SkipWhite встречает LF, он пытается выполнить GetChar. Но так как входной буфер теперь пуст, оператор чтения в GetChar настаивает на наличии другой строки. Процедура Scan получает завершающую точку, все правильно, но она вызывает SkipWhite и SkipWhite не возвращается до тех пор, пока не получит непустую строку.

Такое поведение не настолько плохое, как кажется. В настоящем компиляторе мы читали бы символы из входного файла вместо консоли и пока мы имеем какую-то процедуру для работы с концом файла, все получится ОК. Но для чтения данных с консоли такое поведение слишком причудливое. Суть в том, что соглашение C/Unix просто не совместимо со структурой нашего анализатора, который запрашивает предсказывающий символ. Код, который мастера из Bell реализовали, не использует это соглашение, поэтому они нуждаются в "ungetc".

ОК, давайте исправим проблему. Чтобы сделать это, мы должны возвратиться к старому определению IsWhite (удалите символы CR и LF) и используйте процедуру Fin, которую я представил в последний раз. Если ее нет в вашей текущей версии Cradle, поместите ее там.

Также измените основную программу следующим обраазом:

{ Main Program }

if Token = CR then Fin;

until Token = ".";

Обратите внимание на «охраняющую» проверку, предшествующую вызову Fin. Это то, что заставляет все это работать, и проверяет, то мы не пытаемся прочитать строку дальше.

Сейчас испытайте этот код. Я думаю он понравится вам больше.

Если вы обратитесь к коду, который мы написали в последней главе, вы обнаружите, что я расставил вызовы Fin по всему коду, где прерывание строки было бы уместным. Это одна из тех областей, которые действительно влияют на восприятие, о котором я упомянул. В этой точке я должен убедить вас поэкспериментировать с различными способами организациями и посмотреть, как вам это понравится. Если вы хотите, чтобы ваш язык был по настоящему свободного стиля, тогда новые строки должны быть прозрачны. В этом случае наилучшим подходом было бы поместить следующие строки в начале Scan:

while Look = CR do

Если, с другой стороны, вам нужен строчно-ориентированный язык подобный Ассемблеру, BASIC или FORTRAN (или даже Ada... заметьте, что он имеет комментарии, завершаемые новой строкой), тогда вам необходимо, чтобы Scan возвращал CR как токены. Он также должен съедать завершающие LF. Лучший способ сделать – использовать эту строку в самом начале Scan:

if Look = LF then Fin;

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

Операторы

Мы могли бы сейчас остановиться и иметь в своем распоряжении довольно полезный сканер. В тех фрагментах KISS, которые мы построили, единственными токенами, состоящими из нескольких символов, являются идентификаторы и числа. Все операторы были односимвольными. Единственное исключение, которое я могу придумать – это операторы отношений «<=», «>=» и «<>», но они могут быть обработаны как особые случаи.

Однако другие языки имеют многосимвольные операторы такие как «:=» в Паскале или «++» и «>>» в C. Хотя пока нам и не нужны многосимвольные операторы, было бы хорошо знать как получить их в случае необходимости.

Само собой разумеется, что мы можем обрабатывать операторы точно таким же способом, что и другие токены. Давайте начнем с подпрограммы распознавания:

{ Recognize Any Operator }

function IsOp(c: char): boolean;

IsOp:= c in ["+", "-", "*", "/", "<", ">", ":", "="];

Важно заметить, что мы не должны включать в этот список каждый возможный оператор. К примеру круглые скобки не включены, так же как и завершающая точка. Текущая версия Scan и так хорошо поддерживает односимвольные операторы. Список выше включает только те символы, которые могут появиться в многосимвольных операторах. (Для конкретных языков список конечно всегда может быть отредактирован).

Теперь давайте изменим Scan следующим образом:

{ Lexical Scanner }

Function Scan: string;

while Look = CR do

if IsAlpha(Look) then

else if IsDigit(Look) then

else if IsOp(Look) then

Теперь испытайте программу. Вы убедитесь, что любые фрагменты кода, которые вы захотите бросить в нее будут аккуратно разложены на индивидуальные токены.

Списки, запятые и командные строки

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

Сколько раз вы работали с программой или операционной системой, которая имела жесткие правила того, как вы должны разделять элементы в списке? (Попробую, последний раз вы использовали MS DOS!). Некоторые программы требуют пробелов как разделителей, некоторые требуют запятые. Хуже всего, что некоторые требуют и того и другого в разных местах. Большинство довольно неумолимы к нарушениям их правил.

Я думаю, это непростительно. Слишком просто написать синтаксически анализатор, который поддерживает и пробелы и запятые гибким способом. Рассмотрите следующую процедуру:

{ Skip Over a Comma }

procedure SkipComma;

if Look = "," then begin

Эта процедура из восьми строк пропустит разделитель, состоящий из любого числа (включая ноль) пробелов, с нулем или одной запятой, вложенной в строку.

Временно измените вызов SkipWhite в Scan на вызов SkipComma и попробуйте ввести какие-нибудь списки. Хорошо работает, да? Разве вы не хотите, чтобы больше создателей программ знало о SkipComma?

К слову сказать, я обнаружил, что добавление эквивалента SkipComma в мою программу на ассемблере для Z80 заняло всего шесть дополнительных байт кода. Даже на 64K машинах это не слишком большая цена за дружелюбие к пользователю.

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

Я утверждаю, что у вас будет лучший, более дружественный интерфейс если вы потратите время на то, чтобы определить синтаксис явно. Запишите синтаксические уравнения или нарисуйте «railroad-track» диаграммы и закодируйте синтаксический анализатор используя методы, которые я показал вам здесь. Вы получите более хорошую программу и ее будет проще писать, в придачу.

Хорошо, сейчас мы имеем довольно хороший лексический анализатор, который разбивает входной поток на лексемы. Мы могли бы использовать его как есть и иметь полезный компилятор. Но есть некоторые другие аспекты лексического анализа, которые мы должны охватить.

Особенно следует рассмотреть (вздрогните) эффективность. Помните, когда мы работали с односимвольными токенами, каждой проверкой было сравнение одного символа Look с байтовой константой. Мы также использовали в основном оператор Case.

С многосимвольными лексемами, возвращаемыми Scan, все эти проверки становятся сравнением строк. Гораздо медленнее. И не только медленнее но и неудобней, так как в Паскале не существут строкового эквивалента оператора Case. Особенно расточительным кажется проверять то что состоит из одного символа... "=", "+" и другие операторы... используя сравнение строк.

Сравнение строк не является невозможным. Рон Кейн использовал этот подход при написании Small C. Так как мы придерживаемся принципа KISS мы были бы оправданы согласившись с этим подходом. Но тогда я не смог бы рассказать вам об одном из ключевых методов, используемых в «настоящих» компиляторах.

Вы должны запомнить: лексический анализатор будет вызываться часто! Фактически один раз для каждой лексемы во всей исходной программе. Эксперименты показали, что средний компилятор тратит где-то от 20 до 40 процентов своего времени на подпрограммах лексического анализа. Если существовало когда-либо место, где эффективность заслуживает пристального рассмотрения, то это оно.

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

Первое, что нам нужно – это способ идентификации ключевых слов. Мы всегда можем сделать это с помощью последовательных проверок IF, но несомненно было бы хорошо, если бы мы имели универсальную подпрограмму, которая могла бы сравнивать данную строку с таблицей ключевых слов. (Между прочим, позднее нам понадобится такая же подпрограмма для работы с таблицей идентификаторов). Это обычно выявляет проблему Паскаля, потому что стандартный Паскаль не имеет массивов переменной длины. Это настоящая головная боль – обьявлять различные подпрограммы поиска для каждой таблицы. Стандартный Паскаль также не позволяет инициализировать массивы, поэтому вам придется видеть код типа:

Table := "ELSE";

Table[n] := "END";

что может получиться довольно длинным если есть много ключевых слов.

К счастью Turbo Pascal 4.0 имеет расширения, которые устраняют обе эти проблемы. Массивы-константы могут быть обьявлены с использованием средства TP «типизированные константы» а переменные размерности могут быть поддержаны с помощью Си-подобных расширений для указателей.

Сначала, измените ваши объявления подобным образом:

{ Type Declarations }

type Symbol = string;

TabPtr = ^SymTab;

(Размерность, использованная в SymTab не настоящая... память не распределяется непосредственно этим объявлением, а размерность должна быть только «достаточно большой»)

Затем, сразу после этих объявлений, добавьте следующее:

Затем, вставьте следующую новую функцию:

{ Table Lookup }

{ If the input string matches a table entry, return the entry

index. If not, return a zero. }

while (i > 0) and not found do

if s = T^[i] then

Чтобы проверить ее вы можете временно изменить основную программу следующим образом:

{ Main Program }

WriteLn(Lookup(Addr(KWList), Token, 4));

Обратите внимание как вызывается Lookup: функция Addr устанавливает указатель на KWList, который передается в Lookup.

ОК, испытайте ее. Так как здесь мы пропускаем Scan, для получения соответствия вы должны набирать ключевые слова в верхнем регистре.

Теперь, когда мы можем распознавать ключевые слова, далее необходимо договориться о возвращаемых для них кодах.

Итак, какие кода мы должны возвращать? В действительности есть только два приемлемых варианта. Это похоже на идеальное применения перечислимого типа Паскаля. К примеру, вы можете определить что-то типа

SymType = (IfSym, ElseSym, EndifSym, EndSym, Ident, Number, Operator);

и договориться возвращать переменную этого типа. Давайте попробуем это. Вставьте строку выше в описание типов.

Теперь добавьте два описания переменных:

Token: Symtype; { Current Token }

Value: String; { String Token of Look }

Измените сканер так:

{ Lexical Scanner }

while Look = CR do

if IsAlpha(Look) then begin

Value:= GetName;

Token:= SymType(k – 1);

else if IsDigit(Look) then begin

else if IsOp(Look) then begin

Token:= Operator;

Token:= Operator;

(Заметьте, что Scan сейчас стала процедурой а не функцией).

Наконец, измените основную программу:

{ Main Program }

Ident: write("Ident ");

Number: Write("Number ");

Operator: Write("Operator ");

IfSym, ElseSym, EndifSym, EndSym: Write("Keyword ");

until Token = EndSym;

Мы заменили строку Token, используемую раньше, на перечислимый тип. Scan возвращает тип в переменной Token и возвращает саму строку в новой переменной Value.

ОК, откомпилируйте программу и погоняйте ее. Если все работает, вы должны увидеть, что теперь мы распознаем ключевые слова.

Теперь у нас все работает правильно, и было легко сгенерировать это из того, что мы имели раньше. Однако, она все равно кажется мне немного «перегруженной». Мы можем ее немного упростить, позволив GetName, GetNum, GetOp и Scan работать с глобальными переменными Token и Value, вследствие этого удаляя их локальные копии. Кажется немного умней было бы переместить просмотр таблицы в GetName. Тогда новая форма для этих четырех процедур будет такой:

{ Get an Identifier }

procedure GetName;

if not IsAlpha(Look) then Expected("Name");

while IsAlNum(Look) do begin

Value:= Value + UpCase(Look);

k:= Lookup(Addr(KWlist), Value, 4);

Token:= SymType(k-1);

{ Get a Number }

procedure GetNum;

if not IsDigit(Look) then Expected("Integer");

while IsDigit(Look) do begin

Value:= Value + Look;

{ Get an Operator }

procedure GetOp;

while IsOp(Look) do begin

Value:= Value + Look;

Token:= Operator;

{ Lexical Scanner }

while Look = CR do

if IsAlpha(Look) then

else if IsDigit(Look) then

else if IsOp(Look) then

Token:= Operator;

Возвращение символа

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

Прежде всего, список возможных типов символов может получиться довольно длинным. Здесь я использовал только один символ «Operator» для обозначения всех операторов, но я видел другие проекты, в которых фактически возвращаются различные кода для каждого.

Существует, конечно, другой простой тип, который может быть возвращен как код: символ. Вместо возвращения значения «Operator» для знака "+", что неправильного в том, чтобы просто возвращать сам символ? Символ – такая же хорошая переменная для кодирования различных типов лексем, она легко может быть использована в операторах Case, и это гораздо проще набрать. Что может быть проще?

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

Некоторые из вас могут почувствовать, что идея с возвращение символьных кодов слишком детская. Я должен допустить, что она становится немного неуклюжей для операторов типа «<=». Если вы хотите остаться с перечислимыми типами, хорошо. Для остальных я хотел бы показать как изменить то, что мы сделали выше, для поддержки такого подхода.

Во-первых, сейчас вы можете удалить объявление типа SymType... он нам больше не понадобится. И вы можете изменить тип Token в char.

Затем, чтобы заменить SymType, добавьте следующую константу:

(Я буду кодировать все идентификаторы одиночным символом "x").

Наконец измените Scan и его родственников следующим образом:

{ Get an Identifier }

procedure GetName;

if not IsAlpha(Look) then Expected("Name");

while IsAlNum(Look) do begin

Value:= Value + UpCase(Look);

{ Get a Number }

procedure GetNum;

if not IsDigit(Look) then Expected("Integer");

while IsDigit(Look) do begin

Value:= Value + Look;

{ Get an Operator }

procedure GetOp;

if not IsOp(Look) then Expected("Operator");

while IsOp(Look) do begin

Value:= Value + Look;

if Length(Value) = 1 then

{ Lexical Scanner }

while Look = CR do

if IsAlpha(Look) then

else if IsDigit(Look) then

else if IsOp(Look) then begin

{ Main Program }

"x": write("Ident ");

"#": Write("Number ");

"i", "l", "e": Write("Keyword ");

else Write("Operator ");

until Value = "END";

Эта программа должна работать также как и предыдущая версия. Небольшое различие в структуре, может быть, но она кажется мне более простой.

Распределенные сканеры против централизованных

Структура лексического анализатора, которую я только что вам показал, весьма стандартна и примерно 99% всех компиляторов используют что-то очень близкое к ней. Это, однако, не единственно возможная структура, или даже не всегда самая лучшая.

Проблема со стандартным подходом состоит в том, что сканер не имеет никаких сведений о контексте. Например, он не может различить оператор присваивания "=" и оператор отношения "=" (возможно именно поэтому и C и Паскаль используют для них различные строки). Все, что сканер может сделать, это передать оператор синтаксическому анализатору, который может точно сказать исходя из контекста, какой это оператор. Точно так же, ключевое слово «IF» не может быть посредине арифметического выражения, но если ему случится оказаться там, сканер не увидит в этом никакой проблемы и возвратит его синтаксическому анализатору, правильно закодировав как «IF».

С таким подходом, мы в действительности не используем всю информацию, имеющуюся в нашем распоряжении. В середине выражения, например, синтаксический анализатор «знает», что нет нужды искать ключевое слово, но он не имеет никакой возможности сказать это сканеру. Так что сканер продолжает делать это. Это, конечно, замедляет компиляцию.

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

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

В языке KISS, как и большинстве языков, ключевые слова появляются только в начале утверждения. В таких местах, как выражения они запрещены. Также, с одним небольшим исключением (многосимвольные операторы отношений), которое легко обрабатывается, все операторы односимвольны, что означает, что нам совсем не нужен GetOp.

Так что, оказывается, даже с многосимвольными токенами мы все еще можем всегда точно определить вид лексемы исходя из текущего предсказывающего символа, исключая самое начало утверждения.

Даже в этой точке, единственным видом лексемы, который мы можем принять, является идентификатор. Нам необходимо только определить, является ли этот идентификатор ключевым словом или левой частью оператора присваивания.

Тогда мы заканчиваем все еще нуждаясь только в GetName и GetNum, которые используются так же, как мы использовали их в ранних главах.

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

Объединение сканера и парсера

Теперь, когда мы охватили всю теорию и общие аспекты лексического анализа, я наконец готов подкрепит свое заявление о том, что мы можем приспособить многосимвольные токены с минимальными изменениями в нашей предыдущей работе. Для краткости и простоты я ограничу сам себя подмножеством того, что мы сделали ранее: я разрешу только одну управляющую конструкцию (IF) и никаких булевых выражений. Этого достаточно для демонстрации синтаксического анализа и ключевых слов и выражений. Расширение до полного набора конструкций должно быть довольно очевидно из того, что мы уже сделали.

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

{ Constant Declarations }

{ Type Declarations }

type Symbol = string;

SymTab = array of Symbol;

TabPtr = ^SymTab;

{ Variable Declarations }

procedure GetChar;

{ Report an Error }

procedure Error(s: string);

{ Report Error and Halt }

procedure Abort(s: string);

{ Report What Was Expected }

Abort(s + " Expected");

{ Recognize a Decimal Digit }

IsDigit:= c in ["0".."9"];

{ Recognize an AlphaNumeric Character }

function IsAlNum(c: char): boolean;

IsAlNum:= IsAlpha(c) or IsDigit(c);

{ Recognize an Addop }

IsAddop:= c in ["+", "-"];

{ Recognize a Mulop }

IsMulop:= c in ["*", "/"];

{ Recognize White Space }

IsWhite:= c in [" ", TAB];

procedure SkipWhite;

while IsWhite(Look) do

procedure Match(x: char);

if Look <>

if Look = CR then GetChar;

if Look = LF then GetChar;

{ Get an Identifier }

function GetName: char;

while Look = CR do

if not IsAlpha(Look) then Expected("Name");

Getname:= UpCase(Look);

{ Get a Number }

function GetNum: char;

if not IsDigit(Look) then Expected("Integer");

{ Generate a Unique Label }

function NewLabel: string;

NewLabel:= "L" + S;

{ Post a Label To Output }

WriteLn(L, ":");

{ Output a String with Tab }

procedure Emit(s: string);

procedure EmitLn(s: string);

procedure Ident;

if Look = "(" then begin

EmitLn("BSR " + Name);

EmitLn("MOVE " + Name + "(PC),D0");

procedure Factor;

if Look = "(" then begin

else if IsAlpha(Look) then

EmitLn("MOVE #" + GetNum + ",D0");

procedure SignedFactor;

if IsAddop(Look) then begin

EmitLn("NEG D0");

procedure Multiply;

EmitLn("MULS (SP)+,D0");

procedure Divide;

EmitLn("MOVE (SP)+,D1");

EmitLn("EXS.L D0");

EmitLn("DIVS D1,D0");

procedure Term1;

while IsMulop(Look) do begin

EmitLn("MOVE D0,-(SP)");

procedure FirstTerm;

EmitLn("ADD (SP)+,D0");

procedure Subtract;

EmitLn("SUB (SP)+,D0");

EmitLn("NEG D0");

procedure Expression;

while IsAddop(Look) do begin

EmitLn("MOVE D0,-(SP)");

{ This version is a dummy }

Procedure Condition;

EmitLn("Condition");

procedure Block;

var L1, L2: string;

EmitLn("BEQ " + L1);

if Look = "l" then begin

EmitLn("BRA " + L2);

procedure Assignment;

EmitLn("MOVE D0,(A0)");

procedure Block;

while not(Look in ["e", "l"]) do begin

CR: while Look = CR do

else Assignment;

procedure DoProgram;

if Look <> "e" then Expected("END");

{ Main Program }

Пара комментариев:

Форма синтаксического анализатора выражений, использующего FirstTerm и т.п., немного отличается от того, что вы видели ранее. Это еще одна вариация на ту же самую тему. Не позволяйте им вертеть вами... изменения необязательны для того, что будет дальше.

Прежде чем приступить к добавлению сканера, сначала скопируйте этот файл и проверьте, что он действительно корректно выполняет анализ. Не забудьте «кода»: "i" для IF, "l" для ELSE и "e" для ELSE или ENDIF.

Если программа работает, тогда давайте поспешим. При добавлении модулей сканера в программу поможет систематический план. Во всех синтаксических анализаторах, которые мы написали до этого времени, мы придерживались соглашения, что текущий предсказывающий символ должен всегда быть непустым символом. Мы предварительно загружали предсказывающий символ в Init и после этого оставляли «помпу запущенной». Чтобы позволить программе работать правильно с новыми строками мы должны ее немного модифицировать и обрабатывать символ новой строки как допустимый токен.

В многосимвольной версии правило аналогично: текущий предсказыващий символ должен всегда оставаться на начале следующей лексемы или на новой строке.

Многосимвольная версия показана ниже. Чтобы получить ее я сделал следующие изменения:

Добавлены переменные Token и Value и определения типов, необходимые для Lookup.

Добавлено определение KWList и KWcode.

Добавлен Lookup.

GetName и GetNum заменены их многосимвольными версиями. (Обратите внимание, что вызов Lookup был перемещен из GetName, так что он не будет выполняться внутри выражений).

Создана новая, рудиментарная Scan, которая вызывает GetName затем сканирует ключевые слова.

Создана новая процедура MatchString, которая ищет конкретное ключевое слово. Заметьте, что в отличие от Match, MatchString не считывает следующее ключевое слово.

Изменен Block для вызова Scan.

Немного изменены вызовы Fin. Fin теперь вызывается из GetName.

Программа полностью:

{ Constant Declarations }

{ Type Declarations }

type Symbol = string;

SymTab = array of Symbol;

TabPtr = ^SymTab;

{ Variable Declarations }

var Look: char; { Lookahead Character }

Token: char; { Encoded Token }

Value: string; { Unencoded Token }

Lcount: integer; { Label Counter }

{ Definition of Keywords and Token Types }

const KWlist: array of Symbol =

("IF", "ELSE", "ENDIF", "END");

const KWcode: string = "xilee";

{ Read New Character From Input Stream }

procedure GetChar;

{ Report an Error }

procedure Error(s: string);

WriteLn(^G, "Error: ", s, ".");

{ Report Error and Halt }

procedure Abort(s: string);

{ Report What Was Expected }

procedure Expected(s: string);

Abort(s + " Expected");

{ Recognize an Alpha Character }

function IsAlpha(c: char): boolean;

IsAlpha:= UpCase(c) in ["A".."Z"];

{ Recognize a Decimal Digit }

function IsDigit(c: char): boolean;

IsDigit:= c in ["0".."9"];

{ Recognize an AlphaNumeric Character }

function IsAlNum(c: char): boolean;

IsAlNum:= IsAlpha(c) or IsDigit(c);

{ Recognize an Addop }

function IsAddop(c: char): boolean;

IsAddop:= c in ["+", "-"];

{ Recognize a Mulop }

function IsMulop(c: char): boolean;

IsMulop:= c in ["*", "/"];

{ Recognize White Space }

function IsWhite(c: char): boolean;

IsWhite:= c in [" ", TAB];

{ Skip Over Leading White Space }

procedure SkipWhite;

while IsWhite(Look) do

{ Match a Specific Input Character }

procedure Match(x: char);

if Look <> x then Expected("""" + x + """");

if Look = CR then GetChar;

if Look = LF then GetChar;

{ Table Lookup }

function Lookup(T: TabPtr; s: string; n: integer): integer;

while (i > 0) and not found do

if s = T^[i] then

{ Get an Identifier }

procedure GetName;

while Look = CR do

if not IsAlpha(Look) then Expected("Name");

while IsAlNum(Look) do begin

Value:= Value + UpCase(Look);

{ Get a Number }

procedure GetNum;

if not IsDigit(Look) then Expected("Integer");

while IsDigit(Look) do begin

Value:= Value + Look;

{ Get an Identifier and Scan it for Keywords }

Token:= KWcode;

{ Match a Specific Input String }

procedure MatchString(x: string);

if Value <> x then Expected("""" + x + """");

{ Generate a Unique Label }

function NewLabel: string;

NewLabel:= "L" + S;

{ Post a Label To Output }

procedure PostLabel(L: string);

WriteLn(L, ":");

{ Output a String with Tab }

procedure Emit(s: string);

{ Output a String with Tab and CRLF }

procedure EmitLn(s: string);

{ Parse and Translate an Identifier }

procedure Ident;

if Look = "(" then begin

EmitLn("BSR " + Value);

EmitLn("MOVE " + Value + "(PC),D0");

{ Parse and Translate a Math Factor }

procedure Expression; Forward;

procedure Factor;

if Look = "(" then begin

else if IsAlpha(Look) then

EmitLn("MOVE #" + Value + ",D0");

{ Parse and Translate the First Math Factor }

procedure SignedFactor;

if IsAddop(Look) then begin

EmitLn("NEG D0");

{ Recognize and Translate a Multiply }

procedure Multiply;

EmitLn("MULS (SP)+,D0");

{ Recognize and Translate a Divide }

procedure Divide;

EmitLn("MOVE (SP)+,D1");

EmitLn("EXS.L D0");

EmitLn("DIVS D1,D0");

{ Completion of Term Processing (called by Term and FirstTerm }

procedure Term1;

while IsMulop(Look) do begin

EmitLn("MOVE D0,-(SP)");

{ Parse and Translate a Math Term }

{ Parse and Translate a Math Term with Possible Leading Sign }

procedure FirstTerm;

{ Recognize and Translate an Add }

EmitLn("ADD (SP)+,D0");

{ Recognize and Translate a Subtract }

procedure Subtract;

EmitLn("SUB (SP)+,D0");

EmitLn("NEG D0");

{ Parse and Translate an Expression }

procedure Expression;

while IsAddop(Look) do begin

EmitLn("MOVE D0,-(SP)");

{ Parse and Translate a Boolean Condition }

{ This version is a dummy }

Procedure Condition;

EmitLn("Condition");

{ Recognize and Translate an IF Construct }

procedure Block; Forward;

var L1, L2: string;

EmitLn("BEQ " + L1);

if Token = "l" then begin

EmitLn("BRA " + L2);

MatchString("ENDIF");

{ Parse and Translate an Assignment Statement }

procedure Assignment;

var Name: string;

EmitLn("LEA " + Name + "(PC),A0");

EmitLn("MOVE D0,(A0)");

{ Recognize and Translate a Statement Block }

procedure Block;

while not (Token in ["e", "l"]) do begin

else Assignment;

{ Parse and Translate a Program }

procedure DoProgram;

MatchString("END");

{ Main Program }

Сравните эту программу с ее односимвольным вариантом. Я думаю вы согласитесь, что различия минимальны.

Заключение

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

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

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

Страница 1 из 14

ВВЕДЕНИЕ

В последней главе я оставил вас с компилятором который должен почти работать, за исключением того, что мы все еще ограничены одно-символьными токенами. Цель этого урока состоит в том, чтобы избавиться от этого ограничения раз и навсегда. Это означает, что мы должны иметь дело с концепцией лексического анализатора (сканера).

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

Единственная причина, на самом деле, имеет отношение к ключевым словам. Это факт компьютерной жизни, что синтаксис ключевого слова имеет ту же самую форму, что и синтаксис любого другого идентификатора. Мы не можем сказать пока не получим полное слово действительно ли это ключевое слово. К примеру переменная IFILE и ключевое слово IF выглядят просто одинаковыми до тех пор, пока вы не получите третий символ. В примерах до настоящего времени мы были всегда способны принять решение, основанное на первом символе токена, но это больше невозможно когда присутствуют ключевые слова. Нам необходимо знать, что данная строка является ключевым словом до того, как мы начнем ее обрабатывать. И именно поэтому нам нужен сканер.

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

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

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

В информатике лексический анализ - процесс аналитического разбора входной последовательности символов (например, такой как исходный код на одном из языков программирования) с целью получения на выходе последовательности символов, называемых лексемами или «токенами » (подобно группировке букв в словах). Таким образом, в процессе лексического анализа производится распознавание и выделение лексем из входной последовательности символов.

Как правило, лексический анализ производится с точки зрения определённого языка или набора языков. Язык, а точнее его грамматика, задаёт определённый набор лексем, которые могут встретиться на входе процесса.

Традиционно принято организовывать процесс лексического анализа, рассматривая входную последовательность символов, как поток символов. При такой организации, процесс самостоятельно управляет выборкой отдельных символов из входного потока.

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

Каждую лексему можно представить в виде структуры, содержащей идентификатор лексемы (или идентификатор класса лексемы) и, если нужно, последовательность символов лексемы выделенной из входного потока (строку, число и т. д.).

Цель такой конвертации обычно состоит в том, чтобы подготовить входную последовательность для другой программы, например, для грамматического анализатора, и избавить его от определения лексических подробностей в контекстно-свободной грамматике (что привело бы к усложнению грамматики).

Синтаксический (грамматический) анализ

В информатике грамматический анализ (грамматический разбор , парсинг ) - это процесс сопоставления линейной последовательности лексем языка с его формальной грамматикой. Результатом обычно является дерево разбора. Обычно применяется совместно с лексическим анализом, в процессе синтаксического анализа.

Грамматический анализатор (парсер ) - программа или алгоритм, осуществляющий грамматический разбор.

Процессы компиляции и интерпретации можно представить следующей схемой.

Интерпретатор

Исходная программа

Интерпретация

Компилятор

Компиляция

Выполнение

Целевая программа

Рис. 2.4. Компиляция и интерпретация

Комбинированные компиляции и интерпретации

Смешанная стратегия предполагает, что компилятор создаёт код на промежуточном языке, который понятен виртуальной машине

Исходная программа

Компилятор

Виртуальная машина

Интерпретатор

Компиляция

Рис. 2.5. Компиляция плюс интерпретация

Виртуальные машины, байт-код и JIT (Just In Time )

Реализация таких языков, как Java, C# и других языков платформы.NET, основана на смешанном решении. Промежуточный код для Java называется байт-кодом. В этом термине отображается тот факт, что виртуальная машина использует компактные команды, подобные командам фактического процессора. Для повышения эффективности времени выполнения байт-кода применяют JIT (Just In Time) компиляторы, называемые джитерами , осуществляющие компиляцию по требованию. Основная идея состоит в том, что машинный код для некоторого модуля создаётся «на лету», в тот момент, когда он первый раз вызывается на выполнение. Последовательность обработки программы можно представить следующей схемой.

Исходная программа

Компилятор

Компиляция

Интерпретатор

Выполнение

Машинный код

Рис. 2.6. Компиляция плюс интерпретация и джитинг

Четвертую группу базового программного обеспечения образуют программы технического обслуживания компьютера . К множеству таких программ, например, относятся программы диагностики и обнаружения ошибок в процессе работы локального компьютера или сети.

2.1. Проверка достижения целей

2.2.Правильные ответы

3. Структура программ и правила их кодирования

Цели

Изучив данный учебный элемент, вы будете способны:

    правильно трактовать виды и назначение единиц трансляции;

    распознавать структуру программы;

    правильно трактовать определение функции;

    выделять структурные составляющие определения функции;

    распознавать объявления внешних объектов.

3.1. Общие сведения

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

    комментарии,

    определения и объявления,

    операторы.

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

Машинную программу начинает формировать транслятор. Он переводит исходный текст программы, записанный на языке программирования высокого уровня, на какой-либо промежуточный язык или машинный язык. Обрабатываемый объект в исходном тексте программы может быть представлен именем, которое формирует программист по своему усмотрению. Такой объект – это блок памяти, а котором нужно хранить значение предписанного типа. Поэтому для создания начального состояния рабочей программы транслятору необходимо знать, как трактовать встретившееся ему имя (т.е., какой объект представлен этим именем).

Чтобы транслятор смог выполнить свои функции, ему нужно предоставить сведения о данном объекте и потребовать его создания, если объект ещё не создан. Под созданием объекта понимают выделение для него блока памяти и связывание адреса этого блока с именем объекта. После создания объекта его имя является условным обозначением адреса блока памяти объекта. Требование создания объекта программист указывает в форме определения.

Определение объекта – это указание транслятору о необходимости создать объект. Оно содержит информацию о его имени и типе. Тип объекта характеризует множество допустимых значений, множество допустимых операций над значением, правила выделения памяти и правила хранения значения (правила разметки памяти). Следовательно, при определении объекта обязательно явно или неявно указывают его имя и тип . При этом тип может быть представлен именем или описанием. Разработчики транслятора вкладывают в него имена некоторых часто используемых типов. В данный момент обратите лишь внимание на следующие предопределённые имена типов:

Таким образом, тип Integer описывает значения, которые представлены целыми (т.е. без дробной части) числами, Double – множество значений, представленных числами с дробной частью, Char символьные значения, Boolean – логические значения, а String – строки символов, т.е. некоторую последовательность символов.

Прежде чем обрабатывать какой-либо объект, его нужно создать, т.е. выделить соответствующий блок памяти и обеспечить доступ к нему. Объект может создать транслятор или программист. Чтобы транслятор смог создать объект ему нужно передать определённую инструкцию, содержащую сведения о правилах выделения памяти. Такая инструкция записывается в форме определения объекта.

В языках C++ и C# определения записываются предложениями, которые могут последовательно располагаться на одной строке или на отдельных строках. В Visual Basic они записываются только на отдельных строках теста программы. В любом из представленных языков разрешено в одном определении можно указывать имя одиночного объекта или список имён. Такие предложения формируются по следующим шаблонам:

Язык С++:

<тип > <Имя объекта >;

<тип > <Список имён объектов >;

Язык С#:

<тип > <Имя объекта >;

<тип > <Список имён объектов >;

var <Имя объекта >=<Инициализатор >;

Язык Visual Basic :

Dim <Имя объекта > As <Имя типа >

Dim <Список имён объектов > As <Имя типа >

Dim <Имя объекта >=<Инициализатор >

Например, конструкции

Язык Visual Basic : Язык С ++: Язык С#:

Dim m As Integer int m; int m;

Dim n1, n2 As Integer int n1, n2; int n1, n2;

Dim x, w, z As Double; double x, w, z; double x, w, z;

Dim box=56 var box=56;

являются определением целочисленного объекта (переменной) m, двух целочисленных переменных n1, n2 и трёх вещественных переменных x, w, z, соответственно.

На заметку! В языках C ++, C # определения записываются в любой позиции среди других предложений и завершаются точкой с запятой. В Visual Basic каждое определение точку с запятой не содержит и записывается на отдельной строке или в одной строке через двоеточие. В любом случае определения всегда должны предшествовать позициям, в которых указано использования объектов.

Непосредственно после создания объект не содержит полезного для программиста значения. Такое значение программист должен задавать ему самостоятельно. Для выполнения такого действия используют операции инициализации, присваивания и ввода.

Операция инициализации – это копирование значения сразу же после выделения памяти. Она указывается непосредственно в определении объекта с помощью символа = (равно). Выглядит это следующим образом:

Язык Visual Basic : Язык С ++: Язык С#:

Dim a As Integer=60; int a=60; или int a(60); int a=60;

Dim b=700 var b=700;

В данном случае переменная A инициализирована значением 60.

Операция присваивания указывается в любой момент и в нужной позиции текста программы после определения объекта. В простейшем случае присваивание также указывается символом равно (=). Например, для задания значения 35 целочисленной переменной mom задают операцию

Язык Visual Basic : Язык С ++: Язык С#:

mom = 35 mom=35 mom=35

Операция ввод а предполагает копирование значения от внешнего источника. Каждый язык может иметь собственный набор средств для кодирования ввода данных.

В Visual Basic ввод данных с клавиатуры можно кодировать с помощью блоков диалога или методов классов Console. Например, представленная ниже программа иллюстрирует возможности ввода данных с помощью блока диалога InputBox() и метода Console.ReadLine().

Листинг 3.1.1.

" Иллюстрация ввода и вывода разными средствами

Dim userName As String = InputBox("Как вас зовут?")

MsgBox("Имя пользователя: " & userName)

Console.Write("Введите имя пользователя: ") : userName = Console.ReadLine()

Console.WriteLine("Имя пользователя: {0}", userName)

Console.ReadKey()

Не забывайте, что методы класса – это просто функции, которые определены внутри соответствующего класса. Для обращения к методу класса указывают имя класса, точку и имя метода с круглыми скобками, которые могут быть пустыми или содержать список аргументов. Обычно при работе с консольным окном для кодирования ввода с клавиатуры вы будете вызывать методы Console.ReadLine() и Console.Read(). Первый из этих методов считывает строку символов, а второй – внутренний код одиночного символа. В вызовах методов внутри круглых скобок не указывают никаких аргументов , считанные данные поступают в точку вызова методов. При использовании разных средств ввода данных нужно помнить, что вызов блока ввода InputBox() приводит к отображению блока диалога с однострочным редакторов, в строку которого пользователь вводит нужные ему данные. Например, для данной программы отображается блок диалога, вид которого после ввода имени «Волобуев» представлен на рис. 3.1.1.

Рис. 3.1.1. Вид блока диалога ввода после ввода в строку редактора имени пользователя

Поскольку метод Console.ReadLine() возвращает строку символов, которая может представлять текст или образ числа, то для получения самого числа нужно явно указывать преобразование текстового образа в число. Для этой цели в VB используются методы класса Convert или собственные макросы вида CInt(), CDbl() и т.п.. Метод с именем ToInt32(s) или макрос CInt(s) вызывают для преобразования строки символов в целое число, а метод ToDouble(s) или макрос CDbl(s) – в вещественное число удвоенной точности. В конкретном случае ввод целого и вещественного числа и преобразование к числовому типу с помощью методов класса Convert выглядит следующим образом:

Dim mm As Integer= Convert.ToInt32(Console.ReadLine())

Dim m1 As Double = Convert.ToDouble(Console.ReadLine())

Dim box= Convert.ToDouble(Console.ReadLine())

Если использовать макросы, то эти дже операторы приобретают вид

Dim mm As Integer= CInt(Console.ReadLine())

Dim m1 As Double = CDbl(Console.ReadLine())

Dim box= CDbl(Console.ReadLine())

Пример .

Написать консольное приложение для вычисления площади прямоугольника, задаваемого длинами своих сторон. Длины сторон вводятся с клавиатуры.

Ответ (Листинг 3.1.2).

Листинг 3.1.2.

" Вычисление площади прямоугольника

Console.Write("Введите длину строны A прямоугольника: ")

Dim a As Double = Convert.ToDouble(Console.ReadLine())

Console.Write("Введите длину строны B прямоугольника: ")

Dim b As Double = Convert.ToDouble(Console.ReadLine())

Console.WriteLine("Площадь прямоугольника: {0}", a * b)

Console.ReadKey()

После активизации приложения отображается диалог, показанный на рис. 3.1.2.

Рис. 3.1.2. Возможная форма диалога, отображаемого в консольном окне

В C # ввод с клавиатуры кодируется методами Console.ReadLine() и Console.Read(). Они могут вызывается в той же форме, которая описана для VB, но для преобразования к числовому типу можно обычно использовать только средства класса Convert. Например, если записать

int a, b, mm = Convert.ToInt32(Console.ReadLine());

double m1 = Convert.ToDouble(Console.ReadLine());

a=Convert.ToInt32(Console.ReadLine());

b=Convert.ToInt32(Console.ReadLine());

Операция ввода может указываться не обязательно в сочетании с именем объекта. Например, если активизировать приложение, в форме листинга 3.1.3

Листинг 3.1.3.

namespace ConsAppSharp00 {

class Program {

Console.Title = "Ввод данных";

Console.WriteLine("Введите a, b."); // Заголовок консольного окна

var a=Convert.ToInt32(Console.ReadLine());

Console.WriteLine("a={0} b={1}",a, Convert.ToInt32(Console.ReadLine()));

Console.ReadKey();

то консольное окно будет иметь состояние, представленное на рис. 3.1.3.

Рис. 3.1.3. Итоговое состояние консольного окна

В C ++ ISO / ANSI операция ввода с клавиатуры кодируется операцией извлечения из потока cin , записываемой символом ‘>>’. Например, если записать

int mm; cin>>mm;

double m1; cin>>m1;

int a, b; cin>>a>>b;

то это будет означать ввод значений переменных mm, m1, a и b с клавиатуры.

В C ++/ CLI операция ввода с клавиатуры кодируется методами класса Console точно так же, как в C# или с помощью потока cin и операции ‘>>’. Например, если записать

int mm = Convert::ToInt32(Console::ReadLine());

double m1 = Convert::ToDouble(Console::ReadLine());

a=Convert::ToInt32(Console::ReadLine());

b=Convert::ToInt32(Console::ReadLine());

int mm; cin>>mm;

double m1; cin>>m1;

int a, b; cin>>a>>b;

то это будет означать ввод значений переменных mm, m1, a и b с клавиатуры. Подобно C#, операция ввода с помощью вызовов методов класса Console может указываться не обязательно в сочетании с именем объекта. Например, если активизировать приложение, в форме листинга 3.1.4

Листинг 3.1.4.

// qq.cpp: главный файл проекта.

// Пример использования операции ввода

#include "stdafx.h"

using namespace System;

int main(array ^args) {

Console::Title = L"Ввод данных";

Console::WriteLine(L"Введите a, b."); // Заголовок консольного окна

int a=Convert::ToInt32(Console::ReadLine());

Console::WriteLine(L"a={0} b={1}",a, Convert::ToInt32(Console::ReadLine()));

Console::ReadKey();

Итог выполнения данного приложения будет иметь такую же форму, которая указана на рис. 3.1.3.

Операция вывод а. Значения объектов можно не только задавать, но и отображать на экране. Каждый язык программирования может иметь собственный набор средств кодирования этой операции. Однако для языков, поддерживающих платформу.NET, вывод в консольное окно может кодироваться вызовами методов класса Console. К такому множеству языков относятся VB, C# и C++/CLI. В C++ ISO/ANSI вывод в консольное окно можно реализовать операцией вставки в поток cout, которая указывается символом ‘<<’.

В среде VB для вывода можно использовать как блоки диалога, так и средства класса Console. К множеству блоков вывода сообщений относится, например, блок с именем MsgBox(). Этот блок имеет несколько модификаций. В простейшем случае он может содержать внутри скобок одну строку символов, представляющую отображаемое сообщение. Например, для отображения текста «Это моя первая программа.» достаточно указать вызов этого блока в форме

MsgBox("Это моя первая программа. ")

Если же сообщение состоит из нескольких частей, то части можно сцеплять с помощью символа амперсанда (&) в единую строку символов. Например, в представленной выше программе введённое имя пользователя сцепляется со строкой "Имя пользователя: ".

После вызова такого блока диалога на экране отображается блок диалога, вид которого представлен на рис. 3.1.4.

Рис. 3.1.4. Вид блока диалога вывода сообщения об имени пользователя

Для вывода данных в консольное окно средствами.NET вызывают методы Console.WriteLine(]) и Console.Write(s[, list_elem]), которые отображают строку символов, представленную первым параметром s. Второй параметр – это список элементов, используемых для модификации первого параметра непосредственно перед его отображением. Квадратные скобки в данном случае – это условное обозначение того, что эти параметры могут опускаться. В простейших ситуациях в круглых скобках можно указывать текст в кавычках или конкретное одиночное значение или имя одной переменной или выражение.. Например, операторы

Console.WriteLine("Объём контейнера")

Console.Write("Стоимость работ: ")

Console.WriteLine(235)

Console.Write(67+33)

Аналогично, любой вызов методов Console.ReadLine(), Console.Read() предполагает, что ввод данных сопровождается их автоматическим отображением в консольном окне. Например, диалог ввода данных в программе, представленной листингом 3.1.1, отображается в консольном окне в форме, которая представлена на рис. 3.1.5.

Рис. 3.1.5. Состояние консольного окна после ввода имени пользователя

В C # и C ++/ CLI операция вывода в консольное окно указывается аналогично. Если операция записывается в форме отдельного оператора, то вызов метода завершается точкой с запятой. Например, в среде C # операторы

Console.WriteLine("Объём контейнера");

Console.Write("Стоимость работ: ");

отображают в консольном окне текст, который указан между кавычками. А операторы

Console.WriteLine(235);

Console.Write(67+33);

обеспечивают отображение числа 235 и 100, соответственно.

Аналогичные операторы для среды C ++/ CLI имеют вид:

Console::WriteLine(L"Объём контейнера");

Console::Write(L"Стоимость работ: ");

Console::WriteLine(235);

Console::Write(67+33);

Например, вот как может выглядеть приложение C# для ввода значений с клавиатуры и отображения значений в консольном окне.

Листинг 3.1.5.

// Иллюстрация кодирования ввода/вывода в консольное окно

// Программист А. Ф.

namespace AppSh100 {

class Program {

static void Main(string args) {

Console.WriteLine("Введите целое и вещественное число."); // Подсказка о вводе

int mm = Convert.ToInt32(Console.ReadLine()); // Ввод целого числа

double valueCap = Convert.ToDouble(Console.ReadLine()); // Ввод вещественного числа

Console.WriteLine("Сумма чисело=: {0}", mm + valueCap); // Вывод суммы чисел

Console.ReadKey(); // Пауза для просмотра итога

После активизации приложения и ввода чисел 50 и 25,5 состояние консольного окна может быть таким, как показано ра рис. 3.1.6.

Рис. 3.1.6. Итоговое состояние консольного окна

Простейшее приложение C++/CLI может иметь следующий вид:

Листинг 3.1.6.

// Программист А. Ф.

#include "stdafx.h" // Сохраните в своем проекте это подключение

using namespace System; // Объявление пространства имен

int main(array ^args) {

// Ввод в режиме диалога значений А и В

Console::WriteLine(L"Введите А, В: ");

double a = Convert::ToDouble(Console::ReadLine());

double b = Convert::ToDouble(Console::ReadLine());

double y = Math::Sin(a+b); // Определение переменной y

// Вывод итога в консольное окно

Console::WriteLine(L"Y= {0}", y);

После активизации такой программы диалог с пользователем может иметь показанный на рис. 3.1.7 вид:

Рис. 3.1.7. Форма диалога, генерируемого приложением

В C ++ ISO / ANSI вывод в консольное окно кодируется операцией вставки в поток cout. Например, представленные выше операторы могут записываться в форме

cout<<"Объём контейнера"<

cout<<"Стоимость работ: ";

cout<<(67+33);

Примером кодирования простейшего приложения является следующая программа:

Листинг 3.1.7.

/* Вычисление значения выражения */

#include "stdafx.h" // Всегда сохраняйте в своем проекте это подключение

#include // Для доступа к функции Sin(x)

#include // Подключение средств ввода/вывода

using namespace std; // Объявление пространства имён std

int _tmain(int argc, _TCHAR* argv){

double a, b; // Определение вещественных переменных a, b

setlocale(LC_CTYPE, "Russian"); // Настройуа вывода на кириллицу

cout<<"Введите A, B: "; cin >>a >> b; // Ввод с клавиатуры

cin.get(); // Для считывания завершающего нажатия Enter

/* Вычисление значения и вывод на экран */

double y = sin(a+b); // Определение вещественной переменной y

cout << "\nY= " <

cin.get(); // Пауза для просмотра итога

При формировании текста этой программы многострочными комментариями указано назначение программы и выделен этап вычисления и вывода значения на экран. Все остальные комментарии однострочные. После активизации такой программы диалог с пользователем может иметь показанный на рис 3.1.8 вид.

Рис. 3.1.8. Итоговый вид консольного окна приложения

Единица трансляции . Языки программирования C++, C# и Visual Basic 2010 являются языками высокого уровня. Это означает, что любая записанная с их помощью программа обязательно должна транслироваться. Единицей трансляции для них является файл . Из этого вытекает, что программа на этих языках может представляться одним или несколькими файлами исходного кода, каждый из которых должен подаваться на вход транслятора. Любой файл, содержащий исходный код всей программы или её части, может включать в себя последовательность определений интерфейсов, классов, делегатов и структур. Для VB файл может содержать модули. Внутри каждого модуля вы можете объявлять и определять типы, постоянные, переменные и подпрограммы. Для C++ файл может также содержать набор функций, границы которых не пересекаются.

Напоминание!

Структуры – типы, которые являются упрощёнными вариантами классов.

Делегаты - специальные классы, предназначенные для описания объектов, способ-ных стать средством вызова связанных с ними методов.

Интерфейсы – специальная разновидность абстрактных классов, которые формально описывают заголовки средств доступа к ресурсам, а тип перечисляемых предназначен для описания объектов, значения которых перечисляются программистом при описании типа.

3.2. Структура программы и подпрограммы

Программа Visual Basic построена из стандартных блоков – решений, проектов, сборок, исходных файлов, модулей, классов и интерфейсов. Решение состоит из одного или нескольких проектов. Проект, в свою очередь, может содержать одну или несколько сборок. Каждая сборка компилируется из одного или нескольких исходных файлов. Исходный файл включает в себя определения и реализацию классов, структур, модулей и интерфейсов, и, в конечном счёте, содержит весь код.

ЛАБОРАТОРНАЯ РАБОТА 3. ИЗУЧЕНИЕ ЭТАПА ЛЕКСИЧЕСКОГО АНАЛИЗА ТРАНСЛЯТОРОВ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ

Цель работы: Изучить методы построения лексических сканеров на основе конечных автоматов и формальных грамматик.

Архитектура компилятора

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

Препроцессирование;

Лексический анализ;

Синтаксический анализ;

Генерация кода;

Оптимизация программы.

Рассмотрим более подробно этап лексического анализа. Программа, которая выполняет этот этап называется лексический анализатор или сканер. Сканер переводит текст исходной программы из последовательности символов или строк в последовательность лексем. Лексема - минимальный элемент языка программирования. Для заданного языка программирования число типов лексем предполагается конечным.

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

Лексический анализ

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

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

Числа, строки символов заключенные в кавычки и другие самоопределенные данные классифицируются как "литералы". Информация о них заносится в таблицу литералов. В отличие от идентификаторов литералы позволяют определить их атрибуты.

Рассмотрим пример построения описанных таблиц при компиляции следующей программы:

char * b=".dat";

Таблица терминальных символов(TRM). Таблица стандартных символов.

Символ Разделитель Другие Тип Индекс Строка программы
; TRM main
( TRM (
) TRM )
, TRM {
main TRM int
int IDN a
{ TRM ;
} TRM char
= TRM *
+ IDN b
* TRM =
char TRM "
" LTR .dat
TRM "
IDN a
TRM =
IDN a
TRM +
LTR
TRM ;
TRM }

Таблица идентификаторов (IDN). Таблица литералов (LTR).

Рассмотренные выше таблицы демонстрируют основные правила построения. Таблица терминальных символов уже заложена в компиляторе и в данном случае ее содержимое построено конкретно для примера.

Простейший способ организации таблицы символов - добавлять элементы в порядке их поступления, но тогда на поиск приходится тратить много времени. Элементы таблицы можно отсортировать и использовать различные методы поиска.



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

Таблица блоков.

Лексический сканер должен учитывать области видимости и кодировать их по-разному.

Содержание задания: Разработать программу лексического сканирования и анализа для заданных языка программирования и типов лексем. Программа должна построить заданные таблицы и на их основе преобразовать анализируемую программу, заменив искомые лексемы на мнемонические имена. Мнемонические имена должны генерироваться так, чтобы любая лексема заменялась уникальным именем, а имя отражало ее тип (например, I1 - первая лексема целого типа).



Понравилась статья? Поделитесь с друзьями!