ВВЕДЕНИЕ В последней главе я оставил вас
с компилятором который должен почти работать, за исключением того, что
мы все еще ограничены односимвольными токенами. Цель этого урока состоит
в том, чтобы избавиться от этого ограничения раз и навсегда. Это означает,
что мы должны иметь дело с концепцией лексического анализатора (сканера).
ЛЕКСИЧЕСКИЙ АНАЛИЗ Лексический анализ - это процесс
сканирования потока входных символов и разделения его на строки, называемые
лексемами. Большинство книг по компиляторам начинаются с этого и посвящают
несколько глав обсуждению различных методов построения сканеров. Такой
подход имеет свое место, но, как вы уже видели, существуют множество вещей,
которые вы можете сделать даже никогда не обращавшись к этому вопросу,
и, фактически, сканер, который мы здесь закончим, не очень будет напоминать
то, что эти тексты описывают. Причина? Теория компиляторов и, следовательно,
программы следующие из нее, должны работать с большинством общих правил
синтаксического анализа. Мы же не делаем этого. В реальном мире возможно
определить синтаксис языка таким образом, что будет достаточно довольно
простого сканера. И как всегда KISS - наш девиз.
Хорошая сторона этих двух типов в том, что существуют очень специфические пути для их анализа. Было показано, что любая регулярная грамматика может быть анализирована с использованием частной формы абстрактной машины, называемой конечным автоматом. Мы уже реализовывали конечные автоматы в некоторых их наших распознающих программ. Аналогично грамматики Типа 2 (контекстно-свободные) всегда могут быть анализированы с использованием магазинного автомата (конечный автомат, дополненный стеком). Мы также реализовывали эти машины. Вместо реализации явного стека для выполнения работы мы положились на встроенный стек связанный с рекурсивным кодированием и это фактически является предочтительным способом для нисходящего синтаксического анализа. Случается что в реальных, практических грамматиках части, которые квалифицируются как регулярные выражения, имеют склонность быть низкоуровневыми частями, как определение идентификатора: <ident> ::= <letter> [ <letter> | <digit> ]* Так как требуется различные виды
абстрактных машин для анализа этих двух типов грамматик, есть смысл отделить
эти низкоуровневые функции в отдельный модуль, лексический анализатор,
который строится на идее конечного автомата. Идея состоит в том, чтобы
использовать самый простой метод синтаксического анализа, необходимый для
работы.
КОНЕЧНЫЕ АВТОМАТЫ И АЛЬТЕРНАТИВЫ Я упомянул, что регулярные выражения
могут анализироваться с использованием конечного автомата. В большинстве
книг по компиляторам а также в большинстве компиляторов, вы обнаружите,
что это применяется буквально. Обычно они имеют настоящую реализацию конечного
автомата с целыми числами, используемыми для определения текущего состояния
и таблицей действий, выполняемых для каждой комбинации текущего состояния
и входного символа. Если вы пишите "front end" для компилятора, используя
популярные Unix инструменты LEX и YACC, это то, что вы получите. Выход
LEX - конечый автомат, реализованный на C плюс таблица действий, соответствующая
входной грамматике данной LEX. Вывод YACC аналогичен... исскуственный таблично-управляемый
синтаксический анализатор плюс таблица, соответствующая синтаксису языка.
ЭКСПЕРИМЕНТЫ ПО СКАНИРОВАНИЮ Прежде чем возвратиться к нашему
компилятору, было бы полезно немного поэкспериментировать с общими понятиями.
<ident> ::= <letter> [
<letter> | <digit> ]*
(Не забудьте, что "*" указывает
на ноль или более повторений условия в квадратных скобках, а "+" на одно
и более.)
{--------------------------------------------------------------}
function IsAlNum(c: char): boolean;
Используя ее, давайте напишем следующие две подпрограммы, которые очень похожи на те, которые мы использовали раньше: {--------------------------------------------------------------}
function GetName: string;
{--------------------------------------------------------------}
function GetNum: string;
(Заметьте, что эта версия GetNum возвращает строку,
а не целое число, как прежде).
WriteLn(GetName); Эта программа выведет любое допустимое
набранное имя (максимум восемь знаков, потому что мы так сказали GetName).
Она отвергнет что-либо другое.
ПРОБЕЛ Раньше мы также работали с вложенными пробелами, используя две подпрограммы IsWhite и SkipWhite. Удостоверьтесь, что эти подпрограммы есть в вашей текущей версии Cradle и добавьте строку: SkipWhite; в конец GetName и GetNum.
{--------------------------------------------------------------}
Function Scan: string;
Мы можем вызвать ее из новой основной программы: {--------------------------------------------------------------}
begin
(Вы должны добавить описание
строки Token в начало программы. Сделайте ее любой удобной длины, скажем
16 символов).
КОНЕЧНЫЕ АВТОМАТЫ Подпрограмма анализа типа GetName действительно реализует конечный автомат. Состояние неявно в текущей позиции в коде. Очень полезным приемом для визуализации того, что происходит, является синтаксическая диаграмма или "railroad-track" диаграмма. Немного трудно нарисовать их в этой среде, поэтому я буду использовать их очень экономно, но фигура ниже должна дать вам идею:
Как вы можете видеть, эта
диаграмма показывает логические потоки по мере чтения символов. Начинается
все, конечно, с состояния "start" и заканчивается когда найден символ,
отличный от алфавитно-цифрового. Если первый символ не буква, происходит
ошибка. Иначе автомат продолжит выполнение цикла до тех пор, пока не будет
найден конечный разделитель.
НОВЫЕ СТРОКИ Продвигаясь прямо вперед, давайте
модифицируем наш сканер для поддержки более чем одной строки. Как я упомянул
последний раз, наиболее простой способ сделать это - просто обработать
символы новой строки, возврат каретки и перевод строки, как незаполненное
пространство. Фактически это способ, используемый подпрограммой iswhite
из стандартной библиотеки C. Прежде мы не этого делали. Я хотел бы сделать
это теперь, чтобы вы могли почувствовать результат.
IsWhite := c in [' ', TAB, CR, LF]; Мы должны дать основной программы новое условие останова, так как она никогда не увидит CR. Давайте просто используем: until Token = '.'; ОК, откомпилируйте эту программу и запустите ее. Попробуйте пару строк, завершаемых точкой. Я использовал: now is the time
Эй, что случилось? Когда я набрал
это, я не получил последний токен, точку. Программа не остановилась. Более
того, когда я нажал клавишу 'enter' несколько раз, я все равно не получил
точку.
{--------------------------------------------------------------}
begin
Обратите внимание на "охраняющую"
проверку, предшествующую вызову Fin. Это то, что заставляет все это работать,
и проверяет, то мы не пытаемся прочитать строку дальше.
while Look = CR do
Если, с другой стороны, вам нужен строчно-ориентированный язык подобный Ассемблеру, BASIC или FORTRAN (или даже Ada... заметьте, что он имеет комментарии, завершаемые новой строкой), тогда вам необходимо, чтобы Scan возвращал CR как токены. Он также должен съедать завершающие LF. Лучший способ сделать - использовать эту строку в самом начале Scan: if Look = LF then Fin; Для других соглашений вы будете должны использовать другие способы организации. В моем примере на последнем уроке я разрешил новые строки только в определенных местах, поэтому я занял какое-то промежуточное положение. В остальных частях этих занятий я буду выбирать такие способы обработки новых строк какие мне понравятся, но я хочу, чтобы вы знали, как выбрать для себя другой путь. ОПЕРАТОРЫ Мы могли бы сейчас остановиться
и иметь в своем распоряжении довольно полезный сканер. В тех фрагментах
KISS, которые мы построили, единственными токенами, состоящими из нескольких
символов, являются идентификаторы и числа. Все операторы были односимвольными.
Единственное исключение, которое я могу придумать - это операторы отношений
"<=", ">=" и "<>", но они могут быть обработаны как особые
случаи.
{--------------------------------------------------------------}
function IsOp(c: char): boolean;
Важно заметить, что мы не должны
включать в этот список каждый возможный оператор. К примеру круглые скобки
не включены, так же как и завершающая точка. Текущая версия Scan и так
хорошо поддерживает односимвольные операторы. Список выше включает только
те символы, которые могут появиться в многосимвольных операторах. (Для
конкретных языков список конечно всегда может быть отредактирован).
{--------------------------------------------------------------}
Function Scan: string;
Теперь испытайте программу. Вы убедитесь, что любые фрагменты кода, которые вы захотите бросить в нее будут аккуратно разложены на индивидуальные токены. СПИСКИ, ЗАПЯТЫЕ И КОМАНДНЫЕ СТРОКИ. Прежде чем возвратиться к основной
цели нашего обучения, я хотел бы немного выступить.
{--------------------------------------------------------------}
procedure SkipComma;
Эта процедура из восьми строк
пропустит разделитель, состоящий из любого числа (включая ноль) пробелов,
с нулем или одной запятой, вложенной в строку.
СТАНОВИТСЯ ИНТЕРЕСНЕЙ Хорошо, сейчас мы имеем довольно
хороший лексический анализатор, который разбивает входной поток на лексемы.
Мы могли бы использовать его как есть и иметь полезный компилятор. Но есть
некоторые другие аспекты лексического анализа, которые мы должны охватить.
Table[1] := 'IF';
что может получиться довольно длинным если есть много
ключевых слов.
{--------------------------------------------------------------}
type Symbol = string[8]; SymTab = array[1..1000] of Symbol; TabPtr = ^SymTab;
(Размерность, использованная в SymTab не настоящая...
память не распределяется непосредственно этим объявлением, а размерность
должна быть только "достаточно большой")
{--------------------------------------------------------------}
const KWlist: array [1..4] of Symbol =
{--------------------------------------------------------------} Затем, вставьте следующую новую функцию: {--------------------------------------------------------------}
{ If the input string matches a table entry, return the
entry
function Lookup(T: TabPtr; s: string; n: integer): integer;
Чтобы проверить ее вы можете временно изменить основную программу следующим образом: {--------------------------------------------------------------}
begin
Обратите внимание как вызывается
Lookup: функция Addr устанавливает указатель на KWList, который передается
в Lookup.
SymType = (IfSym, ElseSym, EndifSym, EndSym, Ident, Number, Operator); и договориться возвращать переменную этого типа. Давайте
попробуем это. Вставьте строку выше в описание типов.
Token: Symtype;
{ Current Token }
Измените сканер так: {--------------------------------------------------------------}
procedure Scan;
(Заметьте, что Scan сейчас стала
процедурой а не функцией).
{--------------------------------------------------------------}
begin
Мы заменили строку Token, используемую
раньше, на перечислимый тип. Scan возвращает тип в переменной Token и возвращает
саму строку в новой переменной Value.
{--------------------------------------------------------------}
procedure GetName;
{--------------------------------------------------------------}
procedure GetNum;
{--------------------------------------------------------------}
procedure GetOp;
{--------------------------------------------------------------}
procedure Scan;
ВОЗВРАЩЕНИЕ СИМВОЛА По существу, все сканер, которые
я когда-либо видел и которые написаны на Паскале, использовали механизм
перечислимых типов, который я только что описал. Это конечно работающий
механизм, но он не кажется мне самым простым подходом.
const KWcode: string[5] = 'xilee'; (Я буду кодировать все идентификаторы
одиночным символом 'x').
{--------------------------------------------------------------}
procedure GetName;
{--------------------------------------------------------------}
procedure GetNum;
{--------------------------------------------------------------}
procedure GetOp;
{--------------------------------------------------------------}
procedure Scan;
{--------------------------------------------------------------}
begin
Эта программа должна работать также как и предыдущая версия. Небольшое различие в структуре, может быть, но она кажется мне более простой. РАСПРЕДЕЛЕННЫЕ СКАНЕРЫ ПРОТИВ ЦЕНТРАЛИЗОВАННЫХ Структура лексического анализатора,
которую я только что вам показал, весьма стандартна и примерно 99% всех
компиляторов используют что-то очень близкое к ней. Это, однако, не единственно
возможная структура, или даже не всегда самая лучшая.
ОБЪЕДИНЕНИЕ СКАНЕРА И ПАРСЕРА Теперь, когда мы охватили всю
теорию и общие аспекты лексического анализа, я наконец готов подкрепит
свое заявление о том, что мы можем приспособить многосимвольные токены
с минимальными изменениями в нашей предыдущей работе. Для краткости и простоты
я ограничу сам себя подмножеством того, что мы сделали ранее: я разрешу
только одну управляющую конструкцию (IF) и никаких булевых выражений. Этого
достаточно для демонстрации синтаксического анализа и ключевых слов и выражений.
Расширение до полного набора конструкций должно быть довольно очевидно
из того, что мы уже сделали.
{--------------------------------------------------------------}
{--------------------------------------------------------------}
const TAB = ^I;
{--------------------------------------------------------------}
type Symbol = string[8]; SymTab = array[1..1000] of Symbol; TabPtr = ^SymTab; {--------------------------------------------------------------}
var Look : char;
{ Lookahead Character }
{--------------------------------------------------------------}
procedure GetChar;
{--------------------------------------------------------------}
procedure Error(s: string);
{--------------------------------------------------------------}
procedure Abort(s: string);
{--------------------------------------------------------------}
procedure Expected(s: string);
{--------------------------------------------------------------}
function IsAlpha(c: char): boolean;
{--------------------------------------------------------------}
function IsDigit(c: char): boolean;
{--------------------------------------------------------------}
function IsAlNum(c: char): boolean;
{--------------------------------------------------------------}
function IsAddop(c: char): boolean;
{--------------------------------------------------------------}
function IsMulop(c: char): boolean;
{--------------------------------------------------------------}
function IsWhite(c: char): boolean;
{--------------------------------------------------------------}
procedure SkipWhite;
{--------------------------------------------------------------}
procedure Match(x: char);
{--------------------------------------------------------------}
procedure Fin;
{--------------------------------------------------------------}
function GetName: char;
{--------------------------------------------------------------}
function GetNum: char;
{--------------------------------------------------------------}
function NewLabel: string;
{--------------------------------------------------------------}
procedure PostLabel(L: string);
{--------------------------------------------------------------}
procedure Emit(s: string);
{--------------------------------------------------------------}
procedure EmitLn(s: string);
{---------------------------------------------------------------}
procedure Ident;
{---------------------------------------------------------------}
procedure Expression; Forward; procedure Factor;
{---------------------------------------------------------------}
procedure SignedFactor;
{--------------------------------------------------------------}
procedure Multiply;
{-------------------------------------------------------------}
procedure Divide;
{---------------------------------------------------------------}
procedure Term1;
{---------------------------------------------------------------}
procedure Term;
{---------------------------------------------------------------}
procedure FirstTerm;
{---------------------------------------------------------------}
procedure Add;
{---------------------------------------------------------------}
procedure Subtract;
{---------------------------------------------------------------}
procedure Expression;
{---------------------------------------------------------------}
Procedure Condition;
{---------------------------------------------------------------}
procedure Block;
procedure DoIf;
{--------------------------------------------------------------}
procedure Assignment;
{--------------------------------------------------------------}
procedure Block;
{--------------------------------------------------------------}
procedure DoProgram;
{--------------------------------------------------------------}
procedure Init;
{--------------------------------------------------------------}
begin
Пара комментариев:
Если программа работает, тогда давайте поспешим. При добавлении модулей сканера в программу поможет систематический план. Во всех синтаксических анализаторах, которые мы написали до этого времени, мы придерживались соглашения, что текущий предсказывающий символ должен всегда быть непустым символом. Мы предварительно загружали предсказывающий символ в Init и после этого оставляли "помпу запущенной". Чтобы позволить программе работать правильно с новыми строками мы должны ее немного модифицировать и обрабатывать символ новой строки как допустимый токен. В многосимвольной версии правило аналогично: текущий предсказыващий символ должен всегда оставаться на начале следующей лексемы или на новой строке. Многосимвольная версия показана ниже. Чтобы получить ее я сделал следующие изменения:
{--------------------------------------------------------------}
{--------------------------------------------------------------}
const TAB = ^I;
{--------------------------------------------------------------}
type Symbol = string[8]; SymTab = array[1..1000] of Symbol; TabPtr = ^SymTab; {--------------------------------------------------------------}
var Look : char;
{ Lookahead Character }
{--------------------------------------------------------------}
const KWlist: array [1..4] of Symbol =
const KWcode: string[5] = 'xilee'; {--------------------------------------------------------------}
procedure GetChar;
{--------------------------------------------------------------}
procedure Error(s: string);
{--------------------------------------------------------------}
procedure Abort(s: string);
{--------------------------------------------------------------}
procedure Expected(s: string);
{--------------------------------------------------------------}
function IsAlpha(c: char): boolean;
{--------------------------------------------------------------}
function IsDigit(c: char): boolean;
{--------------------------------------------------------------}
function IsAlNum(c: char): boolean;
{--------------------------------------------------------------}
function IsAddop(c: char): boolean;
{--------------------------------------------------------------}
function IsMulop(c: char): boolean;
{--------------------------------------------------------------}
function IsWhite(c: char): boolean;
{--------------------------------------------------------------}
procedure SkipWhite;
{--------------------------------------------------------------}
procedure Match(x: char);
{--------------------------------------------------------------}
procedure Fin;
{--------------------------------------------------------------}
function Lookup(T: TabPtr; s: string; n: integer): integer;
{--------------------------------------------------------------}
procedure GetName;
{--------------------------------------------------------------}
procedure GetNum;
{--------------------------------------------------------------}
procedure Scan;
{--------------------------------------------------------------}
procedure MatchString(x: string);
{--------------------------------------------------------------}
function NewLabel: string;
{--------------------------------------------------------------}
procedure PostLabel(L: string);
{--------------------------------------------------------------}
procedure Emit(s: string);
{--------------------------------------------------------------}
procedure EmitLn(s: string);
{---------------------------------------------------------------}
procedure Ident;
{---------------------------------------------------------------}
procedure Expression; Forward; procedure Factor;
{---------------------------------------------------------------}
procedure SignedFactor;
{--------------------------------------------------------------}
procedure Multiply;
{-------------------------------------------------------------}
procedure Divide;
{---------------------------------------------------------------}
procedure Term1;
{---------------------------------------------------------------}
procedure Term;
{---------------------------------------------------------------}
procedure FirstTerm;
{---------------------------------------------------------------}
procedure Add;
{---------------------------------------------------------------}
procedure Subtract;
{---------------------------------------------------------------}
procedure Expression;
{---------------------------------------------------------------}
Procedure Condition;
{---------------------------------------------------------------}
procedure Block; Forward; procedure DoIf;
{--------------------------------------------------------------}
procedure Assignment;
{--------------------------------------------------------------}
procedure Block;
{--------------------------------------------------------------} { Parse and Translate a Program } procedure DoProgram;
{--------------------------------------------------------------} { Initialize } procedure Init;
{--------------------------------------------------------------}
begin
Сравните эту программу с ее односимвольным вариантом. Я думаю вы согласитесь, что различия минимальны. ЗАКЛЮЧЕНИЕ К этому времени вы узнали как
анализировать и генерировать код для выражений, булевых выражений и управляющих
структур. Теперь вы изучили, как разрабатывать лексические анализаторы
и как встроить их элементы в транслятор. Вы все еще не видели всех элементов,
объединеных в одну программу, но на основе того, что мы сделали ранее вы
должны прийти к заключению, что легко расширить наши ранние программы для
включения лексических анализаторов.
|