Написание простого DSL компилятора на Delphi (6. Дамп AST)
Перевод поста Writing a Simple DSL Compiler with Delphi (6. AST Dumper)open in new window.
Эта статья представляет описание инструмента для тестирования моего игрушечного языка программирования. Если вы только начинаете читать эту серию, то я бы рекомендовал вам начать с этого постаopen in new window.
Пожалуйста, имейте в виду, что эта статья описывают начальную реализацию парсера. Если вы хотите просматривать код во время чтения статьи, убедитесь, что вы переключились на ветку dsl_v1open in new window.
Теперь, когда мы имеем работающий токинезаторopen in new window и парсерopen in new window генерирующие на выходе ASTopen in new window, мы можем начать работать над компилятором. Тем не менее, было бы отлично проверить корректность выходных данных парсера. Или по другому — нам нужны модульные тесты.
Тем не менее, написание модульных тестов для древовидных структур очень утомительная операция. В следующем посту я покажу тест для дерева в котором только пять листьев и это уже будет процесс который лучше пропустить. К счастью, мы можем сделать что-то более весёлое — мы можем написать код, который воссоздаст оригинальную программу из AST.
Чтобы сделать это, мы напишем генератор кода который вместо машинного кода генерирует исходную программу. Мы можем сделать просто поместив новую фабрику codegen
в гибкий каркас компилятораopen in new window.
sl := TStringList.Create;
try
compiler := CreateSimpleDSLCompiler;
compiler.CodegenFactory :=
function: ISimpleDSLCodegen
begin
Result := CreateSimpleDSLCodegenDump(sl);
end;
compiler.Compile(CMultiProcCode);
Writeln(sl.Text);
finally FreeAndNil(sl); end;
2
3
4
5
6
7
8
9
10
11
codegen
будет сохранять результирующую программу в список строк sl
, который мы можем передать в анонимную процедуру (CodegenFactory
) из-за полезного захвата переменнойopen in new window.
Наш генератор кода — TSimpleDSLCodegenDump
должен реализовывать интерфейс ISimpleDSLCodegen
содержащий одну функцию — Generate
. Эта функция берет AST и возвращает что-то запускаемое — ISimpleDSLProgram
. Поскольку наш код не знает как создавать запускаемую программу, он будет возвращать nil
в этом параметре.
type
ISimpleDSLCodegen = interface ['{C359C174-E324-4709-86EF-EE61AFE3B1FD}']
function Generate(const ast: ISimpleDSLAST; var runnable: ISimpleDSLProgram): boolean;
end;
2
3
4
Полезные данные будут сохранены в список строк (TStringList
) который передан в конструктор.
constructor TSimpleDSLCodegenDump.Create(dump: TStringList);
begin
inherited Create;
FDump := dump;
end;
2
3
4
5
Основная функция генерации кода сохраняет ссылку на AST в поле так что мы имеем доступ к нему из всех методов и затем восстаёт исходный код функция за функцией вызовами DumpFunction
.
function TSimpleDSLCodegenDump.Generate(const ast: ISimpleDSLAST;
var runnable: ISimpleDSLProgram): boolean;
var
i: integer;
begin
FErrors := false;
FAST := ast;
for i := 0 to ast.Functions.Count - 1 do begin
if i > 0 then begin
WritelnText;
WritelnText;
end;
DumpFunction(ast.Functions[i]);
end;
FDump.Text := FText;
Result := not FErrors;
end;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Код программы собирается в поле FText: string
. Два вспомогательных метода WritelnText
используются для добавления данных в это поле.
procedure TSimpleDSLCodegenDump.WritelnText(const s: string);
begin
WriteText(s);
WriteText(#13#10);
end;
procedure TSimpleDSLCodegenDump.WriteText(const s: string);
begin
FText := FText + s;
end;
2
3
4
5
6
7
8
9
10
Я покажу только две части этого "генератора кода" так как он довольно скучен. Как первый пример, эта функция записывает исходный код одной функции.
procedure TSimpleDSLCodegenDump.DumpFunction(const func: TASTFunction);
begin
FCurrentFunc := func;
WriteText(Format('%s(%s) ', [func.Name, ''.Join(',', func.ParamNames.ToArray)]));
DumpBlock('', func.Body);
FCurrentFunc := nil;
end;
2
3
4
5
6
7
Ссылка на текущую функцию сохраняется снаружи так как она нужна нам в методе который записывает название переменной (не показан здесь). Затем мы записываем название функции, с последующим списком параметров. Хелпер Join
для строк соединяет параметры в одну строку с разделителем в виде запятой. В конце мы вызываем DumpBlock
для записи тела функции.
Во втором примере, метод DumpExpression
, записывает выражение. Он вызывает DumpTerm
для записи первого слагаемого, записывает оператор (если он есть) и записывает второе слагаемое.
procedure TSimpleDSLCodegenDump.DumpExpression(const expr: TASTExpression);
begin
DumpTerm(expr.Term1);
case expr.BinaryOp of
opNone: Exit;
opAdd: WriteText(' + ');
opSubtract: WriteText(' - ');
opCompareLess: WriteText(' < ');
else begin
WritelnText('*** Unexpected operator');
FErrors := true;
end;
end;
DumpTerm(expr.Term2);
end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Если мы запустим этот "генератор кода" на простой программе вычисляющей i-тое число Фибоначчи, то...
'fib(i) { '#13#10 +
' if i < 3 { '#13#10 +
' return 1 '#13#10 +
' } else { '#13#10 +
' return fib(i-2) + fib(i-1) '#13#10 +
' } '#13#10 +
'} '
2
3
4
5
6
7
...мы увидим что результат довольно близок к оригиналу
fib(i) {
if i < 3 {
return 1
} else {
return fib(i - 2) + fib(i - 1)
}
}
2
3
4
5
6
7
Разделители немного отличаются, но это не имеет значения так как мой игрушечный язык игнорирует пробельные символы. Это даёт нам уверенность что AST действительно правильно сгенерированно (как минимум в некоторых случаях). Чтобы быть более уверенными в правильной работе парсера мы могли бы написать больше тестов... или мы можем просто поиграть с компилятором, написать больше программ и проверить что они работают корректно. Однако, чтобы это сделать нам нужен сам компилятор.