为编程语言编写解析器:Output
Writing a Parser for a programming language: Output
我正在尝试用C++编写一种简单的解释编程语言。我读到很多人使用Lex/Flex Bison这样的工具来避免"重新发明轮子",但由于我的目标是了解这些小野兽是如何提高我的知识的,我决定从头开始编写Lexer和Parser。目前我正在开发解析器(lexer已经完成),我在问自己它的输出应该是什么。一棵树?带有"depth"或"shift"参数的语句的线性向量?我应该如何管理循环和if语句?我应该用不可见的goto语句替换它们吗?
解析器几乎应该总是输出AST。从最广泛的意义上讲,AST只是程序语法结构的树表示。Function变为包含函数体AST的AST节点。CCD_ 1成为包含条件和主体的AST的AST节点。运算符的使用将成为包含每个操作数的AST的AST节点。整型文字、变量名等将成为叶AST节点。运算符优先级等隐含在节点关系中:1 * 2 + 3
和(1 * 2) + 3
都表示为Add(Mul(Int(1), Int(2)), Int(3))
。
AST中的许多细节(显然)取决于您的语言以及您想对树做什么。如果你想分析和转换程序(即在最后分割出修改后的源代码),你可以保留注释。如果您想要详细的错误消息,可以添加源位置(如中所示,此整数文本位于第5行第12列)。
编译器将继续将AST转换为不同的格式(例如,具有goto
s的线性IR或数据流图)。浏览AST仍然是一个好主意,因为设计良好的AST在面向语法和只存储对理解程序重要的内容之间有很好的平衡。解析器可以专注于解析,而后面的转换则受到保护,不受无关细节的影响,如空白量和运算符优先级。请注意,这样的"编译器"也可能输出稍后解释的字节码(Python的引用实现会这样做)。
一个相对纯粹的解释器可能会解释AST。关于这一点,已经写了很多文章;这是执行解析器输出的最简单方法。这种策略从AST中获益的方式与编译器非常相似;特别是,大多数解释只是AST的自上而下遍历。
正式且最正确的答案是,您应该返回一个抽象语法树。但这只是冰山一角,根本没有答案。
AST只是描述解析的节点结构;解析通过令牌/状态机的路径的可视化。
每个节点表示一个路径或描述。例如,您将有表示语言语句的节点、表示编译器指令的节点和表示数据的节点。
考虑一个描述变量的节点,假设您的语言支持int和string变量以及"const"的概念。您可以选择将该类型设置为Variable节点结构/类的直接属性,但通常在AST中,您会将constness等属性设置为"mutator",它本身就是链接到Variable节点的某种形式的节点。
您可以通过将局部作用域变量作为BlockStatement节点的突变来实现C++的"范围"概念;"循环"节点的约束(for、do、while等)作为变元器。
当您将解析器/标记化器与语言实现紧密联系在一起时,即使进行很小的更改也可能成为一场噩梦。
虽然这是真的,但如果你真的想了解这些东西是如何工作的,那么至少有一个第一次实现是值得的,在那里你开始实现你的运行时系统(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可能是可以的,但对于一种复杂的、对性能敏感的语言来说,你需要把它收紧很多(此时"AST"中的"a"更少)。
你走这棵树的方式是从"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,直接进入某种形式的最终表示,有时(罕见)这是可取的;有时,可以直接进入某种形式的最终表示,但现在你必须更改语言,这个决定将花费你大量的重新实现和头痛。
这通常也是构建编译器的核心——lexer和解析器通常是这种不足的部分。使用抽象/后解析表示是工作中更重要的部分。
这就是为什么人们经常直接去flex/bison或antlr或诸如此类的地方。
如果这就是你想要做的,那么看看.NET或LLVM/Clang可能是一个不错的选择,但你也可以很容易地用这样的东西引导自己:http://gnuu.org/2009/09/18/writing-your-own-toy-compiler/4/
祝你好运:)
我会构建一个语句树。在那之后,是的,goto语句就是它的大部分工作方式(跳转和调用)。你是在翻译成一个低级的程序集吗?
解析器的输出应该是一个抽象语法树,除非你对编写编译器以直接生成字节码有足够的了解,如果这是你的目标语言的话。这可以一蹴而就,但你需要知道自己在做什么。AST直接表达循环和if:您还不关心翻译它们。这属于代码生成。
人们使用lex/yacc不是为了避免重新发明轮子,而是为了更快、更轻松地构建更健壮的编译器原型,并专注于语言,避免陷入其他细节。根据我在几个VM项目、编译器和汇编程序方面的个人经验,我建议如果你想学习如何构建一种语言,就这样做吧——专注于构建语言(首先)。
不要分心:
- 编写自己的VM或运行时
- 编写自己的解析器生成器
- 编写自己的中间语言或汇编程序
你可以稍后再做。
当一个聪明的年轻计算机科学家第一次染上"语言热"(这是一件好事)时,我看到了这一点,但你需要小心,把精力集中在你想做好的一件事上,并利用其他强大、成熟的技术,如解析器生成器、lexer和运行时平台。当你先杀死编译器龙的时候,你总是可以稍后再回来。
只要花你的精力学习LALR语法是如何工作的,用Bison或Yacc++编写你的语言语法,如果你仍然能找到它,不要被那些说你应该使用ANTLR或其他任何东西的人分散注意力,这不是早期的目标。早期,你需要专注于打造你的语言,消除歧义,创建一个合适的AST(也许是最重要的技能集),语义检查、符号解析、类型解析、类型推理、隐式转换、树重写,当然还有最终程序生成。制作一门合适的语言已经足够了,你不需要学习其他多个研究领域,有些人花了整个职业生涯来掌握这些领域。
我建议您针对CLR(.NET)这样的现有运行时。它是制作业余语言的最佳运行时之一。使用到IL的文本输出启动您的项目,并使用ilasm进行组装。假设你花了一些时间学习ilasm,它相对容易调试。一旦你启动了原型,你就可以开始考虑其他事情,比如为自己的解释器提供替代输出,以防你的语言功能对CLR来说过于动态(然后看看DLR)。这里的要点是,CLR提供了一个很好的中间表示来输出。不要听任何人告诉你应该直接输出字节码。文本是早期学习的王者,可以让你使用不同的语言/工具。作者约翰·高夫写了一本好书,书名为《编译》。NET公共语言运行时(CLR),他将带您了解Gardens Point Pascal编译器的实现,但这不是一本关于Pascal的书,而是一本关于如何在CLR上构建真正编译器的书。它将回答您关于实现循环和其他高级构造的许多问题。
与此相关,一个很好的学习工具是使用Visual Studio和ildasm(反汇编程序)和。NET反射器。全部免费。您可以编写小代码示例,编译它们,然后对它们进行反汇编,以查看它们如何映射到基于堆栈的IL
如果您出于任何原因对CLR不感兴趣,还有其他选择。您可能会在搜索中遇到llvm、Mono、NekoVM和Parrot(所有这些都是值得学习的好东西)。我是一个最初的Parrot VM/Perl 6开发人员,编写了Perl中间表示语言和imcc编译器(我可能会添加一段相当糟糕的代码)以及第一个原型Perl6编译器。我建议你远离鹦鹉,坚持做一些更容易的事情,比如。NET CLR,你会得到更多。然而,如果您想构建一种真正的动态语言,并想使用Parrot来实现其延续和其他动态功能,请参阅O’Reilly BooksPerl和Parrot Essentials(有几个版本),关于PIR/IMCC的章节是关于我的东西的,非常有用。如果你的语言不是动态的,那就远离鹦鹉。
如果你想编写自己的虚拟机,我建议你用Perl、Python或Ruby制作虚拟机的原型。我已经成功地做过几次了。它允许您避免过早地进行过多的实现,直到您的语言开始成熟。Perl+Regex很容易调整。Perl或Python中的中间语言汇编程序需要几天的编写时间。稍后,如果你仍然喜欢,你可以用C++重写第二个版本。
我可以总结所有这些:避免过早的优化,避免尝试一次完成所有事情。
首先你需要一本好书。因此,在我的另一个答案中,我请您参阅John Gough的书,但要强调,首先要专注于学习为单个现有平台实现AST。它将帮助您了解AST的实现。
如何实现循环
在WHILE语句的reduce步骤中,语言解析器应该返回一个树节点。您可以将AST类命名为WhileStatement,WhileStatements具有ConditionExpression和BlockStatement以及几个标签(也是可继承的,但为了清晰起见,我添加了内联)作为成员。
下面的语法伪代码显示了reduce如何从典型的shift-reduce语法分析器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(尽管如果你想变得迂腐,$$=…是用户代码,解析器正在隐式地进行自己的reduce)。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实例及其相应的成员。在后一阶段,实现访问者模式来访问每个语句,解析符号并生成代码。因此,while循环可以用以下ASTC++类来实现:
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,在blockStatements的末尾,转到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 %sn", 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 %sn", redoLabel->getName()); // goto redoLabel
compiler->out("%s:n", endLabel->getName()); // endLabel for goto out of loop
}
- 了解算法的性能差异(如果以不同的编程语言实现)
- 为什么编程语言被编译为汇编程序而不是二进制?
- 如何在同时包含C++和Python的项目(多编程语言项目)中使用doxygen
- 什么是编程语言支持定义您自己的自定义运算符?
- 如何通过不同的编程语言发送,接收和解析XML消息
- 今天的主流编程语言主要使用动态还是静态(词汇)作用域?
- 谁以编程语言(例如C )制定标准
- 如何使用任何编程语言组合序列中的多个图像
- 我可以使用功能指针在编程语言边界上调用函数
- 有没有办法将cin.fail和cin.clear翻译成C编程语言
- 编程语言中的 char-int 等价性
- C 编程语言帮助我
- 从其他编程语言调用 c++ dll 类函数
- 值和对象不同的编程语言
- 返回 2 语句的含义 c++ 编程语言
- 互联网连接速度与HTTP请求的编程语言速度
- 在什么编程语言游戏引擎上编写"Frostbit 3"?
- 在 "Code Blocks" IDE 中混合编程语言?
- 一些应用程序是如何用几种编程语言制作的
- 为编程语言编写解析器:Output