为编程语言编写解析器:输出

时间:2014-04-27 21:48:34

标签: c++ parsing output lexer

我正在尝试用C ++编写一个简单的解释型编程语言。我读过很多人使用像Lex / Flex Bison这样的工具来避免“重新发明轮子”,但由于我的目标是了解这些小动物是如何改善我的知识的,所以我决定写出Lexer和从头开始解析。目前我正在研究解析器(词法分析器已完成),我问自己它的输出应该是什么。一颗树?带有“深度”或“移位”参数的语句的线性向量?我该如何管理循环和if语句?我应该用隐形goto语句替换它们吗?

6 个答案:

答案 0 :(得分:7)

解析器几乎总是输出AST。从最广泛的意义上讲,AST简单地表示了程序的句法结构。 Function成为包含函数体AST的AST节点。 if成为AST节点,包含条件和正文的AST。运算符的使用成为包含每个操作数的AST的AST节点。整数文字,变量名等成为叶AST节点。运算符优先级等在节点关系中是隐含的:1 * 2 + 3(1 * 2) + 3都表示为Add(Mul(Int(1), Int(2)), Int(3))

AST中的内容的许多细节取决于您的语言(显然)以及您想要对树做什么。如果要分析和转换程序(即最后拆分更改的源代码),您可以保留注释。如果您需要详细的错误消息,可以添加源位置(例如,此整数文字位于第5行第12列)。

编译器将继续将AST转换为不同的格式(例如,带有goto的线性IR或数据流图)。通过AST仍然是一个好主意,因为精心设计的AST在面向语法方面具有良好的平衡,但只存储对于理解程序的重要性。解析器可以专注于解析,同时保护以后的转换免受不相关的细节,例如空白量和运算符优先级。请注意,这样的“编译器”也可能输出稍后解释的字节码(Python的参考实现会这样做)。

相对纯粹的解释器可能会解释AST。关于这一点已经写了很多;它是执行解析器输出的最简单方法。这种策略从AST中获益的方式与编译器非常相似;特别是大多数解释只是自上而下的AST遍历。

答案 1 :(得分:2)

正式且最正确的答案是你应该返回一个抽象语法树。但这同时也是冰山一角,根本没有答案。

AST只是描述解析的节点结构;通过令牌/状态机对您的解析所采用的路径进行可视化。

每个节点代表一个路径或描述。例如,您将拥有表示语言语句的节点,表示编译器指令的节点和表示数据的节点。

考虑一个描述变量的节点,并假设您的语言支持int和string的变量以及“const”的概念。您可能选择使类型成为Variable节点struct / class的直接属性,但通常在AST中,您可以使属性(如constness)成为“mutator”,它本身是链接到Variable节点的某种形式的节点。 / p>

您可以通过将本地范围的变量作为BlockStatement节点的突变来实现“范围”的C ++概念; “循环”节点(for,do,while等)的约束作为mutators。

当你将解析器/标记器紧密地绑定到语言实现时,即使是很小的变化也会成为一场噩梦。

虽然这是真的,如果你真的想要了解这些东西是如何工作的,那么至少需要经历一个第一个实现,你开始实现你的运行时系统(vm,解释器等)并让你的解析器定位它直。 (另一种选择是,例如,购买“龙书”的副本,并阅读它应该如何完成,但听起来你实际上想要通过自己的问题得到充分的理解)。

告诉返回AST的麻烦是AST实际上需要一种解析形式。

struct Node
{
    enum class Type {
        Variable,
        Condition,
        Statement,
        Mutator,
    };

    Node*  m_parent;
    Node*  m_next;
    Node*  m_child;
    Type   m_type;
    string m_file;
    size_t m_lineNo;

};

struct VariableMutatorNode : public Node
{
    enum class Mutation {
        Const
    };
    Mutation m_mutation;
    // ...
};

struct VariableNode
{
    VariableMutatorNode* m_mutators;

    // ...
};

Node* ast;  // Top level node in the AST.

对于独立于运行时的编译器来说,这种AST可能是正常的,但是对于复杂的,性能敏感的语言来说,你需要对它进行大量收紧(此时'A'较少在'AST')。

你走这棵树的方式是从'ast'的第一个节点开始,并根据它来行动。如果您使用C ++编写,则可以通过将行为附加到每个节点类型来完成此操作。但同样,那不是那么“抽象”,是吗?

或者,你必须写一些通过树的方式。

switch (node->m_type) {
    case Node::Type::Variable:
        declareVariable(node);
        break;
    case Node::Type::Condition:
        evaluate(node);
        break;
    case Node::Type::Statement:
        execute(node);
        break;
}

