Пишем интерпретатор на PascalABC.NET. Лексический анализатор. Часть 2
Данный текст - продолжение https://teletype.in/@pascalabcnet/Lex1
Давайте разберемся, как разбить текст программы на лексемы и написать класс Lexer лексического анализатора.
Общие типы и подпрограммы
Каждый токен начинается с определенной позиции в тексте. Текст состоит из строк и столбцов, поэтому опишем тип
type Position = auto class Line: integer; Col: integer; end;
и с каждым токеном свяжем позицию его начала. Пока опишем базовый тип токена без привязки к конкретному типу токена:
type TokenBase = class public Pos: Position; // позиция начала токена Value: Object; constructor (Pos: Position; Value: Object) := (Self.Pos, Self.Value) := (Pos, Value); end;
Здесь Value - значение токена. Это как правило текстовое представление токена, но для целых и вещественных Value будет хранить соответствующее числовое значение.
Теперь опишем класс LexerBase - предок всех лексических анализаторов:
LexerBase = class protected code: string; // код программы. Инициализируется в конструкторе line := 1; // текущая строка column := 0; // текущий столбец cur := 0; // текущая позиция start := 0; // начальная позиция токена. // code[start:cur] - текущий токен atEoln: boolean := False; // сервисное поле для метода NextChar function CurrentPosition := new Position(line,column); function IsAtEnd := cur >= code.Length; ... end;
Здесь code - текст программы, (line, column) - текущая позиция в коде, cur - индекс текущего символа в строке code (строки будем индексировать с нуля), start - начальная позиция токена в этой же строке, atEoln - сервисный флаг, равный True если функция NextChar в последний вызов вернула запоминающий, побывали ли мы только что на конце строки (используется в функции NextChar).
Реализуем несколько простых методов, необходимых в каждом лексере:
/// Вернуть текущий символ function PeekChar: char := IsAtEnd ? #0 : code[cur]; /// Вернуть следующий символ function PeekNextChar: char; begin var pos := cur + 1; Result := pos > code.Length ? #0 : code[pos]; end; /// Является ли символ буквой static function IsAlpha(c: char) := Regex.IsMatch(c, '[A-Za-zА-Яа-яёЁ_]'); /// Является ли символ буквой или цифрой static function IsAlphaNumeric(c: char) := IsAlpha(c) or c.IsDigit;
Здесь надо обратить внимание, что функция PeekChar возвращает текущий символ в коде и не продвигается вперед. А если символов больше не осталось (мы прочитали всю программу), то возвращается символ с кодом 0.
Важнейшим сервисным методом в базовом лексическом анализаторе является метод NextChar - он возвращает текущий символ в коде и переходит к следующему:
function NextChar: char; begin Result := PeekChar; // вернуть текущий символ if atEoln then begin atEoln := False; line += 1; column := 0; end; if Result = #0 then exit; if Result = #10 then atEoln := True; cur += 1; // перейти к следующему column += 1; end;
Он достаточно непрост, поэтому разберем его подробнее.
Прежде всего функция возвращает текущий символ.
Если atEoln равен True, то при предыдущем вызове NextChar мы вернули символ перехода на следующую строку, и значит, надо сбросить этот флаг в False, продвинуться вперед к следующей строке и позицию column начала в этой строке обнулить.
Если мы встретили конец файла, то выйти из NextChar и не продвигаться вперед.
Если встреченный символ - переход на новую строку, то взвести флаг atEoln (для следующего вызова NextChar)
Наконец, в любом случае продвинуть индексы cur и column вперед на 1 (если column был обнулен, то он как раз примет значение 1, что означает, что мы стоим в начале строки.
Наконец, реализуем еще одну сервисную функцию IsMatch - для определения того, соответствует ли текущий символ указанному в параметре:
// Если текущий символ = expected, то продвигаемся вперед function IsMatch(expected: char): boolean; begin Result := PeekChar = expected; if Result then NextChar; end;
Обратим внимание, что если соответствует, то мы продвигаемся к следующему символу, в противном случае стоим на месте.
Все сервисные функции и поля объявим как protected - мы будем ими пользоваться в конкретном потомке класса LexerBase, реализующим конкретный лексер.
Наконец, в публичной секции LexerBase объявим конструктор и функцию Lines, разбивающую код на строки:
function Lines: array of string := code.Split(#10); constructor (code: string) := Self.code := code;
Полный код класса LexerBase
LexerBase = class protected code: string; // код программы. Инициализируется в конструкторе line := 1; // текущая строка column := 0; // текущий столбец cur := 0; // текущая позиция start := 0; // начальная позиция токена. atEoln: boolean := False; // сервисное поле для метода NextChar function CurrentPosition := new Position(line,column); function IsAtEnd := cur >= code.Length; /// Возвращает текущий символ и переходит к следующему function NextChar: char; begin Result := PeekChar; // вернуть текущий символ if atEoln then begin atEoln := False; line += 1; column := 0; end; if Result = #0 then exit; if Result = #10 then atEoln := True; cur += 1; // перейти к следующему column += 1; end; // Если текущий символ = expected, то продвигаемся вперед function IsMatch(expected: char): boolean; begin Result := PeekChar = expected; if Result then NextChar; end; /// Вернуть текущий символ function PeekChar: char := IsAtEnd ? #0 : code[cur]; /// Вернуть следующий символ function PeekNextChar: char; begin var pos := cur + 1; Result := pos > code.Length ? #0 : code[pos]; end; /// Является ли символ буквой static function IsAlpha(c: char) := Regex.IsMatch(c, '[A-Za-zА-Яа-яёЁ_]'); /// Является ли символ буквой или цифрой static function IsAlphaNumeric(c: char) := IsAlpha(c) or c.IsDigit; public function Lines: array of string := code.Split(#10); constructor (code: string) := Self.code := code; end;
В следующей части мы расскажем, как на базе данного базового лексического анализатора построить лексический анализатор для конкретного языка.