在C ++中表示多次传递抽象语法树(AST)?

时间:2013-04-16 17:05:04

标签: c++ compiler-construction abstract-syntax-tree parse-tree

我目前正在探索设计一个可以在多个阶段转换AST的编译器。我们的想法是从解析树开始,每次传递都会转换树,直到生成的AST得到优化,并包含生成中间代码所需的树的每个节点中的所有必需信息(在本例中为LLVM IR )。树上的传递可能会显着改变其结构,例如通过operator precedence parsing将操作符和操作数列表更改为有序操作的层次结构。请注意,传递可能会使结构的某些部分完全不变。

所以,我的问题是我如何最好(阅读:最容易,尽可能少的重复)代表一个在C ++中有多个中间表示的AST?我希望每个阶段的AST版本中的节点类型在编译时遵守它们的不兼容性。我认为关键问题是如何在避免重复代码的同时代表结构中不改变通道的部分?我想这是编译器作者过去多次解决的一个问题。

请注意,我目前在AST中使用Boost Variant而不是普通的运行时多态,并希望解决方案与其兼容。

2 个答案:

答案 0 :(得分:4)

AST节点本身并不需要大量的复杂性。我认为所有这些AST节点机器都是矫枉过正。

AST的问题不是节点类型安全;它的树形安全。 AST代表(可能)某些语言L的某些有效实例。您理想的是要转换AST以生成其他有效的AST(语言L的实例)。您不能保证通过保证任何一个节点具有有效类型;您只能通过保证任何树补丁生成有效的来实现。如果树操作是原子的(例如,“改变节点”,“替换子”,“替换父”)并且单独应用,则这很难做到;在经过几个这样的步骤之后,你究竟能对这棵树说些什么呢?

使用一种树重写事务更好地完成,例如源语法转换,其语法结构对语言L有效,并且应用于对该转换有效的地方。

大多数标准program transformation systems执行此操作。他们通过为L保持语法模型来实现这一点,并检查所提出的变换是否是良好类型的。这确保了语言L到语言L的转换保持良好的形式。

如果转换从一种语言A映射到另一种语言B,则很难做到这一点;如果应用了某些此类转换,您通常会得到一个混合类型的树,这种树在任何一种语言中都是不合法的。小心翼翼地,可以定义一组转换,将语言A的所有子树映射到语言B,并将其彻底应用;那么你希望得到的树能够很好地形成B.你可以通过坚持在混合树中插入B-patch,如果它与另一个B-patch相邻,那么得到的化合物B-patch很好形成。您可以使用相同的语法检查方式。

使用这些想法,您可以构建一个系统,通过一系列“表示”(langauges A,B,C,....)来映射AST,并且相信结果树形状良好。这个想法概括为图形重写。

答案 1 :(得分:3)

这是对基于类型安全boost::variant的AST的快速尝试。

我包含了一个简单的“结构保留变换”,它只是改变了每个AST节点中存储的数据类型。但是,理论上,您可以编写一个任意astFunc,它们都可以对节点进行基于结构和数据的转换 - 只需编写一个type_list,其中包含每个节点之前和之后的有效类型。 / p>

template<typename... Ts>
struct type_list {};

// specialize data_type to store something special in your AST node:
// (by default, an entry means "the type of the data")
tempalte<typename T>
struct data_type { typedef T type; };
template<typename T>
using DataType = typename data_type<T>::type;

template<template<typename>class F, typename typelist>
struct map_types;
template<template<typename>class F, template<typename...>L, typename... Ts>
struct map_types<F, L<Ts...>> {
  typedef L< F<Ts>... > type;
};
template<template<typename>class F, typename typelist>
using MapTypes = typename map_types<F, typelist>::type;

template<template<typename...>class F, typename typelist>
struct apply_list;
template<template<typename...>class F, template<typename...>class L, typename... Ts>
struct apply_list<F, L<Ts...>> {
  typedef F<Ts...> type;
};
template<template<typename...>class F, typename typelist>
using ApplyList = typename apply_list<F, typelist>::type;
template<typename typelist>
using Var = ApplyList< boost::variant, MapTypes<DataType, typelist> >;

template<typename type_list>
struct AST_Node {
  typedef std::unique_ptr<AST_Node> upAST_Node;
  std::vector<upAST_Node> children;
  Var<type_list> data;
  template<typename T>
  AST_Node( T&& t ):data( std::forward<T>(t) ) {}
};
template<typename type_list>
using upAST_Node = typename AST_Node<type_list>::upAST_Node;

