Пишем интерпретатор на PascalABC.NET. Лексический анализатор. Часть 2
Данный текст - продолжение https://teletype.in/@pascalabcnet/Lex1
Следующая статья - https://teletype.in/@pascalabcnet/Lex3
Давайте разберемся, как разбить текст программы на лексемы и написать класс 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; В следующей части мы расскажем, как на базе данного базового лексического анализатора построить лексический анализатор для конкретного языка.