当你写这篇文章时,你会发现自己在想“等等,为什么解析器不为我做这个?”因为处理AST通常感觉就像你执行AST的废话一样:)

有些时候你可以跳过AST并直接进入某种形式的最终表现形式,并且(很少)时候这是可取的;那么有时你可以直接进入某种形式的最终表现形式,但现在你必须改变语言,这个决定将花费你很多重新实现和头痛。

这通常也是构建编译器的核心 - 词法分析器和解析器通常是这种不足的较小部分。使用抽象/后解析表示是工作中更重要的部分。

这就是为什么人们经常直接选择flex / bison或antlr等等。

如果这就是你想做的事情,那么查看.NET或LLVM / Clang可能是一个不错的选择,但你也可以用这样的东西轻松地引导自己:http://gnuu.org/2009/09/18/writing-your-own-toy-compiler/4/

祝你好运:)

答案 2 :(得分:1)

我会建立一个陈述树。在那之后,是的goto语句是它的大部分工作原理(跳转和调用)。你是否正在翻译成像汇编一样的低级别?

答案 3 :(得分:1)

解析器的输出应该是一个抽象语法树,除非你对编写编译器直接产生字节码有足够的了解,如果这是你的目标语言。它可以一次完成,但你需要知道你在做什么。 AST直接表达循环和ifs:你不关心翻译它们。这是代码生成的。

答案 4 :(得分:1)

人们不使用lex / yacc来避免重新发明轮子,使用它来更快地构建更强大的编译器原型,更省力,并专注于语言,并避免陷入困境在其他细节。根据几个VM项目,编译器和汇编器的个人经验,我建议如果你想学习如何构建一种语言,那就做 - 专注于构建一种语言(第一)。

不要分散注意力:

  1. 编写自己的VM或运行时
  2. 编写自己的解析器生成器
  3. 编写自己的中间语言或汇编程序
  4. 你可以稍后再做。

    当一位聪明的年轻计算机科学家第一次发现语言发烧时,这是我常见的事情。 (以及它的好处),但是你需要小心并将精力集中在你想做的一件事上,并利用其他强大的成熟技术,如解析器生成器,词法分析器和运行时平台。当你先杀死编译器龙时,你总是可以稍后回圈。

    只需花费精力学习LALR语法如何工作,用Bison或Yacc ++编写语言语法,如果你仍能找到它,不要被那些说你应该使用ANTLR或其他任何东西的人分心,不是早期的目标。在早期,您需要专注于制作语言,消除歧义,创建正确的AST (可能是最重要的技能组合),语义检查,符号解析,类型解析,类型推断,隐式转换,树重写,当然还有结束程序生成。有足够的时间来制作一种合适的语言,你不需要学习一些人在整个职业生涯中掌握的其他研究领域。

    我建议你定位像CLR(.NET)这样的现有运行时。它是制作业余爱好语言的最佳运行时之一。使用IL的文本输出让您的项目离开地面,并与ilasm一起组装。假设您花了一些时间学习它,ilasm相对容易调试。一旦你获得原型,你就可以开始考虑其他的东西,比如你自己的解释器的替代输出,以防你的语言功能对于CLR过于动态(然后看看DLR)。这里的要点是CLR提供了一个良好的中间表示输出。不要听任何告诉你应该直接输出字节码的人。文本是早期学习的王者,允许您使用不同的语言/工具进行即插即用。一本好书由作者John Gough撰写,标题为 编译.NET公共语言运行时(CLR) ,他将带您完成Gardens Point Pascal编译器的实现,但它并不是关于Pascal的书,它是一本关于如何在CLR上构建真实编译器的书。它将回答您关于实现循环和其他高级构造的许多问题。

    与此相关,一个很好的学习工具是使用Visual Studio和ildasm(反汇编程序)和.NET Reflector。全部免费提供。您可以编写小代码示例,编译它们,然后反汇编它们以查看它们如何映射到基于IL的堆栈。

    如果您因任何原因对CLR不感兴趣,那么还有其他选择。您可能会在搜索中遇到llvm,Mono,NekoVM和Parrot(所有需要学习的好东西)。我是一个原始的Parrot VM / Perl 6开发人员,编写了Perl中间表示语言和imcc编译器(这是我可能添加的一段非常糟糕的代码)和第一个原型Perl 6编译器。我建议你远离Parrot并坚持使用像.NET CLR这样的东西,你会得到更多。但是,如果您想构建一个真正的动态语言,并希望将Parrot用于其延续和其他动态功能,请参阅O' Reilly Books Perl和Parrot Essentials (有几个版本),关于PIR / IMCC的章节是关于我的东西,并且很有用。如果你的语言没有动态,那就远离Parrot。

    如果您一心想编写自己的VM,请允许我建议您使用Perl,Python或Ruby对VM进行原型设计。我已成功完成了几次这样的事情。它允许您尽早避免过多的实施,直到您的语言开始成熟。 Perl + Regex很容易调整。 Perl或Python中的中间语言汇编程序需要几天时间才能编写。之后,如果您仍然喜欢,可以用C ++重写第二个版本。

    所有这些我可以总结:避免过早优化,并避免尝试一次完成所有事情。

