【问题标题】:Constructing an Abstract Syntax Tree with a list of Tokens构建带有标记列表的抽象语法树
【发布时间】:2014-09-22 20:35:38
【问题描述】:

我想从一个令牌列表构造一个 AST。我正在制作一种脚本语言,并且已经完成了词法分析部分,但我不知道如何创建 AST。所以问题是,我该如何看待这样的事情:

WORD, int
WORD, x
SYMBOL, =
NUMBER, 5
SYMBOL, ;

并将其转换为抽象语法树?最好,我希望没有像 ANTLR 之类的库或其他库,我宁愿自己尝试从头开始。但是,如果这是一项非常复杂的任务,我不介意使用库:) 谢谢

【问题讨论】:

  • 也许你可以从我的类似问题中找到一些资源:stackoverflow.com/q/24661870/2498956
  • 您要查找的词是“解析器”。有很多方法可以构建它们。 JavaCC 浮现在脑海中。如果你坚持不使用外部软件,你或多或少会被谴责使用递归下降。
  • 纯解析器不会这样做。您需要一个增加了树节点构建操作的解析器。看我的回答。

标签: java interpreter abstract-syntax-tree


【解决方案1】:

基本技巧是要认识到解析,无论多么完成,都是在增量步骤中发生的,包括一个一个地读取标记。

在每个增量步骤中,都有机会通过组合其他增量步骤构建的 AST 片段来构建 AST 的一部分。这是一个递归的想法,它会在扫描令牌时为令牌构建 AST 叶节点。这个基本思想出现在几乎所有的 AST 构建解析器中。

如果一个人构建了一个递归下降解析器,那么一个人实际上构建了一个递归过程的协作系统,每个递归过程都可以识别正在实施的任何语法中的非终结符。对于纯解析,每个过程只返回一个布尔值,表示“非终结(未)识别”。

要使用递归下降解析器构建 AST,需要将这些过程设计为返回两个值:布尔值“已识别”,以及(如果已识别)为非终结符构造(以某种方式)的 AST。 (一个常见的技巧是返回一个指针,它对于“未识别”是无效的,或者如果“识别”则指向构造的 AST)。构建单个过程的结果 AST 的方式是组合来自它调用的子过程的 AST。这对于读取输入标记并可以立即构建树的叶过程来说非常简单。

所有这一切的缺点是必须手动编码递归下降,并通过树构建步骤对其进行扩充。总体而言,这实际上很容易为小语法编写代码。

对于 OP 的例子,假设我们有这样的语法:

GOAL = ASSIGNMENT 
ASSIGNMENT = LHS '=' RHS ';' 
LHS = IDENTIFIER 
RHS = IDENTIFIER | NUMBER

好的,我们的递归下降解析器:

boolean parse_Goal()
{  if parse_Assignement()
   then return true
   else return false
}

boolean parse_Assignment()
{  if not Parse_LHS()
   then return false
   if not Parse_equalsign()
   then throw SyntaxError // because there are no viable alternatives from here
   if not Parse_RHS()
   then throw SyntaxError
   if not Parse_semicolon()
   the throw SyntaxError
   return true
}

boolean parse_LHS()
{  if parse_IDENTIFIER()
   then return true
   else return false
}

boolean parse_RHS()
{  if parse_IDENTIFIER()
   then return true
   if parse_NUMBER()
   then return true
   else return false
}

boolean parse_equalsign()
{  if TestInputAndAdvance("=")  // this can check for token instead
   then return true
   else return false
}

boolean parse_semicolon()
{  if TestInputAndAdvance(";")
   then return true
   else return false
}

boolean parse_IDENTIFIER()
{  if TestInputForIdentifier()
   then return true
   else return false
}

boolean parse_NUMBER()
{  if TestInputForNumber()
   then return true
   else return false
}

现在,让我们修改它构建一个抽象语法树:

AST* parse_Goal() // note: we choose to return a null pointer for "false"
{  node = parse_Assignment()
   if node != NULL
   then return node
   else return NULL
}

AST* parse_Assignment()
{  LHSnode = Parse_LHS()
   if LHSnode == NULL
   then return NULL
   EqualNode = Parse_equalsign()
   if EqualNode == NULL
   then throw SyntaxError // because there are no viable alternatives from here
   RHSnode = Parse_RHS()
   if RHSnode == NULL
   then throw SyntaxError
   SemicolonNode = Parse_semicolon()
   if SemicolonNode == NULL
   the throw SyntaxError
   return makeASTNode(ASSIGNMENT,LHSNode,RHSNode)
}

AST* parse_LHS()
{  IdentifierNode = parse_IDENTIFIER()
   if node != NULL
   then return IdentifierNode
   else return NULL
}

AST* parse_RHS()
{  RHSnode = parse_IDENTIFIER()
   if RHSnode != null
   then return RHSnode
   RHSnode = parse_NUMBER()
   if RHSnode != null
   then return RHSnode
   else return NULL
}

AST* parse_equalsign()
{  if TestInputAndAdvance("=")  // this can check for token instead
   then return makeASTNode("=")
   else return NULL
}

AST* parse_semicolon()
{  if TestInputAndAdvance(";")
   then return makeASTNode(";")
   else return NULL
}

AST* parse_IDENTIFIER()
{  text = TestInputForIdentifier()
   if text != NULL
   then return makeASTNode("IDENTIFIER",text)
   else return NULL
}

AST* parse_NUMBER()
{  text = TestInputForNumber()
   if text != NULL
   then return makeASTNode("NUMBER",text)
   else return NULL
}

我显然掩盖了一些细节,但我认为读者填写它们不会有问题。

JavaCC 和 ANTLR 等解析器生成器工具基本上生成递归下降解析器,并且具有构造与此类似的树的工具。

构建自下而上解析器(YACC、Bison、GLR 等)的解析器生成器工具也以相同的方式构建 AST 节点。但是,没有一组递归函数;取而代之的是,这些工具管理着一堆看到并简化为非终结符的令牌。 AST 节点构建在并行堆栈上;当减少发生时,被减少覆盖的堆栈部分上的 AST 节点被组合以产生一个非终端 AST 节点来替换它们。这发生在语法规则的“零大小”堆栈段也为空时,导致 AST 节点(通常用于“空列表”或“缺少选项”)似乎不知从何出现。

使用小语言,编写构建树的递归下降解析器非常实用。

真正的语言(无论是像 COBOL 那样陈旧而陈旧,还是像 Scala 那样炙手可热)的一个问题是语法规则的数量非常多,而且由于语言的复杂性以及对任何语言委员会负责的坚持而变得复杂它不断添加其他语言提供的新好东西(“语言嫉妒”,参见 Java、C# 和 C++ 之间的进化竞赛)。现在编写递归下降解析器已经失控,人们倾向于使用解析器生成器。但即使使用解析器生成器,编写所有自定义代码来构建 AST 节点也是一场大战(我们还没有讨论过如何设计一个好的“抽象”语法与首先想到的东西)。随着规模的扩大和不断的发展,维护语法规则和构建 AST 变得越来越困难。 (如果你的语言是成功的,一年之内你会想要改变它)。因此,即使编写 AST 构建规则也会变得很尴尬。

理想情况下,人们只想写一个语法,并获得一个解析器和树。你can do this with some recent parser generators: Our DMS Software Reengineering Toolkit accepts full context free grammars, and automatically constructs an AST,语法工程师没有工作;自 1995 年以来一直在这样做。ANTLR 家伙终于在 2014 年发现了这一点,而 ANTLR4 现在提供了这样的选项。

最后一点:拥有解析器(即使使用 AST)几乎不能解决您打算解决的实际问题,无论它是什么。它只是一个基础部分,令大多数解析器新手感到震惊,它是操作代码的工具的最小部分。谷歌我关于解析后的生活的文章(或查看我的简历)以获取更多详细信息。

【讨论】:

    【解决方案2】:

    一点都不难;事实上,这是我做过的最简单的事情之一。 一般的想法是每个结构(也称为解析器规则)只是其他结构的列表,当调用 parse() 函数时,它们只是循环遍历它们的子结构,并告诉它们进行解析。这不是一个无限循环;标记是结构,当调用它们的 parse() 时,它们会扫描词法分析器的输出。他们还应该有一个用于识别的名称,但这不是必需的。 parse() 通常会返回一个解析树。解析树就像结构 - 子列表。最好有一个“文本”字段及其父结构,用于识别。 这是一个示例(您希望更好地组织它并为实际项目处理 null):

    public void push(ParseTree tree) { // ParseTree
        children.add(tree);
        text += tree.text;
    }
    
    public ParseTree parse() { // Structure
        ParseTree tree = new ParseTree(this);
        for(Structure st: children) {
            tree.push(st.parse());
        }
        return tree;
    }
    
    public ParseTree parse() { // Token
        if(!lexer.nextToken() || !matches(lexer.token))
            return null;
        ParseTree tree = new ParseTree(this);
        tree.text = lexer.token;
        return tree;
    }
    

    那里。调用主结构的 parse(),你得到了一个 AST。当然,这是一个非常简单的示例,不能开箱即用。 拥有“修饰符”也很有用;例如匹配孩子 3 一次或多次,孩子 2 是可选的。这也很容易做到;将它们存储在与您的孩子数量相同大小的数组中,并在解析时检查它:

    public void setModifier(int id, int mod) {
        mods[id] = mod;
    }
    
    public ParseTree parse() {
        ...
        ParseTree t;
        switch(mods[i]) {
            case 1: // Optional
                if((t = st.parse()) != null) tree.push(t);
            case 2: // Zero or more times
                while((t = st.parse()) != null) tree.push(t);
            ...
            default:
                tree.push(st.parse());
        }
        ...
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-03
      • 2014-02-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多