Не так редко возникает ситуация, когда приходится писать парсер. Написание парсера вручную с нуля — задача не очень сложная, но довольно нудная и однообразная. Поэтому…

Для генерации парсеров существуют известные утилиты yacc и lex. Они позволяют достаточно кратко описать синтаксис некоего языка и по этому описанию автоматически сгенерировать код парсера для него.

По какой-то причине я не смог найти их версию для Delphi. Есть довольно известный проект TP Lex and Yacc, его корни уходят аж в 1990 год (да-да, TP — это именно Turbo Pascal). Добрые люди его долгое время поддерживали, но несколько лет назад и их энтузиазм иссяк.

В итоге я решил попробовать самостоятельно реанимировать этот проект, взяв за основу последнюю его инкарнацию из найденных мной — Delphi Lex & Yacc. Были и другие попытки оживить проект, но по-моему эта попытка самая свежая.

Во-первых, пришлось внести некоторые изменения, чтобы код компилировался и работал в свежей версии Delphi. Во-вторых, я добавил немного объектно-ориентированности — все-таки в 2013 году не очень приятно интегрировать в свое приложение нечто пусть и полезное, но использующее кучу глобальных переменных и STDIN/STDOUT для организации ввода-вывода.

Так родился Next Delphi Lex & Yacc (ndyacclex).

Это только первый шаг. На чем-то серьезном я этот код пока не тестировал. Остановился на том, что добился корректной работы тестового приложения, прилагавшегося к оригинальной версии кода, немного переделав его под современные реалии.

К сожалению, все еще нет никакой поддержки юникода. Всюду AnsiString и AnsiChar. Пожалуй, этим вопросом я и займусь, но это явно потребует некоторого времени.

Для тех, кому лень качать, прямо здесь маленькое демо.

exprparse

Имея описание синтаксиса (в данном случае это арифметические выражения), утилиты ndyacclex позволяют сгенерировать исходный код класса-наследника TCustomParser, который затем можно использовать вот так:

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
var
  StrStream: TStringStream;
begin
  if Key = #13 then
  begin
    Key := #0;
    Memo1.Lines.Add('> ' + Edit1.Text);

    StrStream := TStringStream.Create;
    try
      StrStream.WriteString(Edit1.Text);
      StrStream.Position := 0;
      try
        Parser.parse(StrStream, WriteCB);
      except
        on E: EExprParserException do
          Memo1.Lines.Add(E.Message);
      end;
      Edit1.Text := '';
    finally
      StrStream.Free;
    end;
  end;
end;

procedure TForm1.WriteCB(Value: Real);
begin
  Memo1.Lines.Add(Format('%2.2f',[Value]));
end;

Это практически весь код тестового приложения, если не учитывать сгенерированный парсер.

Связанные записи: