Анимация
JavaScript
|
Главная Библионтека ЭКСПЕРИМЕНТЫ ПО СКАНИРОВАНИЮ Прежде чем возвратиться к нашему компилятору, было бы полезно немного поэкспериментировать с общими понятиями. Давайте начнем с двух определений, наиболее часто встречающихся в настоящих языках программирования: <ident> ::= <letter> [ <letter> <digit> ]* <number ::= [<digit>]+ (Не забудьте, что "*" указывает на ноль или более повторений условия в квадратных скобках, а "+" на одно и более.) Мы уже работали с подобными элементами в третьей главе. Давайте начнем (как обычно) с пустого Cradle. Не удивительно, что нам понадобится новая процедура распознавания: { Recognize an Alphanumeric Character } function IsAlNum(c: char): boolean; begin IsAlNum := IsAlpha(c) or IsDigit(c); end; Используя ее, давайте напишем следующие две подпрограммы, которые очень похожи на те, которые мы использовали раньше: { Get an Identifier } function GetName: string; var x: string[8]; begin x := ; if not IsAlpha(Look) then Expected(Name); while IsAlNum(Look) do begin x := x + UpCase(Look); GetChar; end; GetName := x; end; { Get a Number } function GetNum: string; var x: string[16]; begin x := ; if not IsDigit(Look) then Expected(Integer); while IsDigit(Look) do begin x := x + Look; GetChar; end; GetNum := x; end; (Заметьте, что эта версия GetNum возвращает строку, а не целое число, как прежде). Вы можете легко проверить что эти подпрограммы работают, вызвав их из основной программы: WriteLn(GetName); Эта программа выведет любое допустимое набранное имя (максимум восемь знаков, потому что мы так сказали GetName). Она отвергнет что-либо другое. Аналогично проверьте другую подпрограмму. ПРОБЕЛ Раньше мы также работали с вложенными пробелами, используя две подпрограммы IsWhite и SkipWhite. Удостоверьтесь, что эти подпрограммы есть в вашей текущей версии Cradle и добавьте строку: SkipWhite; в конец GetName и GetNum. Теперь давайте определим новую процедуру: { Lexical Scanner } Function Scan: string; begin if IsAlpha(Look) then Scan := GetName else if IsDigit(Look) then Scan := GetNum else begin Scan := Look; GetChar; end; SkipWhite; end; Мы можем вызвать ее из новой основной программы: { Main Program } begin Init; repeat Token := Scan; writeln(Token); until Token = CR; end. (Вы должны добавить описание строки 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 = ОК, откомпилируйте эту программу и запустите ее. Попробуйте пару строк, завершаемых точкой. Я использовал: now is the time for all good men. Эй, что случилось? Когда я набрал это, я не получил последний токен, точку. Программа не остановилась. Более того, когда я нажал клавишу enter несколько раз, я все равно не получил точку. Если вы все еще не можете выбраться из вашей программы, вы обнаружите, что набор точки в новой строке прервет ее. Что здесь происходит? Ответ в том, что мы зависаем в SkipWhite. Короткий осмотр этой подпрограммы покажет, что пока мы печатаем пустые строки, мы просто продолжаем выполнение цикла. После того, как SkipWhite встречает LF, он пытается выполнить GetChar. Но так как входной буфер теперь пуст, оператор чтения в GetChar настаивает на наличии другой строки. Процедура Scan получает завершающую точку, все правильно, но она вызывает SkipWhite и SkipWhite не возвращается до тех пор, пока не получит непустую строку. Такое поведение не настолько плохое, как кажется. В настоящем компиляторе мы читали бы символы из входного файла вместо консоли и пока мы имеем какую-то процедуру для работы с концом файла, все получится ОК. Но для чтения данных с 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 [ 25 ] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |