Обход AST в посетителе или в узлах? - PullRequest
9 голосов
/ 25 февраля 2011

Обновление принял ответ Ира Бакстера, поскольку он направил меня в правильном направлении: сначала я понял, что мне действительно нужно, начав реализацию этапа компиляции, и довольно скоро стало очевидно, что обход вузлы сделали невозможным подход.Не все узлы должны быть посещены, а некоторые из них в обратном порядке (например, сначала права доступа присваивания, чтобы компилятор мог проверить, совпадает ли тип с оператором rhs /).Установка обхода в посетителе делает все это очень простым.

Я поиграюсь с AST и тому подобным, прежде чем принять решение о крупной рефактории обработки мини-языка, используемого в приложении.Я построил Lexer / Parser и могу получить AST просто отлично.Также есть Visitor, и в качестве конкретной реализации я создал ASTToOriginal, который просто воссоздает исходный файл.В конце концов, есть какой-то компилятор, который также реализует Vsisitor и создает реальный код C ++ во время выполнения, поэтому я хочу убедиться, что все правильно с самого начала.Хотя теперь все работает нормально, есть некоторый подобный / дублирующий код, поскольку порядок обхода реализован в самом Visitor.

При поиске дополнительной информации кажется, что некоторые реализации предпочитают сохранять порядок обхода в посещаемых объектахсами по себе, чтобы не повторять это у каждого конкретного посетителя.Даже GoF только кратко говорит об этом, таким же образом.Поэтому я хотел попробовать и этот подход, но довольно скоро застрял .. Позвольте мне объяснить.

Пример строки исходного кода и соответствующих узлов AST:

if(t>100?x=1;sety(20,true):x=2)
Conditional
  BinaryOp
    left=Variable [name=t], operator=[>], right=Integer [value=100]
  IfTrue
    Assignment
      left=Variable [name=x], operator=[=], right=Integer [value=1] 
    Method
      MethodName [name=sety], Arguments( Integer [value=20], Boolean [value=true] )
  IfFalse
    Assignment
      left=Variable [name=x], operator=[=], right=Integer [value=1]

Некоторый код:

class BinaryOp {
  void Accept( Visitor* v ){ v->Visit( this ); }
  Expr* left;
  Op* op;
  Expr* right;
};    
class Variable {
  void Accept( Visitor* v ){ v->Visit( this ); }
  Name* name;
};
class Visitor { //provide basic traversal, terminal visitors are abstract
  void Visit( Variable* ) = 0;
  void Visit( BinaryOp* p ) {
    p->left->Accept( this );
    p->op->Accept( this );
    p->right->Accept( this );        
  }
  void Visit( Conditional* p ) {
    p->cond->Accept( this );
    VisitList( p->ifTrue ); //VisitList just iterates over the array, calling Accept on each element
    VisitList( p->ifFalse );
  }
};

Реализация ASTToOriginal довольно проста: все абстрактные методы Visitor просто выводят имя или значение члена терминала.Для нетерминалов это зависит;печать Назначения работает нормально с обходом посетителя по умолчанию, для Условного необходим дополнительный код:

class ASTToOriginal {
  void Visit( Conditional* p ) {
    str << "if(";
    p->cond->Accept( this );
    str << "?";
      //VisitListWithPostOp is like VisitList but calls op for each *except the last* iteration
    VisitListWithPostOp( p->ifTrue, AppendText( str, ";" ) );
    VisitListWithPostOp( p->ifFalse, AppendText( str, ";" ) );
    str << ")";
  }
};

Таким образом, можно видеть, что оба метода Visit для Условного в Visitor и ASTToOriginal действительно очень похожи.Однако попытка решить эту проблему путем введения обхода в узлы не только усугубила ситуацию, но и привела к полному беспорядку.Я попробовал подход с методами PreVisit и PostVisit, который решил некоторые проблемы, но просто вводил все больше и больше кода в узлы.Это также стало выглядеть так, как будто мне нужно было отслеживать несколько состояний внутри Посетителя, чтобы иметь возможность знать, когда добавлять закрывающие скобки и т. Д.

