为什么我的递归下降解析器是右关联的

时间:2018-06-13 21:42:51

标签: c# recursive-descent associativity

我正在编写自己的编程语言,并且我完成了令牌器(词法分析器)。但是对于解析,我在编写递归下降解析器时遇到了麻烦。它似乎是正确的联想,应该留下,我不知道为什么。例如,它将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);
        }
    }
}

至于AdditionNodeSubtractionNodeMultiplicationNode的定义,它们都是相同的,仅用于语义目的。为简洁起见,这里只是AdditionNode

namespace Phi
{
    public class AdditionNode : INode
    {
        public AdditionNode(INode left, INode right)
        {
            Left = left;
            Right = right;
        }

        public INode Left { get; }
        public INode Right { get; }

        // Print and GetNodeType have been removed as they aren't relevant
    }
}

至于我的问题,当我运行Phi.Program时,正如开头所说,它正在解析错误的关联性。在root完成后ParseAdditiveExpression

enter image description here enter image description here enter image description here

如您所见,它将23分组,而不是1。为什么要这样做?

1 个答案:

答案 0 :(得分:20)

正如我在评论中所指出的那样,问题在于你已经将二元运算符中最右边的孩子与添加剂最重要的孩子混淆了。二元运算符的最右边的子节点是表达式。添加剂的最右边是 additiveprime ,所以&#34;树节点类型&#34;我们必须得出结论,你已经构建了一个错误的解析树。

跟踪&#34;逻辑类型&#34;每个解析工件都是一种在解析器中查找错误的强大技术。另一个我喜欢的,可悲的是未充分利用,将程序中的每个标记归属于一个解析树节点。如果你这样做,那么你很快就会意识到运算符的标记在逻辑上位于两个位置:在二元运算符中,在最右边的子节点中。这也告诉我们出了问题。

没有什么帮助,你的解析基础设施是一个传递数字和输出参数的混乱。 您的解析器缺乏纪律。您的解析器代码看起来像计算令牌是解析器执行的最重要的事情,而其他一切都是偶然的。

解析是一个非常清晰的问题,解析器方法应该做一件事,只做一件事,并且做得很好。解析器的结构和每个方法的结构应直接反映正在解析的语法。在解析器中应该几乎没有关于整数的算术,因为解析是关于构建一个解析树,而不是关于计数标记。

我构建了递归下降解析器以谋生。让我告诉你如何构建这个解析器,我是否为了自己的目的快速构建它。 (如果我为生产应用程序构建它,它在许多方面会有所不同,但我们在这里很容易理解。)

好的,让我们开始吧。首先是:当你遇到问题时,解决一个更简单的问题。让我们通过以下方式简化问题:

  • 假设令牌流是格式良好的程序。没有错误检测。
  • 代币是字符串。
  • 语法为:E ::= T E', E' ::= + T E' | nilT是由单个标记组成的术语。

好的。 首先创建代表这些内容的类型

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 ""; }
}

请注意以下几点:

  • 抽象类是抽象的。
  • 混凝土类已密封。
  • 一切都是不变的。
  • 一切都知道如何打印自己。
  • 没有空! NO NULLS 。空无聊会导致崩溃。您有一个名为nil的产品,因此请创建一个名为Nil的类型来表示它。

下一步:从用户的角度来看,我们希望解析器看起来像什么?我们想要一系列令牌进入,我们想要一个解析树出来。大。所以公众面应该是:

sealed class Parser
{
    public Parser(List<string> tokens) { ... }
    public ParseTree Parse() { ... }
}

如果我们已经做好了一切,那么呼叫网站就是这样的:

public static void Main()
{
    var tokens = new List<string>() { "1" , "+" , "2" , "+" , "3" , "+" , "4"};
    var parser = new Parser(tokens);
    var result = parser.Parse();
    System.Console.WriteLine(result);
}

超级。现在我们所要做的就是实现它。

解析器会跟踪令牌列表和正在考虑的当前令牌。 不要将这些信息从方法转移到方法。它在逻辑上是解析器的一部分,因此请将其保存在解析器中。

public sealed class Parser
{
    private List<string> tokens;
    private int current;    
    public Parser(List<string> tokens)
    {
        this.tokens = tokens;
        this.current = 0;
    }

语言现在只包含加法表达式,所以:

    public ParseTree Parse()
    {
        return ParseAdditive();
    }

很好,我们已经完成了解析器的两个成员。现在,ParseAdditive做了什么? 它完成了它在锡上的说法。它解析了一个加法表达式,它具有语法E:: T E',所以它就是它所做的和它所做的所有,现在。

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);
}

如此简单。同样,所有方法应该看起来就像他们所做的那样

请注意,我没有写

