Пишем интерпретатор на PascalABC.NET. Интерпретатор по синтаксическому дереву
Этот текст - продолжение публикации https://teletype.in/@pascalabcnet/Parse3, в которой описывалось, как построить абстрактное синтаксическое дерево.
Перейдем к тому, ради чего мы публикуем серию этих статей - к написанию собственно интерпретатора. Исходные коды интерпретатора - здесь.
Для простоты интегрируем методы Eval() и Execute() прямо в узлы синтаксического дерева. Метод Eval() будет содержаться во всех узлах, производных от ExprNode, вычислять выражения и возвращать для простоты вещественное. Метод Execute() будет содержаться во всех узлах, производных от StatementNode, и будет выполнять (интерпретировать) конкретные операторы.
type Node = class pos: Position; end; ExprNode = class(Node) public function Eval: real; virtual := 0; end; StatementNode = class(Node) public procedure Execute; virtual := begin end; end;
Рассмотрим вычисление бинарной операции в узле BinOpNode:
BinOpNode = class(ExprNode) public Left,Right: ExprNode; Op: char; constructor (Left,Right: ExprNode; Op: char; pos: Position) := (Self.Left,Self.Right,Self.Op,Self.pos) := (Left,Right,Op,pos); function Eval: real; override; begin var l := Left.Eval; var r := Right.Eval; case Op of '+' : Result := l + r; '*' : Result := l * r; '/' : Result := l / r; '<' : Result := l < r ? 1 : 0; end; end; end;
Мы видим, что для вычисления результата бинарной операции необходимо вычислить значения левого и правого операнда, а потом применить к ним нужную бинарную операцию. Обратим внимание, что операция < возвращает 1 если выражение истинно и 0 если выражение ложно.
Хорошо видно, что мы реализовали не все возможные операции - их несложно пополнить в операторе case. Основная проблема здесь - операции, имеющие для своего обозначения более одного символа - например, <=. В нашем случае приходится кодировать каждую такую операцию отдельным символом. В будущем для всех типов операций сделаем перечислимый тип.
Далее рассмотрим вычисление более простых выражений: целых и вещественных констант:
type IntNode = class(ExprNode) public Value: integer; constructor(Value: integer; pos: Position) := (Self.Value,Self.pos) := (Value,pos); function Eval: real; override := Value; end; DoubleNode = class(ExprNode) public Value: real; constructor(Value: real; pos: Position) := (Self.Value,Self.pos) := (Value,pos); function Eval: real; override := Value; end;
Здесь всё просто: в качестве значения константы возвращается запомненное значение Value при создании узла.
Далее рассмотрим выполнение блока, моделируемого узлом синтаксического дерева StatementListNode. Здесь мы переопределяем метод Execute:
StatementListNode = class(StatementNode) public lst: List<StatementNode> := new List<StatementNode>; procedure Add(st: StatementNode) := lst.Add(st); procedure Execute; override; begin foreach var st in lst do st.Execute; end; end;
Суть проста - чтобы выполнить блок операторов, необходимо последовательно выполнить каждый оператор.
Следующим по сложности является реализация выполнения оператора цикла while:
type WhileNode = class(StatementNode) public Condition: ExprNode; Stat: StatementNode; constructor(Condition: ExprNode; Stat: StatementNode; pos: Position) := (Self.Condition,Self.Stat,Self.pos) := (Condition,Stat,pos); procedure Execute; override; begin while Condition.Eval > 0 do Stat.Execute; end; end;
В методе Execute всякий раз вычисляется условие, хранящееся в узле Condition, и пока оно истинно, выполняется тело цикла Stat.Execute.
Аналогично несложно реализуется выполнение условного оператора:
IfNode = class(StatementNode) public Condition: ExprNode; ThenStat,ElseStat: StatementNode; constructor(Condition: ExprNode; ThenStat,ElseStat: StatementNode; pos: Position) := (Self.Condition,Self.ThenStat,Self.ElseStat,Self.pos) := (Condition,ThenStat,ElseStat,pos); procedure Execute; override; begin if Condition.Eval > 0 then ThenStat.Execute else if ElseStat <> nil then ElseStat.Execute end; end;
Здесь важной является проверка узла ElseStat на непустоту: ветвь else выполняется только если соответствующий ему узел ElseStat непуст.
Наиболее интересным является реализация выполнения оператора присваивания. Для сохранения значений переменных во время выполнения создадим словарь, который ставит в соответствие имени переменной ее значение во время выполнения:
var VarValues := new Dictionary<string,real>;
В более сложных языках, где в разных пространствах имен будут переменные с одинаковыми именами, такая структура будет сложнее.
Теперь вернемся к методу выполнения в узле AssignNode:
AssignNode = class(StatementNode) public Ident: IdNode; Expr: ExprNode; constructor(Ident: IdNode; Expr: ExprNode; pos: Position) := (Self.Ident,Self.Expr,Self.pos) := (Ident,Expr,pos); procedure Execute; override; begin VarValues[Ident.Name] := Expr.Eval; end; end;
Нетрудно видеть, что для выполнения оператора присваивания мы просто меняем значение VarValues[Ident.Name] в словаре VarValues. При первом присваивании переменной словарь пополняется данной переменной, при последующих присваиваниях значение этой переменной в нем изменяется.
Обратим внимание, что поскольку у нас всего один тип - вещественный, то вообще невозможно сказать, какой наш язык: статически или динамически типизированный.
Аналогично реализуется выполнение оператора +=:
AssignPlusNode = class(StatementNode) public Ident: IdNode; Expr: ExprNode; constructor(Ident: IdNode; Expr: ExprNode; pos: Position) := (Self.Ident,Self.Expr,Self.pos) := (Ident,Expr,pos); procedure Execute; override; begin VarValues[Ident.Name] += Expr.Eval; end; end;
Если переменная встречается в выражении (в правой части присваивания, в условиях операторов if и while, то для вычисления ее значения надо просто найти ее значение в словаре VarValues:
IdNode = class(ExprNode) public Name: string; constructor(name: string; pos: Position) := (Self.Name,Self.pos) := (name,pos); function Eval: real; override; begin Result := VarValues[Name]; end; end;
Обратим внимание, что если мы до этого не инициализировали данную переменную присваиванием, то возникнет ошибка во время выполнения - всё как в интерпретаторе Python. Однако этого можно избежать, проверив, было ли присваивание данной переменной ранее, на этапе компиляции. Как это сделать и почему компиляция хороша, расскажем в следующих статьях.
Наконец, нам осталось реализовать выполнение в еще одном узле: ProcCallNode. Поскольку у нас нет пользовательских процедур, то все стандартные процедуры мы можем просто перечислить при выполнении. В нашем языке есть всего одна стандартная процедура print, которая печатает вещественное значение единственного аргумента:
ProcCallNode = class(StatementNode) public Name: IdNode; Pars: ExprListNode; constructor(Name: IdNode; Pars: ExprListNode; pos: Position) := (Self.Name,Self.Pars,Self.pos) := (Name,Pars,pos); procedure Execute; override; begin if Name.Name = 'print' then Println(Pars.lst[0].Eval) end; end;
Заметим, что мы не проверяем количество параметров у print. а также имя процедуры - процедура с другим именем просто не выполнится - никакой ошибки не произойдет. Процедуру же print мы не сможем вызвать без параметров, поскольку в грамматике у процедур предусмотрен хотя бы один параметр.
Приведем полный текст модуля ASTNodes с методами Eval и Execute в соответствующих узлах:
unit ASTNodes; uses Common; var VarValues := new Dictionary<string,real>; type Node = class pos: Position; end; ExprNode = class(Node) public function Eval: real; virtual := 0; end; StatementNode = class(Node) public procedure Execute; virtual := begin end; end; BinOpNode = class(ExprNode) public Left,Right: ExprNode; Op: char; constructor (Left,Right: ExprNode; Op: char; pos: Position) := (Self.Left,Self.Right,Self.Op,Self.pos) := (Left,Right,Op,pos); function Eval: real; override; begin var l := Left.Eval; var r := Right.Eval; case Op of '+' : Result := l + r; '*' : Result := l * r; '/' : Result := l / r; '<' : Result := l < r ? 1 : 0; end; end; end; StatementListNode = class(StatementNode) public lst: List<StatementNode> := new List<StatementNode>; procedure Add(st: StatementNode) := lst.Add(st); procedure Execute; override; begin foreach var st in lst do st.Execute; end; end; ExprListNode = class(Node) public lst: List<ExprNode> := new List<ExprNode>; procedure Add(ex: ExprNode) := lst.Add(ex); end; IntNode = class(ExprNode) public Value: integer; constructor(Value: integer; pos: Position) := (Self.Value,Self.pos) := (Value,pos); function Eval: real; override := Value; end; DoubleNode = class(ExprNode) public Value: real; constructor(Value: real; pos: Position) := (Self.Value,Self.pos) := (Value,pos); function Eval: real; override := Value; end; IdNode = class(ExprNode) public Name: string; constructor(name: string; pos: Position) := (Self.Name,Self.pos) := (name,pos); function Eval: real; override; begin Result := VarValues[Name]; end; end; AssignNode = class(StatementNode) public Ident: IdNode; Expr: ExprNode; constructor(Ident: IdNode; Expr: ExprNode; pos: Position) := (Self.Ident,Self.Expr,Self.pos) := (Ident,Expr,pos); procedure Execute; override; begin VarValues[Ident.Name] := Expr.Eval; end; end; AssignPlusNode = class(StatementNode) public Ident: IdNode; Expr: ExprNode; constructor(Ident: IdNode; Expr: ExprNode; pos: Position) := (Self.Ident,Self.Expr,Self.pos) := (Ident,Expr,pos); procedure Execute; override; begin VarValues[Ident.Name] += Expr.Eval; end; end; IfNode = class(StatementNode) public Condition: ExprNode; ThenStat,ElseStat: StatementNode; constructor(Condition: ExprNode; ThenStat,ElseStat: StatementNode; pos: Position) := (Self.Condition,Self.ThenStat,Self.ElseStat,Self.pos) := (Condition,ThenStat,ElseStat,pos); procedure Execute; override; begin if Condition.Eval > 0 then ThenStat.Execute else if ElseStat <> nil then ElseStat.Execute end; end; WhileNode = class(StatementNode) public Condition: ExprNode; Stat: StatementNode; constructor(Condition: ExprNode; Stat: StatementNode; pos: Position) := (Self.Condition,Self.Stat,Self.pos) := (Condition,Stat,pos); procedure Execute; override; begin while Condition.Eval > 0 do Stat.Execute; end; end; ProcCallNode = class(StatementNode) public Name: IdNode; Pars: ExprListNode; constructor(Name: IdNode; Pars: ExprListNode; pos: Position) := (Self.Name,Self.Pars,Self.pos) := (Name,Pars,pos); procedure Execute; override; begin if Name.Name.ToLower = 'print' then Println(Pars.lst[0].Eval) end; end; FuncCallNode = class(ExprNode) public Name: IdNode; Pars: ExprListNode; constructor(Name: IdNode; Pars: ExprListNode; pos: Position) := (Self.Name,Self.Pars,Self.pos) := (Name,Pars,pos); end; end.
Рассмотрим основную программу, которая запускает интерпретатор:
uses LexUnit,ParseUnit,Common; begin var text := 'i = 1; sum = 0; while i<100000000 do {sum += 1/i; i += 1}; print(sum)'; var lex := new Lexer(text); try var par := new Parser(lex); var progr := par.MainProgram; Println; MillisecondsDelta; progr.Execute; Println(#10,MillisecondsDelta/1000+' с'); except on e: LexerException do OutputError('Lexer error:',e,lex.Lines); on e: SyntaxException do OutputError('Parser error:',e,lex.Lines); end; end.
Здесь выполнение программы осуществляется строкой progr.Execute.
Время выполнения на компьютере с процессором Core I7 8700K с 16 Гб памяти составляет 15.3 сек. Количество строк нашей программы - около 660.
Составим аналогичную программу на языке Python:
import time n = 100000000 c = True start = time.time() i = 1 sum = 0 while i<n: sum += 1/i i += 1 print(sum) print(time.time() - start)
Время ее выполнения на компьютере с той же конфигурацией составляет 12.8 сек.
Вывод: мы написали интерпретатор на PascalABC.NET на 660 строк, по скоростью сравнимый со скоростью промышленного интерпретатора Python.