答案 5 :(得分:0)

首先你需要一本好书。所以我在接下来的答案中向您推荐John Gough的书,但强调的是,重点学习首先为单个现有平台实现AST。它将帮助您了解AST实施。

如何实施循环?

您的语言分析器应在WHILE语句的reduce步骤中返回树节点。你可以命名你的AST类WhileStatement,而WhileStatement作为成员有Con​​ditionExpression和BlockStatement以及几个标签(也是可继承的,但为了清楚起见我添加了内联)。

下面的语法伪代码显示了reduce如何从典型的shift-reduce解析器缩减中创建WhileStatement的新对象。

shift-reduce解析器如何工作?

WHILE ( ConditionExpression )
    BlockStatement
    {
       $$ = new WhileStatement($3, $5);
       statementList.Add($$); // this is your statement list (AST nodes), not the parse stack
    }
 ;

当您的解析器看到" WHILE"时,它会移动堆栈上的令牌。等等。

parseStack.push(WHILE);
parseStack.push('(');
parseStack.push(ConditionalExpression);
parseStack.push(')');
parseStack.push(BlockStatement);

WhileStatement的实例是线性语句列表中的节点。在幕后," $$ ="代表一个解析减少(虽然如果你想要迂腐,$$ = ...是用户代码,并且解析器隐式地进行自己的减少,无论如何)。 reduce可以被认为是弹出生产右侧的标记,并替换为左侧的单个标记,减少了堆栈:

// shift-reduce
parseStack.pop_n(5);         // pop off the top 5 tokens ($1 = WHILE, $2 = (, $3 = ConditionExpression, etc.)
parseStack.push(currToken);  // replace with the current $$ token

您仍需要添加自己的代码,以便将语句添加到链接列表中,例如" statements.add(whileStatement)"所以你以后可以遍历这个。解析器没有这样的数据结构,它的堆栈只是暂时的。

在解析期间,使用适当的成员合成WhileStatement实例。 在后一阶段,实现访问者模式以访问每个语句并解析符号并生成代码。因此可以使用以下AST C ++类实现while循环:

class WhileStatement : public CompoundStatement {
    public:
        ConditionExpression * condExpression;  // this is the conditional check

        Label               * startLabel;      // Label can simply be a Symbol
        Label               * redoLabel;       // Label can simply be a Symbol
        Label               * endLabel;        // Label can simply be a Symbol
        BlockStatement      * loopStatement;  // this is the loop code

        bool ResolveSymbolsAndTypes();
        bool SemanticCheck();
        bool Emit();         // emit code
}

您的代码生成器需要具有为汇编程序生成顺序标签的函数。一个简单的实现是返回一个带有静态int的字符串的函数,该字符串递增,并返回LBL1,LBL2,LBL3等。您的标签可以是符号,或者您可能对Label类感兴趣,并使用构造函数来生成新标签:

class Label : public Symbol {
   public Label() {
      this.name = newLabel();  // incrementing LBL1, LBL2, LBL3
   }
}

通过生成condExpression的代码,然后是redoLabel,然后是blockStatement,并在blockStatement的末尾生成循环,然后转到redoLabel。

来自我的一个编译器的示例,用于为CLR生成代码。

 // Generate code for .NET CLR for While statement
 //
 void WhileStatement::clr_emit(AST *ctx)
 {
    redoLabel = compiler->mkLabelSym();
    startLabel = compiler->mkLabelSym();
    endLabel = compiler->mkLabelSym();

    // Emit the redo label which is the beginning of each loop
    compiler->out("%s:\n", redoLabel->getName());
    if(condExpr) {
       condExpr->clr_emit_handle();
       condExpr->clr_emit_fetch(this, t_bool);
       // Test the condition, if false, branch to endLabel, else fall through
       compiler->out("brfalse %s\n", endLabel->getName());
    }

    // The body of the loop
    compiler->out("%s:\n", startLabel->getName());   // start label only for clarity

    loopStmt->clr_emit(this);                        // generate code for the block

    // End label, jump out of loop
    compiler->out("br %s\n", redoLabel->getName());   // goto redoLabel
    compiler->out("%s:\n", endLabel->getName());      // endLabel for goto out of loop
 }