June 8, 2023

Пишем интерпретатор на PascalABC.NET. Синтаксический анализатор. Часть 1. Базовый класс ParserBase

Этот текст - продолжение публикации https://teletype.in/@pascalabcnet/Lex4, в которой описывалось, как постороить лексический анализатор языка программирования.

Приступим к написанию синтаксического анализатора (парсера) простого языка программирования.

Описание языка

Наш язык будет содержать операторы присваивания, выражения с арифметическими и логическими операциями, условный оператор, оператор while, составной оператор и оператор вывода (точнее, оператор вызова стандартной функции вывода). Вот - простейшая программа на нашем языке:

i = 1; 
sum = 0; 
n = 100000000; 
while i<100000000 do 
{ 
  sum += 1/i; 
  i += 1 
};
Print(sum)

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

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

Код класса ParserBase

type 
  ParserBase = class
  private
    lex: Lexer;
  protected  
    curToken: Token;
  public
    constructor (lex: Lexer);
    begin
      Self.lex := lex;
      Advance;
    end;
    /// Вернуть текущий токен и перейти к следующему 
    function Advance: Token;
    begin
      Result := curToken;
      curToken := lex.NextToken;
    end;
    /// Проверить, что тип текушего токена совпадает с одним из данных типов
    function At(params types: array of TokenType): boolean;
    begin
      Result := types.Any(typ -> PeekToken.typ = typ);
    end;
    procedure Check(params types: array of TokenType);
    begin
      if not At(types) then
        ExpectedError(types)
    end;
    /// Проверить, что тип текушего токена совпадает с одним из данных типов
    /// В случае успеха перейти к следующему токену
    function IsMatch(params types: array of TokenType): boolean;
    begin
      Result := At(types);
      if Result then
        Advance;
    end;
    // Проверить, что тип текушего токена совпадает с одним из данных типов
    // В случае успеха вернуть текущий токен и продвинуться к следующему, 
    // а иначе выбросить ошибку
    function Requires(params types: array of TokenType): Token;
    begin
      if At(types) then
        Result := Advance
      else ExpectedError(types)
    end;
    /// Мы - в конце файла
    function IsAtEnd: boolean := PeekToken.typ = TokenType.Eof;
    /// Мы - в конце файла
    function PeekToken := curToken; 
    function CurrentToken := curToken;
    procedure ExpectedError(params types: array of TokenType) := 
      SyntaxError(#39;{types.JoinToString('' или '')} ожидалось, но {PeekToken.Typ} найдено',PeekToken.Pos);
  end;

Методы класса ParserBase

Рассмотрим каждый метод по отдельности.

В конструкторе ParserBase инициализируется лексер, представленный полем класса, и осуществляется пропуск первой лексемы вызовом метода Advance.

Метод Advance возвращает текущую лексему, находящуюся в поле curToken и считывает в поле curToken следующую лексему вызовом метода lex.NextToken.

Метод CurrentToken возвращает текущую прочитанную лексему, хранящуюся в переменной curToken. Метод PeekToken является его синонимом.

Метод IsAtEnd возвращает ситуацию конца файла, что означает, что в данный момент мы находимся на токене Eof:

function IsAtEnd := PeekToken.typ = TokenType.Eof;

Метод At возвращает True если тип текущего токена совпадает с одним из типов - параметров At. Например,

At(TokenType.Plus,TokenType.Minus)

возвращает True только если мы находимся на токене + или -.

Самыми интересными являются методы Check, IsMatch и Requires, вызывающие метод At и немного отличающиеся по функциональности.

Метод Check проверяет, что тип текущего токена совпадает с одним из данных типов и в случае неуспеха выбрасывает ошибку.

Метод IsMatch проверяет проверяет, что тип текущего токена совпадает с одним из данных типов и в случае успеха возвращает True и переходит к следующему токену, а при неуспехе возвращает False.

Метод Requires проверяет, что тип текушего токена совпадает с одним из данных типов, в случае успеха возвращает текущий токен и продвигается к следующему, а иначе выбрасывает ошибку.