手编码解析器

时间:2010-04-13 19:40:13

标签: c# compiler-construction parsing lexer

对于你所有的编译大师,我想编写一个递归下降解析器,我想用代码来做。没有从其他语法中生成词法分析器和解析器并且不告诉我阅读龙书,我最终会到达那里。

我想深入了解有关为合理的简单语言实现词法分析器和解析器的细节,比如说CSS。我想这样做。

这可能最终会成为一系列问题,但现在我开始使用词法分析器。可以找到CSS的标记规则here

我发现自己编写了这样的代码(希望你可以从这个代码片段中推断出其余部分):

public CssToken ReadNext()
{
    int val;
    while ((val = _reader.Read()) != -1)
    {
        var c = (char)val;
        switch (_stack.Top)
        {
            case ParserState.Init:
                if (c == ' ')
                {
                    continue; // ignore
                }
                else if (c == '.')
                {
                    _stack.Transition(ParserState.SubIdent, ParserState.Init);
                }
                break;

            case ParserState.SubIdent:
                if (c == '-')
                {
                    _token.Append(c);
                }
                _stack.Transition(ParserState.SubNMBegin);
                break;

这叫什么?我离合理的东西有多远了?我试图平衡一些在效率和易于使用方面公平的东西,使用堆栈来实现某种状态机工作得很好,但我不确定如何继续这样做。

我所拥有的是一个输入流,我可以从中一次读取1个字符。我现在不做任何看法,我只是阅读角色,然后根据当前的状态尝试做一些事情。

我真的很想进入编写可重用代码片段的思维模式。这个Transition方法目前是这样做的,它将弹出堆栈的当前状态,然后以相反的顺序推送参数。这样,当我写Transition(ParserState.SubIdent, ParserState.Init)时,它将“调用”子例程SubIdent,当完成后,它将返回Init状态。

解析器将以大致相同的方式实现,目前,在这样的单个大方法中,所有内容都允许我在找到令牌时轻松返回令牌,但它也迫使我将所有内容保存在一个单一的大方法中。有没有一种很好的方法可以将这些标记化规则拆分成单独的方法?

5 个答案:

答案 0 :(得分:28)

您所写的内容称为下推自动机。这通常比编写词法分析器所需的功能更强大,如果你正在为像CSS这样的现代语言编写词法分析器,那肯定会过度。递归下降解析器接近下推自动机,但递归下降解析器更容易编写和理解。大多数解析器生成器生成下推自动机。

Lexers几乎总是被写成有限状态机,就像你的代码一样,除了摆脱“堆栈”对象。有限状态机与正则表达式密切相关(实际上,它们可以证明彼此相当)。在设计这样的解析器时,通常从正则表达式开始,并使用它们来创建确定性有限自动机,在转换中使用一些额外的代码来记录每个令牌的开头和结尾。

有工具可以做到这一点。 lex工具及其后代是众所周知的,并已被翻译成多种语言。 ANTLR工具链还有一个词法分析器组件。在支持它的平台上,我首选的工具是ragel。手动编写词法分析器大多数几乎没有什么好处,这些工具生成的代码可能更快,更可靠。

如果你想手工编写自己的词法分析器,那么好的词法分析器通常看起来像这样:

function readToken() // note: returns only one token each time
    while !eof
        c = peekChar()
        if c in A-Za-z
            return readIdentifier()
        else if c in 0-9
            return readInteger()
        else if c in ' \n\r\t\v\f'
            nextChar()
        ...
    return EOF

function readIdentifier()
    ident = ""
    while !eof
        c = nextChar()
        if c in A-Za-z0-9
            ident.append(c)
        else
            return Token(Identifier, ident)
            // or maybe...
            return Identifier(ident)

然后,您可以将解析器编写为递归下降解析器。不要试图将词法分析器/解析器阶段合二为一,这会导致代码混乱。 (根据Parsec作者的说法,它也慢了。)

答案 1 :(得分:5)

如果您要从头开始编写所有代码,我肯定会考虑使用递归的正确解析器。在你的帖子中,你并没有真正说明一旦解析了源代码后你将使用令牌流做什么。

我会建议你处理一些事情 1.您的扫描仪/词法分析器的良好设计,这将是您的解析器的源代码的标记 2.接下来就是解析器,如果你有一个很好的源语言ebnf,解析器通常可以很好地翻译成递归的解析器。
3.您真正需要掌握的另一个数据结构是符号表。这可以像哈希表一样简单,也可以像树结构一样复杂,可以代表复杂的记录结构等。我认为CSS可能介于两者之间。
最后你要处理代码生成。你有很多选择。对于解释器,您可能只是在解析代码时动态解释。一个更好的方法可能是生成一个i代码,然后你可以编写一个iterpreter,甚至编写一个编译器。当然对于.NET平台,您可以直接生成IL(可能不适用于CSS :))

作为参考,我认为你并不沉重于深层理论,我不怪你。如果你不介意Pascal,那么获得没有复杂代码的基础知识的一个非常好的起点是Jack Crenshaw的'让我们构建一个编译器' http://compilers.iecc.com/crenshaw/
祝你好运我相信你会喜欢这个项目。

答案 2 :(得分:4)

您需要从BNF / EBNF编写自己的Recursive Descent Parser。我最近不得不写自己的this page给了我很多帮助。我不确定“只用代码”是什么意思。你的意思是你想知道如何编写自己的递归解析器吗?

如果你想这样做,你需要先掌握你的语法。一旦你的EBNF / BNF到位,就可以很容易地从中编写解析器。

我编写解析器时所做的第一件事就是读取所有内容然后将文本标记化。所以我基本上得到了一个我被视为堆栈的令牌数组。为了减少从堆栈中提取值然后在不需要时将其重新打开的冗长/开销,您可以使用peek方法简单地返回堆栈中的顶部值而不会弹出它。

<强>更新

根据你的评论,我不得不从头开始在Javascript中编写一个递归下降的解析器。您可以查看解析器here。只需搜索constraints功能即可。我编写了自己的tokenize函数来标记输入。我还写了另一个便利函数(peek,我之前提到过)。解析器根据EBNF here进行解析。

这花了我一点时间才弄明白,因为我写了一个解析器已经好几年了(上次我写的是在学校!),但相信我,一旦你得到它,你得到它。我希望我的榜样让你继续前行。

另一个更新

我也意识到我的例子可能不是你想要的,因为你可能会使用shift-reduce解析器。你提到现在你正在尝试编写一个tokenizer。就我而言,我确实在Javascript中编写了自己的tokenizer。它可能不健壮,但它足以满足我的需求。

 function tokenize(options) {
            var str = options.str;
            var delimiters = options.delimiters.split("");
            var returnDelimiters = options.returnDelimiters || false;
            var returnEmptyTokens = options.returnEmptyTokens || false;
            var tokens = new Array();
            var lastTokenIndex = 0;

            for(var i = 0; i < str.length; i++) {
                if(exists(delimiters, str[i])) {
                    var token = str.substring(lastTokenIndex, i);

                    if(token.length == 0) {
                        if(returnEmptyTokens) {
                            tokens.push(token);
                        }
                    }

                    else {
                        tokens.push(token);
                    }

                    if(returnDelimiters) {
                        tokens.push(str[i]);
                    }

                    lastTokenIndex = i + 1;
                }
            }

            if(lastTokenIndex < str.length) {
                var token = str.substring(lastTokenIndex, str.length);
                token = token.replace(/^\s+/, "").replace(/\s+$/, "");

                if(token.length == 0) {
                    if(returnEmptyTokens) {
                        tokens.push(token);
                    }
                }

                else {
                    tokens.push(token);
                }
            }

            return tokens;
        }

根据您的代码,它看起来像是在同时读取,标记和解析 - 我假设这是一个shift-reduce解析器的作用?我所拥有的流程首先是标记化来构建标记堆栈,然后通过递归下降解析器发送标记。

答案 3 :(得分:4)

看起来您想要实现"shift-reduce" parser,您可以在其中显式构建令牌堆栈。通常的替代方案是“递归下降”解析器,其中过程调用的深度在实际硬件堆栈上使用它们自己的局部变量构建相同的令牌堆栈。

在shift-reduce中,术语“reduce”是指在显式维护的令牌堆栈上执行的操作。例如,如果堆栈的顶部变为Term, Operator, Term,则可以应用缩减规则,从而导致Expression替换模式。减少规则在shift-reduce解析器使用的数据结构中明确编码;因此,所有缩减规则都可以在源代码的相同位置找到。

与递归下降相比,降低移位方法带来了一些好处。在主观层面上,我认为shift-reduce比递归下降更容易阅读和维护。更客观地说,shift-reduce允许在发生意外令牌时从解析器获得更多信息性错误消息。

具体来说,因为shift-reduce解析器具有用于进行“缩减”的规则的明确编码,所以解析器可以容易地扩展以清楚地表达可以合法地遵循什么类型的令牌。 (例如,“;预期”)。递归下降实现不能轻易扩展到同样的事情。

关于两种解析器的好书,以及实现不同类型的shift-reduce的权衡是"Introduction to Compiler Construction", by Thomas W. Parsons

Shift-reduce有时被称为“自下而上”解析,递归下降有时被称为“自上而下”解析。在所使用的类比中,具有最高优先级的节点(例如,乘法表达式中的“因子”)被认为是解析的“底部”。这与“递归下降”的“下降”中使用的相同类比是一致的。

答案 4 :(得分:3)

如果你想使用解析器来处理格式不正确的表达式,你真的需要一个递归下降解析器。更容易使错误处理和报告可用。

对于文学,我会推荐一些Niklaus Wirth的旧作。他知道怎么写。 算法+数据结构=程序是我使用的,但您可以在线找到他的Compiler Construction