template<typename before_types, typename after_types>
using typeFunc = std::function< Var<after_types>(Var<before_types>) >;

template<typename before_types, typename after_types>
using astFunc = std::function< upAST_Node<after_types>(upAST_Node<before_types>) >;

template<typename before_types, typename after_types>
astFunc<before_types, after_types> elementWiseTransform( typeFunc<before_types, after_types> func ) {
  return [func]( upAST_Node<before_types> before )->upAST_Nodes<after_types> {
    upAST_Node<after_types> after( new AST_Node<after_types>( func( before ) ) );
    after->children.reserve( before->children.size() );
    for( auto& child: before->children ) {
      after->children.push_back( elementWiseTransform(func)(std::move(child)) );
    }
    return after;
  };
}

现在这只是一个开始。

您可以更进一步,让每种类型的节点都有不同类型的子节点,甚至是不同的数字。只需为data_type等每种类型的节点创建特征类,例如children_types。然后使用类似的技术来定义Var来定义孩子的类型。基本上,您通过variant的链接获得std::vector< AST_Node<ChildType<type_list_element>>> MapTypes。你可以将std::vector个孩子和data捆绑在一起变成一个变体。

这样您就可以为单个AST_Node类型(另一个AST_Node类型)编写映射,将它们聚合在一起并生成一个AST_Node<before, after>仿函数,然后再行走在树上。一些仿函数只对数据进行操作,然后让父逻辑接管子节点,有些会转换整个子树,有些会操作数据并阻止父逻辑运行子节点。

这种技术变得棘手,因为你必须以不需要将它们全部堆叠在一起的方式从你的各个函数中合成boost变体访问者。如果您查看here,您会看到一些关于如何获取一堆std::function<T(U)>并将它们转换为一个带有U联合中任何一个的仿函数的技巧。抛出一些工作来计算返回类型的并集(一个简单的type_list删除了重复的类型,然后抽入boost::variant,可能会这样做 - 这样一个“合并的函子”将是一个有效的访客。

现在你可以写“重新映射一个类型为operator_add的AST节点”仿函数,并“重新映射一个类型为operator_mult的AST节点”,还有一些其他的,将它们组合成一个大型仿函数,将它们抛到AST遍历算法,让它喷出一个AST树,其中一些类型转换为其他类型......

但那将是很多工作。

哦,我们可能想要“阶段标记”,其中阶段1和阶段2 AST是不同类型。我们可以使用其阶段标记type_list中的每种类型,或者我们只需标记AST树本身。哎呀,我们可以使用其他未使用的AST来命名struct的阶段,并定义阶段的进展作为类型,以便在astFunc<before_phase, before_types, after_phase, after_types>的签名中应用和强制执行类型的仿函数

所以这还不错。我们创建了type_list个节点类型。这些类型不必是存储的实际数据。但它可以。

我们创建一个data_type traits类,将每个节点类型映射到存储的数据。我们创建了一个child_types traits类,它将每个节点类型映射到子AST的type_list。

每个AST_Node存储一个variant<AST_Concrete_Node<Ts>...>AST_Concrete_Node包含DataType<T> data;MapTypes< std::vector, MapTypes< AST_Node, ChildTypes<T> > > children;(又名std::vector< AST_Node<ChildrenTypes...> >,但你不能直接说出来。)

接下来,AST_Concrete_Node<T>转换函数通过模板元编程的一小部分连接在一起,形成boost变体访问者。这一步非常棘手,但我认为可行。进行了额外的工作,以便跳过未提及的类型,因此我们不必经常说“哦,我们不想转换节点X”,而是必须说“如果我们命中节点Y,不要改变它的孩子“。

在这一点上,我会说我在喋喋不休 - 以前没有这样做过,在这种混乱的体操类型的具体实施中遇到的问题将压倒我抽象地推理它的能力。但是这个想法很有用 - 我们有类型安全的节点类型转换,我们将它们聚合在一起并生成类型安全的树转换。树不仅仅是一个通用变体的抽象树,而是一个树,每个节点都知道它的子节点允许的类型,它们递归地知道它们。我们甚至可以处理“这必须有3个孩子,第一个是int,第二个是Bob,第三个是double”如果我们走得够远在兔子洞里。