Пишем интерпретатор на PascalABC.NET. Визиторы по синтаксическому дереву
Этот текст - продолжение публикации https://teletype.in/@pascalabcnet/InterpretASTTree.
В данной статье мы рассмотрим, как обойти полученное синтаксическое дерево AST и какие задачи с помощью этого обхода можно решать.
Итак, мы построили синтаксическое дерево. Его корнем является узел всей программы, остальные вершины являются операторами, выражениями и листовые вершины являются терминалами: целыми и вещественными константами, идентификаторами.
По синтаксическому дереву можно собрать большое количество полезной информации о программе: например, сколько в программе присваиваний, циклов, какие используются переменные и проч.
По синтаксическому дереву можно восстановить текст программы - причем, можно это сделать на другом языке: например, программа будет на языке C, а восстановим мы ее на языке Паскаль.
По синтаксическому дереву можно сделать ряд семантических проверок: верно ли описаны переменные, правильно ли типизированы выражения и проч. Семантическими проверками мы займемся в одной из следующих статей.
А пока нам надо понять, как решать разные задачи обхода синтаксического дерева, где накапливать полученную информацию.
Для решения этой важнейшей задачи нам поможет так называемый паттерн проектирования Визитор (Посетитель).
Прежде всего, давайте разберемся, что такое паттерны проектирования. Паттерн проектирования - это повторяющаяся в разных проектах архитектурная конструкция кода, состоящая из нескольких взаимодействующих классов и решающая определенную задачу. Паттерны проектирования впервые были описаны в книге Э.Гаммы и др. "Приёмы объектно-ориентированного проектирования. Паттерны проектирования".
Паттерн Визитор решает следующую проблему. Узлы синтаксического дерева имеют разные типы, и в каждом узле в зависимости от его типа необходимо выполнить определенное действие. Например, в каждом узле можно вернуть строку - правильно отформатированный код части программы, представляемый этим узлом. Но таких задач может быть много. Встраивать подобные действия в узлы дерева неэффективно - для ькаждой задачи в узел потребуется добавить один метод, и количество таких методов будет разрастаться. Паттерн Визитор предлагает выделить все действия по решению определенной задачи в отдельный класс, называемый визитором. Тем самым код узлов синтаксического дерева не будет захламляться.
Рассмотрим интерфейс визитора по нашему синтаксическому дереву:
type IVisitor<T> = interface function VisitNode(bin: Node): T; function VisitExprNode(bin: ExprNode): T; function VisitStatementNode(bin: StatementNode): T; function VisitBinOp(bin: BinOpNode): T; function VisitStatementList(stl: StatementListNode): T; function VisitExprList(exlist: ExprListNode): T; function VisitInt(n: IntNode): T; function VisitDouble(d: DoubleNode): T; function VisitId(id: IdNode): T; function VisitAssign(ass: AssignNode): T; function VisitAssignPlus(ass: AssignPlusNode): T; function VisitIf(ifn: IfNode): T; function VisitWhile(whn: WhileNode): T; function VisitProcCall(p: ProcCallNode): T; function VisitFuncCall(f: FuncCallNode): T; end;
Он состоит из заголовков функций - каждая отвечает за свой тип узла и возвращает некоторое значение типа T. В классах, реализующих этот интерфейс, мы будем возвращать конкретные типы T. Например, в визиторе PrettyPrinter T=string и мы будем в каждой функции строковое представление части кода, связанной с этим узлом.
Обратим внимание, что мы можем создать какое угодно количество конкретных визиторов - классов, реализующих этот интерфейс для решения разных задач - код узлов дерева при этом не изменится!
Для того чтобы паттерн Визитор автоматически вызывал эти методы для каждого типа узла, мы должны встроить в каждый узел синтаксического дерева специальный метод Visit:
type Node = class pos: Position; public function Visit<T>(v: IVisitor<T>): T; virtual := v.VisitNode(Self); end; ExprNode = class(Node) public function Visit<T>(v: IVisitor<T>): T; override := v.VisitExprNode(Self); end; StatementNode = class(Node) public function Visit<T>(v: IVisitor<T>): T; override := v.VisitStatementNode(Self); end; BinOpNode = class(ExprNode) public Left,Right: ExprNode; Op: OpType; constructor (Left,Right: ExprNode; Op: OpType; pos: Position) := (Self.Left,Self.Right,Self.Op,Self.pos) := (Left,Right,Op,pos); function Visit<T>(v: IVisitor<T>): T; override := v.VisitBinOp(Self); end; StatementListNode = class(StatementNode) public lst: List<StatementNode> := new List<StatementNode>; procedure Add(st: StatementNode) := lst.Add(st); function Visit<T>(v: IVisitor<T>): T; override := v.VisitStatementList(Self); end; ExprListNode = class(Node) public lst: List<ExprNode> := new List<ExprNode>; procedure Add(ex: ExprNode) := lst.Add(ex); function Visit<T>(v: IVisitor<T>): T; override := v.VisitExprList(Self); end; IntNode = class(ExprNode) public Value: integer; constructor(Value: integer; pos: Position) := (Self.Value,Self.pos) := (Value,pos); function Visit<T>(v: IVisitor<T>): T; override := v.VisitInt(Self); end; DoubleNode = class(ExprNode) public Value: real; constructor(Value: real; pos: Position) := (Self.Value,Self.pos) := (Value,pos); function Visit<T>(v: IVisitor<T>): T; override := v.VisitDouble(Self); end; IdNode = class(ExprNode) public Name: string; constructor(name: string; pos: Position) := (Self.Name,Self.pos) := (name,pos); function Visit<T>(v: IVisitor<T>): T; override := v.VisitId(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitAssign(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitAssignPlus(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitIf(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitWhile(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitProcCall(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitFuncCall(Self); end;
В каждом методе Visit мы передаем единственным параметром интерфейс описанного нами визитора и в теле метода вызываем соответствующий данному синтаксическому узлу метод визитора. Например, в теле метода Visit класса BinOpNode мы вызываем метод v.VisitBinOp(Self), передавая ему в качестве параметра Self - ссылку на текущий узел синтаксического дерева.
Что мы таким образом приобретаем?
Предположим, мы написали определенный визитор MyVisitor, реализующий интерфейс IVisitor<T> и создали объект этого визитора в переменной v. Тогда вызов метода Visit например для узла BinOpNode вызовет, как мы уже отметили, метод v.VisitBinOp(Self) для нашего класса MyVisitor, тем самым, выполнится конкретное действие, которое мы предусмотрели для узла BinOpNode в классе MyVisitor.
В итоге каждый визитор реализует свой набор действий для разных типов узлов синтаксического дерева, решая какую-то конкретную задачу. А таких задач будет много.
Приведем полный код модуля ASTNodes с интерфейсом визитора:
unit ASTNodes; uses Common; type OpType = (opPlus, opMinus, opMultiply, opDivide, opEqual, opLess, opLessEqual, opGreater, opGreaterEqual, opNotEqual, opAnd, opOr, opNot, opBad); var OpToStr := Dict( (opPlus,#39;+'),(opMinus,#39;-'),(opMultiply,#39;*'),(opDivide,#39;/'), (opEqual,#39;=='),(opLess,#39;<'),(opLessEqual,#39;<='),(opGreater,#39;>'),(opGreaterEqual,#39;>='),(opNotEqual,#39;!='), (opAnd,#39;&&'),(opOr,#39;||'),(opNot,#39;!') ); type Node = class; ExprNode = class; StatementNode = class; BinOpNode = class; StatementListNode = class; ExprListNode = class; IntNode = class; DoubleNode = class; IdNode = class; AssignNode = class; AssignPlusNode = class; IfNode = class; WhileNode = class; ProcCallNode = class; FuncCallNode = class; IVisitor<T> = interface function VisitNode(bin: Node): T; function VisitExprNode(bin: ExprNode): T; function VisitStatementNode(bin: StatementNode): T; function VisitBinOp(bin: BinOpNode): T; function VisitStatementList(stl: StatementListNode): T; function VisitExprList(exlist: ExprListNode): T; function VisitInt(n: IntNode): T; function VisitDouble(d: DoubleNode): T; function VisitId(id: IdNode): T; function VisitAssign(ass: AssignNode): T; function VisitAssignPlus(ass: AssignPlusNode): T; function VisitIf(ifn: IfNode): T; function VisitWhile(whn: WhileNode): T; function VisitProcCall(p: ProcCallNode): T; function VisitFuncCall(f: FuncCallNode): T; end; IVisitorP = interface procedure VisitNode(bin: Node); procedure VisitExprNode(bin: ExprNode); procedure VisitStatementNode(bin: StatementNode); procedure VisitBinOp(bin: BinOpNode); procedure VisitStatementList(stl: StatementListNode); procedure VisitExprList(exlist: ExprListNode); procedure VisitInt(n: IntNode); procedure VisitDouble(d: DoubleNode); procedure VisitId(id: IdNode); procedure VisitAssign(ass: AssignNode); procedure VisitAssignPlus(ass: AssignPlusNode); procedure VisitIf(ifn: IfNode); procedure VisitWhile(whn: WhileNode); procedure VisitProcCall(p: ProcCallNode); procedure VisitFuncCall(f: FuncCallNode); end; Node = class pos: Position; public function Visit<T>(v: IVisitor<T>): T; virtual := v.VisitNode(Self); end; ExprNode = class(Node) public function Visit<T>(v: IVisitor<T>): T; override := v.VisitExprNode(Self); end; StatementNode = class(Node) public function Visit<T>(v: IVisitor<T>): T; override := v.VisitStatementNode(Self); end; BinOpNode = class(ExprNode) public Left,Right: ExprNode; Op: OpType; constructor (Left,Right: ExprNode; Op: OpType; pos: Position) := (Self.Left,Self.Right,Self.Op,Self.pos) := (Left,Right,Op,pos); function Visit<T>(v: IVisitor<T>): T; override := v.VisitBinOp(Self); end; StatementListNode = class(StatementNode) public lst: List<StatementNode> := new List<StatementNode>; procedure Add(st: StatementNode) := lst.Add(st); function Visit<T>(v: IVisitor<T>): T; override := v.VisitStatementList(Self); end; ExprListNode = class(Node) public lst: List<ExprNode> := new List<ExprNode>; procedure Add(ex: ExprNode) := lst.Add(ex); function Visit<T>(v: IVisitor<T>): T; override := v.VisitExprList(Self); end; IntNode = class(ExprNode) public Value: integer; constructor(Value: integer; pos: Position) := (Self.Value,Self.pos) := (Value,pos); function Visit<T>(v: IVisitor<T>): T; override := v.VisitInt(Self); end; DoubleNode = class(ExprNode) public Value: real; constructor(Value: real; pos: Position) := (Self.Value,Self.pos) := (Value,pos); function Visit<T>(v: IVisitor<T>): T; override := v.VisitDouble(Self); end; IdNode = class(ExprNode) public Name: string; constructor(name: string; pos: Position) := (Self.Name,Self.pos) := (name,pos); function Visit<T>(v: IVisitor<T>): T; override := v.VisitId(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitAssign(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitAssignPlus(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitIf(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitWhile(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitProcCall(Self); 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); function Visit<T>(v: IVisitor<T>): T; override := v.VisitFuncCall(Self); end; end.
Напишем простой прикладной визитор простой печати текста программы по синтаксическому дереву. Унаследуем его от IVisitor<string>. Это значит, что каждый его метод будет возвращать строку - часть текста программы, соответствующая данному узлу. Поместим этот визитор в модуль SimplePrint:
unit SimplePrint; uses ASTNodes; type SimplePrintVisitor = class(IVisitor<string>) public function VisitNode(n: Node): string := n.Visit(Self); function VisitExprNode(ex: ExprNode): string := ex.Visit(Self); function VisitStatementNode(st: StatementNode): string := st.Visit(Self); function VisitBinOp(bin: BinOpNode): string := bin.Left.Visit(Self) + OpToStr[bin.Op] + bin.Right.Visit(Self); function VisitStatementList(stl: StatementListNode): string := stl.lst.Select(x -> x.Visit(Self)).JoinToString(NewLine); function VisitExprList(exlist: ExprListNode): string := exlist.lst.Select(x -> x.Visit(Self)).JoinToString(','); function VisitInt(n: IntNode): string := n.Value.ToString; function VisitDouble(d: DoubleNode): string := d.Value.ToString; function VisitId(id: IdNode): string := id.Name; function VisitAssign(ass: AssignNode): string := ass.Ident.Name + ' := ' + ass.Expr.Visit(Self); function VisitAssignPlus(ass: AssignPlusNode): string := ass.Ident.Name + ' += ' + ass.Expr.Visit(Self); function VisitIf(ifn: IfNode): string := 'if ' + ifn.Condition.Visit(Self) + ' then '+ ifn.ThenStat.Visit(Self) + (ifn.ElseStat = nil ? '' : ifn.ElseStat.Visit(Self)); function VisitWhile(whn: WhileNode): string := 'while '+ whn.Condition.Visit(Self) + ' do '+ NewLine + whn.Stat.Visit(Self); function VisitProcCall(p: ProcCallNode): string := p.Name.Name + '(' + p.Pars.Visit(Self) + ')'; function VisitFuncCall(f: FuncCallNode): string := f.Name.Name + '(' + f.Pars.Visit(Self) + ')'; end; end.
Воспользуемся им в основной программе:
uses LexUnit,ParseUnit,SemanticCheck,Common,SimplePrint; begin var text := 'i = 1; sum = 0.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; var semcheck := new SemanticCheckVisitor; progr.VisitP(semcheck); progr.Visit(new SimplePrintVisitor).Println; except on e: LexerException do OutputError('Лексическая ошибка:',e,lex.Lines); on e: SyntaxException do OutputError('Синтаксическая ошибка:',e,lex.Lines); on e: SemanticException do OutputError('Семантическая ошибка:',e,lex.Lines); end; end.
i := 1 sum := 0 while i<100000000 do sum += 1/i i += 1 print(sum)
Заключение. Мы рассмотрели важный для создания интерпретаторов и достаточно сложный паттерн проектирования Визитор, применили его к нашему синтаксическому дереву и реализовали конкретный визитор для упрощенного вывода текста программы, занимающий около 20 строк.