Генерация кода
На основе исходного представления, которое формулирует технолог, нужно сгенерировать код для компиляции. Исходное представление может быть любым, в простейшем случае - это обычный текст. В процессе генерации кода наибольшее внимание нужно уделить диагностике ошибок. То есть, ошибки желательно выявить во время генерации кода и генерировать уже синтаксически правильный код. Для этого можно использовать любые доступные методы, вплоть до синтаксических анализаторов с рекурсивным спуском - такие анализаторы достаточно просты и описаны во многих книгах, например у Бьерна Страуструпа в "Язык программирования C++" (Третье издание). Если есть возможность, то желательно контролировать также семантическую правильность. Далее я буду рассматривать только те моменты, которые являются общими для всех задач без учета их специфики.
Генерировать исходный текст можно любым способом, например, просто посылая строки текста в файл. Более удобный способ, как мне кажется, это направление текста в строко-ориентированный поток. Такой поток предоставляет дополнительное удобство при диагностике ошибок. Библиотека DccUsing содержит два потоковых класса: TFileCompileOut и TStringCompileOut, которые порождаются от TCompileOut. Классы очень просты, их реализацию можно посмотреть в исходном файле библиотеки, поэтому я дам только обзор. Базовый класс имеет методы:
public procedure IncLevel; procedure DecLevel; procedure AddSpace; procedure AddLine(const aLine: String); procedure AddLines(const aLines: array of String); procedure AddFile(const aFileName: String); procedure AddLineTemplate(const aLine: String; const aArgs: array of String); procedure AddLinesTemplate(const aLines, aArgs: array of String); procedure AddFileTemplate(const aFileName: String; const aArgs: array of String); procedure AddPoint(aPoint: Integer); function FindPoint(aLine: Integer): Integer; property Level: Integer read FLevel; property LinesCount: Integer read FLinesCount;
Первые три метода позволяют управлять форматированием кода. Хотя форматирование совсем не обязательно (код никто не читает), но дает удобства при отладке, а, кроме того, мне нравится, когда программа выглядит эстетично. IncLevel увеличивает отступ текста, DecLevel уменьшает, а AddSpace добавляет в поток пустую строку. Два следующих метода добавляют в поток соответственно строку и массив строк, а метод AddFile - весь указанный файл. Свойства позволяют узнать текущий уровень отступа и текущее число строк в потоке. Назначение методов AddPoint и FindPoint будет объяснено в разделе диагностики ошибок.
Методы AddLineTemplate, AddLinesTemplate и AddFileTemplate более сложны, чем предыдущие методы, представляют собой простые макропроцессоры и позволяют параметризовать генерируемый текст. Параметризующие аргументы - это массив строк, которые заменяют метасимволы в исходном тексте шаблона. Метасимволы выглядят так: {{X}}, где Х - это порядковый номер аргумента, начиная от 1. Макроподстановка производится без всякого учета лексики. Поэтому можно параметризовать все что угодно - идентификаторы, строки, комментарии, операторы и т.д. Например, если шаблон текста таков:
const tFunc: array[0..5] of String = ( 'function {{1}}.SortProc{{2}}(const a1, a2: {{2}}): Integer;', 'begin', ' if a2 > a1 then result := 1', ' else if a2 = a1 then result := 0', ' else result := -1;', 'end;' ); то при использовании
c.AddLinesTemplate(tFunc,['TTestClass1','Integer']); мы получим такой результат:
function TTestClass1.SortProcInteger(const a1, a2: Integer): Integer; begin if a2 > a1 then result := 1 else if a2 = a1 then result := 0 else result := -1; end; а при использовании
c.AddLinesTemplate(tFunc,['TTestClass2','String']); такой:
function TTestClass2.SortProcString(const a1, a2: String): Integer; begin if a2 > a1 then result := 1 else if a2 = a1 then result := 0 else result := -1; end;
Наследуемые классы переопределяют абстрактную процедуру записи строки в поток и имеют специфические методы. Класс TFileCompileOut специализируется на построчном выводе в файл:
public constructor Create(const aFileName: String); destructor Destroy; override; property FileName: String read FFileName;
Конструктор принимает имя файла и открывает файл на чтение, а деструктор закрывает файл.
Класс TStringCompileOut хранит генерируемый текст в памяти:
public procedure Clear; procedure SaveToFile(const aFileName: String); procedure SaveToOut(aOut: TCompileOut); property Capacity: Integer ... property Items[aIndex: Integer]: String ... default;
Методы класса позволяют очистить поток, сохранить поток в файле и добавить его к другому потоку. Свойства позволяют изменить резервируемый объем памяти для списка строк и получить доступ на запись и чтение строк по индексу. Общее число строк определяет наследуемое свойство LinesCount. Примеры использования этих классов смотрите в DccExamples.pas.
Отметим, что часть неизменяемого или шаблонного кода может быть заготовлена заранее, располагаться в файлах и объединяться в нужных местах результирующего кода с помощью AddFile и AddFileTemplate. По ходу генерации кода может быть создано несколько потоков - для деклараций переменных и констант, деклараций и реализаций классов и так далее. После просмотра всей задачи, сформулированной технологом, эти потоки сшиваются в один результирующий поток. Для частных потоков можно использовать строковую реализацию, а для результирующего потока - файловую.