May 22, 2023

Пишем интерпретатор на 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;    

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