Написание простого DSL компилятора на Delphi (7. Компилятор AST)
Перевод поста Writing a Simple DSL Compiler with Delphi (7. AST Compiler)open in new window.
Эта статья представляет собой описание компилятора AST используемого для проекта моего языка программирования. Если вы только начинаете читать эту серию, то я бы рекомендовал вам начать с этого постаopen in new window. Как минимум вы должны прочитать предыдущий пост Intermezzoopen in new window так как он разъясняет некоторые части компилятора которых я не касаюсь здесь.
В каркасе моего игрушечного компилятора, компилятор (или codegen
, как он называется внутри) — часть кода которая реализует интерфейс ISimpleDSLCodegen
. Этот интерфейс предоставляет только одну функцию, Generate
, которая принимает абстрактное синтаксическое дерево и преобразует его в объект, который реализует интерфейс ISimpleDSLProgram
, который позволяет вам вызывать любую функцию скомпилированной программы по имени.
type
TParameters = TArray;
TFunctionCall = reference to function (const parameters: TParameters): integer;
ISimpleDSLProgram = interface ['{2B93BEE7-EF20-41F4-B599-4C28131D6655}']
function Call(const functionName: string; const params: TParameters; var return: integer): boolean;
end;
ISimpleDSLCodegen = interface ['{C359C174-E324-4709-86EF-EE61AFE3B1FD}']
function Generate(const ast: ISimpleDSLAST;
var runnable: ISimpleDSLProgram): boolean;
end;
2
3
4
5
6
7
8
9
10
11
Компилятор по умолчанию реализован классом TSimpleDSLCodegen
в модуле SimpleDSLCompiler.Compiler
. Методы в этом классе в основном занимаются чтением и пониманием AST пока код, фактически, создаётся в методах в модуле SimpleDSLCompiler.Compiler.Codegen
.
Этот компилятор создаёт программу которая является интерфейсом класса TSimpleDSLProgram
(также находящемся в SimpleDSLCompiler.Compiler
).
Функционирование компилятора очень похоже на компилятор представленный в Intermezzo — с одним критичным отличием. Выражения в моём игрушечном языке могут использовать параметры функций как слагаемые. Поэтому вычислитель выражений должен иметь доступ к параметрам текущей функции.
История начинается в методе TSimpleDSLCodegen.Generate
который для каждой функции в дереве сначала компилирует тело функции (CompileBlock
) и затем генерирует функциональную обёртку для этого тела (CodegenFunction
).
function TSimpleDSLCodegen.Generate(const ast: ISimpleDSLAST; var runnable:
ISimpleDSLProgram): boolean;
var
block : TStatement;
i : integer;
runnableInt: ISimpleDSLProgramEx;
begin
Result := false; //to keep compiler happy
FAST := ast;
runnable := TSimpleDSLProgram.Create;
runnableInt := runnable as ISimpleDSLProgramEx;
for i := 0 to ast.Functions.Count - 1 do begin
if not CompileBlock(ast.Functions[i].Body, block) then
Exit;
runnableInt.DeclareFunction(i, ast.Functions[i].Name, CodegenFunction(block));
end;
Result := true;
end;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Давайте начнём с последней функции так как она даст нам больше контекста (каламбур, как вы увидите скоро).
type
PExecContext = ^TExecContext;
TExecContext = record
Functions: TArray;
end;
TParameters = TArray;
function CodegenFunction(const block: TStatement): TFunction;
begin
Result :=
function (execContext: PExecContext; const params: TParameters): integer
var
context: TContext;
begin
context.Exec := execContext;
context.Params := params;
context.Result := 0;
block(context);
Result := context.Result;
end;
end;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CodegenFunction
создаёт основную обёртку в виде анонимного метода для текущей функции. Этот анонимный метод получит контекст выполнения, который позволит этой функции вызывать другие функции. Затем анонимный метод построит контекст функции который грубо соответствует стековому фрейму (stack frameopen in new window) производимому "нормальным" компилятором. Этот контекст хранит указатель на контекст выполнения и копию параметров (значений) переданных в функцию. Затем он вызывает block(context)
для выполнения переданного блока.
Спустимся на один уровень ниже... Функция TSimpleDSLCodegen.CompileBlock
компилирует каждое выражение в блоке вызовом CompileStatement
и затем вызывает CodegenBlock
для обёртки скомпилированных выражений в блок.
function CodegenBlock(const statements: TStatements): TStatement;
begin
Result :=
procedure (var context: TContext)
var
stmt: TStatement;
begin
for stmt in statements do
stmt(context);
end;
end;
2
3
4
5
6
7
8
9
10
11
Скомпилированный код, который реализует блок, снова является анонимным методом. Он принимает контекст (передаваемый анонимным методом реализующим функцию) и просто передаёт этот же контекст во все выражения в блоке.
Это продолжается и продолжается. Большая часть кода довольна скучна и предсказуема. Например, это метод который генерирует код для оператора if
.
function CodegenIfStatement(const condition: TExpression; const thenBlock,
elseBlock: TStatement): TStatement;
begin
Result :=
procedure (var context: TContext)
begin
if condition(context) <> 0 then
thenBlock(context)
else
elseBlock(context);
end;
end;
2
3
4
5
6
7
8
9
10
11
12
Вещи становятся интереснее как только мы хотим скомпилировать слагаемое. Слагаемое может представлять константу (целое число), параметр (названный variable
в codegen так как в будущем может быть добавлена поддержка переменных) или вызовом функции.
function TSimpleDSLCodegen.CompileTerm(const astTerm: IASTTerm; var codeTerm: TExpression): boolean;
var
termConst : IASTTermConstant;
termFuncCall: IASTTermFunctionCall;
termVar : IASTTermVariable;
begin
Result := true;
if Supports(astTerm, IASTTermConstant, termConst) then
codeTerm := CodegenConstant(termConst.Value)
else if Supports(astTerm, IASTTermVariable, termVar) then
codeTerm := CodegenVariable(termVar.VariableIdx)
else if Supports(astTerm, IASTTermFunctionCall, termFuncCall) then
Result := CompileFunctionCall(termFuncCall, codeTerm)
else
Result := SetError('*** Unexpected term');
end;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Компиляция константы тривиальна. Нам просто нужна функция возвращающая эту константу.
function CodegenConstant(value: integer): TExpression;
begin
Result :=
function (var context: TContext): integer
begin
Result := value;
end;
end;
2
3
4
5
6
7
8
Доступ к параметрам немного сложнее. AST содержит индекс этого параметра и мы просто должны применить его к свойству Params
контекста.
function CodegenVariable(varIndex: integer): TExpression;
begin
Result :=
function (var context: TContext): integer
begin
Result := context.Params[varIndex];
end;
end;
2
3
4
5
6
7
8
Генерация вызова функции немного более сложная. Она должна настроить массив параметров который будет передан в вызов функции, найти корректную функцию через контекст запуска и затем вызывать функцию.
function CodegenFunctionCall(funcIndex: integer; const params: TFuncCallParams): TExpression;
begin
Result :=
function (var context: TContext): integer
var
funcParams: TParameters;
iParam : Integer;
begin
SetLength(funcParams, Length(params));
for iParam := Low(params) to High(params) do
funcParams[iParam] := params[iParam](context);
Result := context.Exec.Functions[funcIndex](context.Exec, funcParams);
end;
end;
2
3
4
5
6
7
8
9
10
11
12
13
14
В конце компилятор производит большой анонимный метод, использующий внутри другие анонимные методы, и который при вызове возвращает результат.
Например, эта минимальная программа ...
inc(i) { return i+1 }
... генерирует что-то похожее на следующее чудовище. В реальности код даже более странный так как он должен обрабатывать захваченные переменные.
function (execContext: PExecContext; const params: TParameters): integer
var
context: TContext;
begin
context.Exec := execContext;
context.Params := params;
context.Result := 0;
(procedure (var context: TContext)
var
stmt: TStatement;
begin
for stmt in [
procedure (var context: TContext)
begin
context.Result :=
(function (var context: TContext): integer
begin
Result :=
(function (var context: TContext): integer
begin
Result := context.Params[0];
end)(context)
+
(function (var context: TContext): integer
begin
Result := 1;
end)(context);
end)(context);
end
]
do
stmt(context);
end)(context);
Result := context.Result;
end;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Он определённо не для слабонервных, но вы не должны смотреть скомпилированный код (конечно, исключая случая отладки компилятора).
Вы можете удивится скорости этого кода. Должен признать — не очень быстро. Я дам вам более точные цифры в следующей части этой серии которая будет описывать интерпретатор для этого языка.