C# 为什么我的递归下降解析器是正确的
我正在编写自己的编程语言,我已经完成了标记器(lexer)。但是对于解析,我在编写递归下降解析器时遇到了困难。它似乎是右联想,什么时候应该是左联想,我不知道为什么。例如,它将C# 为什么我的递归下降解析器是正确的,c#,recursive-descent,associativity,C#,Recursive Descent,Associativity,我正在编写自己的编程语言,我已经完成了标记器(lexer)。但是对于解析,我在编写递归下降解析器时遇到了困难。它似乎是右联想,什么时候应该是左联想,我不知道为什么。例如,它将1-2-3解析为1-(2-3),而不是正确的(1-2)-3 我删掉了大部分其他代码,只留下了可复制的代码: using System.Collections.Generic; namespace Phi { public enum TokenType { Plus, // '+'
1-2-3
解析为1-(2-3)
,而不是正确的(1-2)-3
我删掉了大部分其他代码,只留下了可复制的代码:
using System.Collections.Generic;
namespace Phi
{
public enum TokenType
{
Plus, // '+'
Minus, // '-'
IntegerLiteral,
}
public interface INode
{
// Commented out as they aren't relevant
//NodeType GetNodeType();
//void Print(string indent, bool last);
}
class Program
{
static void Main(string[] args)
{
List<Token> tokens = new List<Token>()
{
new Token(TokenType.IntegerLiteral, "1"),
new Token(TokenType.Minus, ""),
new Token(TokenType.IntegerLiteral, "2"),
new Token(TokenType.Minus, ""),
new Token(TokenType.IntegerLiteral, "3"),
};
int consumed = ParseAdditiveExpression(tokens, out INode root);
}
private static int ParseAdditiveExpression(List<Token> block, out INode node)
{
// <additiveExpr> ::= <multiplicativeExpr> <additiveExprPrime>
int consumed = ParseMultiplicataveExpression(block, out INode left);
consumed += ParseAdditiveExpressionPrime(GetListSubset(block, consumed), out INode right);
if (block[1].Type == TokenType.Plus)
node = (right == null) ? left : new AdditionNode(left, right);
else
node = (right == null) ? left : new SubtractionNode(left, right);
return consumed;
}
private static int ParseAdditiveExpressionPrime(List<Token> block, out INode node)
{
// <additiveExprPrime> ::= "+" <multiplicataveExpr> <additiveExprPrime>
// ::= "-" <multiplicativeExpr> <additiveExprPrime>
// ::= epsilon
node = null;
if (block.Count == 0)
return 0;
if (block[0].Type != TokenType.Plus && block[0].Type != TokenType.Minus)
return 0;
int consumed = 1 + ParseMultiplicataveExpression(GetListSubset(block, 1), out INode left);
consumed += ParseAdditiveExpressionPrime(GetListSubset(block, consumed), out INode right);
if (block[0].Type == TokenType.Plus)
node = (right == null) ? left : new AdditionNode(left, right);
else
node = (right == null) ? left : new SubtractionNode(left, right);
return consumed;
}
private static int ParseMultiplicataveExpression(List<Token> block, out INode node)
{
// <multiplicativeExpr> ::= <castExpr> <multiplicativeExprPrime>
// unimplemented; all blocks are `Count == 1` with an integer
node = new IntegerLiteralNode(block[0].Value);
return 1;
}
private static List<T> GetListSubset<T>(List<T> list, int start)
{
return list.GetRange(start, list.Count - start);
}
}
}
至于我的问题,当我运行Phi.Program
时,正如前面所说,它是使用错误的关联性进行解析的。以下是ParseAdditiveExpression
完成后的root
:
如您所见,它将
2
与3
分组,而不是1
。它为什么这样做?正如我在一篇评论中指出的,问题是您将二进制运算符最右边的子级与AdditiveTime最右边的子级混淆了。二元运算符最右边的子运算符是表达式。AdditiveTime的最右边是AdditiveTime,因此仅以“树节点类型”为理由,我们就必须断定您构建了错误的解析树
跟踪每个解析工件的“逻辑类型”是在解析器中查找bug的一项强大技术。另一个我喜欢的,不幸的是没有得到充分利用的,是将程序中的每个标记都赋给一个解析树节点。如果您这样做了,那么您将很快意识到操作符的令牌在逻辑上位于两个位置:在二进制操作符中,以及在其最右边的子操作符中。这也告诉我们有些地方出了问题
没有帮助的是,你的解析基础设施是一堆乱七八糟的数字和参数的传递您的解析器缺乏纪律性。您的解析器代码看起来像是计算令牌是解析器所做的最重要的事情,其他一切都是偶然的
解析是一个非常明确的问题,解析器方法应该做一件事,只做一件事,并且做到完美。解析器的结构以及每个方法的结构应该直接反映正在解析的语法。解析器中几乎不应该有关于整数的算术,因为解析是关于构建解析树,而不是关于计算令牌
我以构建递归下降解析器为生。让我向您展示如何构建这个解析器,如果我为了自己的目的快速构建它的话。(如果我为生产应用程序构建它,在许多方面都会有所不同,但我们在这里要简单易懂。)
好了,我们开始吧。第一件事是:当你陷入一个问题时,解决一个更简单的问题。让我们用以下方式简化问题:
- 假设令牌流是格式良好的程序。无错误检测
- 标记是字符串
- 语法是:
,而E:=te',E':=+te'| nil
是由单个标记组成的术语李>T
sealed class Term : ParseTree
{
public string Value { get; private set; }
public Term(string value) { this.Value = value; }
public override string ToString() { return this.Value; }
}
sealed class Additive : ParseTree
{
public ParseTree Term { get; private set; }
public ParseTree Prime { get; private set; }
public Additive(ParseTree term, ParseTree prime) {
this.Term = term;
this.Prime = prime;
}
public override string ToString() { return "" + this.Term + this.Prime; }
}
sealed class AdditivePrime : ParseTree
{
public string Operator { get; private set; }
public ParseTree Term { get; private set; }
public ParseTree Prime { get; private set; }
public AdditivePrime(string op, ParseTree term, ParseTree prime) {
this.Operator = op;
this.Term = term;
this.Prime = prime;
}
public override string ToString() { return this.Operator + this.Term + this.Prime; }
}
sealed class Nil : ParseTree
{
public override string ToString() { return ""; }
}
请注意以下几点:
- 抽象类是抽象的李>
- 混凝土等级是密封的
- 一切都是不变的
- 任何东西都知道如何打印自己
- 没有空值无空值。空值会导致崩溃。您有一个名为nil的产品,因此创建一个名为
的类型来表示它nil
sealed class Parser
{
public Parser(List<string> tokens) { ... }
public ParseTree Parse() { ... }
}
很好,我们已经完成了解析器的两个成员。现在,parseaddition
做什么它按照锡罐上的指示执行。它解析一个加法表达式,它的语法是E::te'
,所以现在它就是这么做的,它所做的一切
private ParseTree ParseAdditive()
{
var term = ParseTerm();
var prime = ParseAdditivePrime();
return new Additive(term, prime);
}
如果您的解析器方法看起来不太简单,那么您就做错了。递归下降解析器的全部要点是它们易于理解和实现
现在我们可以看到如何实现ParseTerm()
;它只消耗一个令牌:
private string Consume()
{
var t = this.tokens[this.current];
this.current += 1;
return t;
}
private ParseTree ParseTerm() {
return new Term(Consume());
}
同样,我们假设令牌流是格式良好的。当然,如果格式不正确,这将崩溃,但这是另一天的问题
最后,最后一个比较难,因为有两种情况
private bool OutOfTokens()
{
return this.current >= this.tokens.Count;
}
private ParseTree ParseAdditivePrime()
{
if (OutOfTokens())
return new Nil();
var op = Consume();
var term = ParseTerm();
var prime = ParseAdditivePrime();
return new AdditivePrime(op, term, prime);
}
这么简单。同样,您的所有方法都应该与它们的功能完全相同
sealed class Term : ParseTree
{
public string Value { get; private set; }
public Term(string value) { this.Value = value; }
public override string ToString() { return this.Value; }
}
sealed class Additive : ParseTree
{
public ParseTree Term { get; private set; }
public ParseTree Prime { get; private set; }
public Additive(ParseTree term, ParseTree prime) {
this.Term = term;
this.Prime = prime;
}
public override string ToString() { return "" + this.Term + this.Prime; }
}
sealed class AdditivePrime : ParseTree
{
public string Operator { get; private set; }
public ParseTree Term { get; private set; }
public ParseTree Prime { get; private set; }
public AdditivePrime(string op, ParseTree term, ParseTree prime) {
this.Operator = op;
this.Term = term;
this.Prime = prime;
}
public override string ToString() { return this.Operator + this.Term + this.Prime; }
}
sealed class Nil : ParseTree
{
public override string ToString() { return ""; }
}
请注意,我没有写信
private ParseTree ParseAdditivePrime()
{
if (this.current >= this.tokens.Count)
return new Nil();
保持程序文本的可读性,就像它正在执行的操作一样。我们想知道什么我们的代币用完了吗?那么说吧。不要让读者——你自己——不得不思考哦,我的意思是
还是这个问题让人困惑。您编写了代码,因此询问它为什么要做某事是因为这就是您编写代码的目的。如果你不希望它是右关联的,那么为什么你要编写一个解析器,将二进制运算符解析为右关联?@EricLippert问题是,我不知道为什么它是右关联的。通过解决一个更简单的问题来解决这个问题。这是一种语言{“1”,“1+1”,“1+1+1”,“1+1+1”,…}。你已经有lexer了。现在给我写两个解析器,一个解析到右边,另一个解析到左边。然后,您将了解解析右边的解析器和解析左边的解析器之间的区别。您可以使用的另一种技术是前置条件和后置条件之一。您只有四种相关的方法。您相信令牌消费在调用之前和调用之后的状态。说出这些信念是什么,然后编写Debug.Assert
调用来检测这些信念是否错误
private string Consume()
{
var t = this.tokens[this.current];
this.current += 1;
return t;
}
private ParseTree ParseTerm() {
return new Term(Consume());
}
private bool OutOfTokens()
{
return this.current >= this.tokens.Count;
}
private ParseTree ParseAdditivePrime()
{
if (OutOfTokens())
return new Nil();
var op = Consume();
var term = ParseTerm();
var prime = ParseAdditivePrime();
return new AdditivePrime(op, term, prime);
}
private ParseTree ParseAdditivePrime()
{
if (this.current >= this.tokens.Count)
return new Nil();
private ParseTree ParseAdditivePrime() =>
OutOfTokens() ? new Nil() : new AdditivePrime(Consume(), ParseTerm(), ParseAdditivePrime());
sealed class Binary : ParseTree
{
public ParseTree Left { get; private set; }
public string Operator { get; private set; }
public ParseTree Right { get; private set; }
public Binary(ParseTree left, string op, ParseTree right)
{
this.Left = left;
this.Operator = op;
this.Right = right;
}
public override string ToString()
{
return "(" + Left + Operator + Right + ")";
}
}
private static ParseTree AdditiveToBinary(ParseTree left, ParseTree prime)
{
if (prime is Nil) return left;
var reallyprime = (AdditivePrime) prime;
var binary = new Binary(left, reallyprime.Operator, reallyprime.Term);
return AdditiveToBinary(binary, reallyprime.Prime);
}
private ParseTree ParseAdditive()
{
var term = ParseTerm();
var prime = ParseAdditivePrime();
return AdditiveToBinary(term, prime);
}
(((1+2)+3)+4)