class BinaryOp {
  void Accept( Conditional* v ) { 
    v->Visit( this );
    op->Accept( v )
    VisitList( ifTrue, v );
    VisitList( ifFalse, v );
};
class Vistor {
  //now all methods are pure virtual
};
class ASTToOriginal {
  void Visit( Conditional* p ) {
    str << "if(";
    //now what??? after returning here, BinaryOp will visit the op automatically so I can't insert the "?"
    //If I make a PostVisit( BinaryOp* ), and call it it BinaryOp::Accept, I get the chance to insert the "?",
    //but now I have to keep a state: my PostVisit method needs to know it's currently being called as part of a Conditional
    //Things are even worse for the ifTrue/ifFalse statement arrays: each element needs a ";" appended, but not the last one,
    //how am I ever going to do that in a clean way?
  }
};

Вопрос : естьэтот подход просто не подходит для моего случая, или я пропускаю что-то важное?Есть ли общий дизайн, чтобы справиться с этими проблемами?Что если мне также понадобится обход в другом направлении?

Ответы [ 3 ]

6 голосов
/ 25 февраля 2011

Есть две проблемы:

  • Какие дочерние узлы можно посетить?
  • В каком порядке их посещать?

Возможно, фактическоедочерние узлы должны быть известны по типу узла;на самом деле это должно быть известно по грамматике и «отражено» от грамматики в общем посетителе.

Порядок, в котором посещаются узлы, полностью зависит от того, что вам нужно сделать.Если вы выполняете симпатичную печать, имеет смысл дочерний порядок слева направо (если дочерние узлы перечислены в порядке грамматики, чего не может быть).Если вы создаете таблицы символов, вы, безусловно, хотите посетить дочерние элементы декларации, прежде чем посетить дочернюю структуру тела оператора.

Кроме того, вам нужно беспокоиться о том, какая информация перемещается вверх или вниз по дереву.Доступ к списку переменных будет проходить вверх по дереву.Построенная таблица символов перемещается вверх по дереву из объявлений и возвращается обратно в тело оператора child.И этот информационный поток вызывает порядок посещения;чтобы иметь таблицу символов для передачи в тело оператора, сначала необходимо создать таблицу символов и передать ее из дочерних объявлений.

Я думаю, что эти проблемы дают вам серьезное представление.Вы пытаетесь навязать своим посетителям единую структуру, когда на самом деле порядок посещения полностью зависит от задачи, и существует множество различных задач, которые вы можете выполнить с деревом, каждое из которых имеет свой собственный информационный поток и, следовательно, зависимость порядка.

Одним из способов решения этой проблемы является понятие атрибута (d) грамматики (AG) , в котором правила грамматики украшаются различными типами атрибутов и тем, как они вычисляются /используемый.Вы буквально записываете вычисление в качестве аннотации к правилу грамматики, например:

  method = declarations statements ;
  <<ResolveSymbols>>: { declarations.parentsymbols=method.symboltable;
                        statements.symboltable = declarations.symboltable;
                      }

Правило грамматики сообщает, какие типы узлов вам нужны.Вычисление атрибута говорит вам, какое значение передается по дереву (ссылка на method.symboltable относится к чему-то, приходящему из родительского объекта), вверх по дереву (ссылка на таблицу декларации.symbol относится к чему-то, вычисленному этим потомком), или черездерево (Statement.symboltable передается дочерним операторам, от значения, вычисляемого объявлением. Symboltable).Атрибут вычисления определяет посетителя.Выполненные вычисления называются «оценкой атрибутов».

Эта запись для этой конкретной грамматики атрибутов является частью нашего инструментария реинжиниринга программного обеспечения DMS .Другие инструменты AG используют аналогичные обозначения.Как и во всех схемах (AG), конкретное правило используется для создания целевого («ResolveSymbols») посетителя для конкретного узла («метод»).С набором таких спецификаций для каждого узла вы получаете набор посетителей с определенной целью, которые могут быть выполнены.Ценность схемы AG заключается в том, что вы можете написать это легко, и будет создан весь шаблонный мусор.

Вы можете подумать о своей проблеме в абстрактном виде, как это, а затем просто сгенерировать целевых посетителей вручную., как вы делаете.

1 голос
/ 25 февраля 2011

Для рекурсивного общего обхода деревьев Visitor и Composite обычно используются вместе, как в (первая соответствующая ссылка Google) там .Я впервые прочитал об этой идее там .Есть также комбинаторы посетителей , которые являются хорошей идеей.

И, кстати ...

это то, где функциональные языки сияют, с их алгебраическими типами данных и образцомсоответствия.Если можете, переключитесь на функциональный язык.Composite и Visitor - только уродливые обходные пути из-за отсутствия языковой поддержки соответственно ADT и сопоставления с образцом.

0 голосов
/ 25 февраля 2011

IMO, мне бы каждый конкретный класс (например, BinaryOp, Variable) расширял класс Visitor.Таким образом, вся логика, необходимая для создания объекта BinaryOp, будет находиться в классе BinaryOp.Этот подход аналогичен шаблону Walkabout .Это может облегчить вашу задачу.

...