private ParseTree ParseAdditivePrime()
{
    if (this.current >= this.tokens.Count)
        return new Nil();

将程序文本保持为正在执行的操作。我们想知道什么? 我们是否没有令牌?所以。不要让读者 - 你自己 - 必须考虑哦,我的意思是><<=或者......不要。解决问题一次,将解决方案放在一个名称好的方法中,然后调用该方法。你未来的自我将会感谢你过去的自我照顾。

另请注意,我没有写过C#7超级光滑:

private ParseTree ParseAdditivePrime() => 
  OutOfTokens() ? new Nil() : new AdditivePrime(Consume(), ParseTerm(), ParseAdditivePrime());

可以将解析器方法编写为单行,这表明您已经设计了一个好的解析器,但这并不意味着您应该< / em>的。如果你将它保持在顺序语句形式而不是光滑的小单行中,它通常更容易理解和调试解析器。做好判断。

好的,我们已经解决了更容易的问题!现在让我们解决稍微更难的问题。我们已经解决了解析语法E ::= T E', E' ::= + T E' | nil,但我们想要解析的语法是B :: T | B + T

请注意,我不会混淆E,这是B的术语和后缀,可以是TB,一个+和一个T。由于BE不同,因此请按不同类型表示。

让我们为B

创建一个类型
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 + ")";
    }
}

请注意,我在输出中添加了括号作为视觉辅助,以帮助我们看到它是左关联的。

现在,假设我们手头有Additive,我们需要Binary我们该怎么做?

添加剂总是一个术语和一个素数。所以有两种情况。素数是零,或者不是。

如果素数为零,那么我们就完成了:Term在需要Binary的情况下是可以接受的,所以我们可以传回这个词。

如果素数不是零,那么素数是op,term,prime。 不知何故,我们必须从中获取Binary 。二进制需要三件事。 请记住,我们将每个令牌归结为一个节点,因此这有助于我们解决这个问题。

  • 我们的左项来自添加剂。
  • 我们有来自黄金时段的操作。
  • 我们有正确的术语。

但这仍然是最重要的巅峰之作!我们需要做点什么。让我们说出来吧。同样,有两种情况:

  • 如果素数素数为零,则结果为二进制。
  • 如果素数的素数不是nil,那么结果是左边有旧二进制的新二进制数,并且...... 等一下,这就是我们刚才描述的算法用于将添加剂转换为二进制

我们刚刚发现这个算法是递归的,一旦你意识到它写起来很简单:

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);
}

现在我们修改了ParseAdditive

private ParseTree ParseAdditive()
{
    var term = ParseTerm();
    var prime = ParseAdditivePrime();
    return AdditiveToBinary(term, prime);       
}     

运行它:

(((1+2)+3)+4)

我们已经完成了。

嗯,不太好。 ParseAdditive不再按照它在锡上说的那样做!它显示ParseAdditive但它返回Binary

事实上...... 我们需要Additive 吗?我们可以完全从解析器中消除它吗?事实上我们可以;我们现在从不创建Additive 的实例,因此可以将其删除,ParseAdditive可以重命名为ParseBinary

这通常在使用&#34;解决更简单的问题&#34;的技术构建程序时发生。你最终能够丢弃你以前的工作,这是伟大的。删除的代码没有错误。

  • 练习:将运算符表示为字符串是粗略的。创建一个类似于Operator的{​​{1}}类,以及一个解析运算符的方法。 继续将解析器的抽象级别从具体字符串提升到解析器的业务域。与令牌类似;他们不应该是字符串。
  • 练习:我们已经解决了一个稍微难点的问题,所以继续前进。你现在可以添加乘法吗?
  • 练习:你能解析一个混合了左右关联运算符的语言吗?

一些额外的想法:

  • 我认为你这样做是为了你自己的乐趣,或者是为了学校作业。 请勿将我的作品粘贴到作业中。这是学术上的欺诈行为。 如果您的工作不完全属于您的工作,请记住在提交时正确归因所有工作。

  • 如果你是为了好玩而做的,那就玩得开心吧!这是一个很好的爱好,如果你真的很幸运,有一天会有人付钱给你。

  • 您正在设计自己的语言,因此您不必重复过去的错误。例如,我注意到您的评论表明您要添加强制转换表达式。欢迎来到一个痛苦的世界,如果你这样做,如C,C ++,C#,Java等。所有这些语言都必须让解析器在Term含义和#34之间消除歧义;对y应用一元加上并将事物转换为(x)+y&#34;和&#34;添加数量{{1}到x&#34;。这是一个重大的痛苦。考虑使用更好的转换语法,例如(x)运算符。另外,检查一个演员的意思;在C#中,强制转换意味着&#34;生成一个代表相同值的不同类型的实例&#34;和&#34;我断言这个东西的运行时类型与它的编译时类型不同;扔掉,如果我错了&#34;。这些操作完全不同,但它们具有相同的语法。所有语言都是对以前语言的回应,因此请仔细考虑您喜欢的内容,因为它很熟悉 vs 因为它很好