为什么将词法分析器实现为二维数组和巨型开关?


24

我正在慢慢地完成学业,这个学期是Compilers101。我们正在使用Dragon Book。在课程开始不久,我们将讨论词法分析以及如何通过确定性有限自动机(以下称DFA)来实现它。设置各种词法分析器状态,定义它们之间的过渡等。

但是教授和这本书都建议通过过渡表来实现它们,过渡表相当于一个巨大的2d数组(一个维的各种非终端状态,而另一个维可能的输入符号),以及一个用于处理所有终端的switch语句以及在非终端状态下调度到过渡表。

这个理论很好,但是作为一个几十年来实际编写代码的人,实现是不道德的。它不可测试,不可维护,不可读,调试起来很麻烦。更糟糕的是,如果该语言具有UTF功能,那么我将看不到它在远程实用。每个非终端状态都有一百万个左右的过渡表条目,这会很不方便。

那怎么办?为什么有关该主题的权威书籍说要这样做呢?

函数调用的开销真的那么多吗?当语法不为人所知时(正则表达式?),这是否行得通?也许可以处理所有情况的东西,即使更具体的解决方案更适合于更具体的语法?

注意:可能重复的“ 为什么使用OO方法而不是巨大的switch语句? ”已经很接近了,但我并不关心OO。使用功能性方法甚至具有独立功能的更明智的命令式方法都可以。)

并且为了示例,考虑一种仅具有标识符的语言,而这些标识符为[a-zA-Z]+。在DFA实施中,您将获得以下内容:

private enum State
{
    Error = -1,
    Start = 0,
    IdentifierInProgress = 1,
    IdentifierDone = 2
}

private static State[][] transition = new State[][]{
    ///* Start */                  new State[]{ State.Error, State.Error (repeat until 'A'), State.IdentifierInProgress, ...
    ///* IdentifierInProgress */   new State[]{ State.IdentifierDone, State.IdentifierDone (repeat until 'A'), State.IdentifierInProgress, ...
    ///* etc. */
};

public static string NextToken(string input, int startIndex)
{
    State currentState = State.Start;
    int currentIndex = startIndex;
    while (currentIndex < input.Length)
    {
        switch (currentState)
        {
            case State.Error:
                // Whatever, example
                throw new NotImplementedException();
            case State.IdentifierDone:
                return input.Substring(startIndex, currentIndex - startIndex);
            default:
                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;
        }
    }

    return String.Empty;
}

(尽管可以正确处理文件结尾的内容)

与我期望的相比:

public static string NextToken(string input, int startIndex)
{
    int currentIndex = startIndex;
    while (currentIndex < startIndex && IsLetter(input[currentIndex]))
    {
        currentIndex++;
    }

    return input.Substring(startIndex, currentIndex - startIndex);
}

public static bool IsLetter(char c)
{
    return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
}

NextToken一旦您从DFA开始就有多个目标,就可以将代码重构为自己的功能。


5
是否继承了古代(1977年)的《编译器设计原理》?40年前,编码风格大为不同
咬到

7
您将如何实施DFA状态的转换?而关于末尾和非末尾的含义是什么,“非末尾”通常是指语法中的生成规则,该规则将词法分析之后得出。

10
这些表并不是人类可读的,它们是编译器可用的并且可以非常快速地执行。在输入中向前看时,很容易在桌子上跳来跳去(例如,捕捉左递归,尽管实际上大多数语言都是为了避免这种情况而建立的)。

5
如果您的某些恼怒来自于知道如何做得更好,并且缺乏对您喜欢的方法的任何反馈或赞赏的能力-数十年来,行业确实训练我们期望获得反馈,有时甚至是赞赏-也许您应该编写更好的实现,然后将其发布到CodeReview.SE,以便您放心使用。
吉米·霍法

7
简单的答案是因为词法分析器通常是作为有限状态机实现的,并且是根据语法自动生成的-因此,毫无疑问,状态表最容易且紧凑地表示为表。与目标代码,事实上,它是不容易为人类与工作无关,因为人类使用它; 他们更改源并生成新实例。
keshlam 2014年

Answers:


16

实际上,这些表是从定义语言标记的正则表达式生成的:

number := [digit][digit|underscore]+
reserved_word := 'if' | 'then' | 'else' | 'for' | 'while' | ...
identifier := [letter][letter|digit|underscore]*
assignment_operator := '=' | '+=' | '-=' | '*=' | '/=' 
addition_operator := '+' | '-' 
multiplication_operator := '*' | '/' | '%'
...

自1975 年编写lex以来,我们就有实用程序来生成词法分析器。

您基本上建议使用程序代码替换正则表达式。这会将正则表达式中的几个字符扩展为几行代码。用于任何中等程度有趣的语言的词法分析的手写过程代码往往效率低下且难以维护。


4
我不确定我是否建议批发。正则表达式将处理任意(正则)语言。使用特定语言时,没有更好的方法吗?该书涉及预测方法,但在示例中忽略了它们。另外,几年前做过C#的幼稚分析器后,我发现它很难维护。效率低下?当然可以,但不是很可怕,因为我当时的技能。
Telastyn 2014年

1
@Telastyn:要快于表驱动的DFA几乎是不可能的:获取下一个字符,在转换表中查找下一个状态,更改状态。如果新状态为终端,则发出令牌。在C#或Java中,任何涉及创建任何临时字符串的方法都将较慢。
凯文·克莱恩

@kevincline-可以,但是在我的示例中没有临时字符串。即使在C语言中,它也只能是索引或指针,逐步遍历字符串。
Telastyn 2014年

6
@JimmyHoffa:是的,性能在编译器中绝对重要。编译器之所以能够快速运行,是因为它们经过了优化,可以适应各种情况。不是微优化,它们只是不做不必要的工作,例如创建和丢弃不需要的临时对象。以我的经验,大多数商业文本处理代码只完成现代编译器工作的十分之一,而所需时间却是它的十倍。处理千兆字节的文本时,性能非常好。
凯文·克莱恩

1
@Telastyn,您想到了什么“更好的方法”,您希望它以什么方式“更好”?鉴于我们已经拥有经过充分测试的词法分析工具,并且它们产生了非常快的解析器(正如其他人所说的,表驱动的DFA非常快),因此使用它们很有意义。当我们只需要编写词法语法时,为什么要发明一种针对特定语言的新特殊方法?词法语法更易于维护,并且得到的解析器更可能是正确的(考虑到词法和类似工具的测试情况如何)。
2014年

7

特定算法的动机主要是它是一种学习练习,因此它试图与DFA概念保持紧密联系,并在代码中保持状态和转换非常明确。通常,没有人会真正地手动编写任何代码-您将使用工具从语法生成代码。而且该工具不在乎代码的可读性,因为它不是源代码,而是基于语法定义的输出。

对于维护手写DFA的人来说,您的代码更干净,但是与所讲授的概念相去甚远。


7

内部循环:

                currentState = transition[(int)currentState][input[currentIndex]];
                currentIndex++;
                break;

具有很多性能优势。根本没有分支,因为您对每个输入字符都执行完全相同的操作。词法分析器可以控制编译器的性能(词法分析器必须在输入的每个字符的标度上进行操作)。编写《龙书》时更是如此。

在实践中,除了学习词法分析器的CS学生外,没有人必须实现(或调试)该内部循环,因为它是构建transition表的工具附带的样板文件的一部分。


5

从内存上来说-自从我阅读这本书已经很长时间了,而且我很确定我没有阅读最新版,我肯定不记得类似Java的东西-该部分是用该代码旨在作为模板,表中填充了lexer生成器之类的lex。仍然从内存中,有一个关于表压缩的部分(同样,从内存中,它的编写方式也适用于表驱动的解析器,因此在本书中可能比您所看到的还要远)。同样,我记得这本书假定使用8位字符集,我希望在以后的版本中有一节介绍处理更大的字符集,这可能是表压缩的一部分。 我已经给出了另一种方法来解决该问题,以解决SO问题。

在现代体系结构中驱动紧密循环数据具有肯定的性能优势:缓存非常友好(如果您已压缩表),并且跳转预测尽可能地完美(词法结尾处可能会错过,也许一个错过了根据符号分配到代码的开关;这是假定您可以通过可预测的跳转完成表解压缩)。将状态机移至纯代码将降低跳转预测性能,并可能增加缓存压力。


2

之前已经读过《龙书》,使用表驱动的杠杆和解析器的主要原因是,您可以使用正则表达式生成词法分析器,并使用BNF生成解析器。该书还介绍了lex和yacc等工具的工作方式,并按顺序排列,使您知道这些工具的工作方式。此外,重要的是您通过一些实际示例进行工作。

尽管有很多评论,但它与40年代,50年代,60年代所编写的代码风格无关...它与对工具为您做的以及所拥有的内容的实践理解有关。使他们工作。它与从理论和实践的角度对编译器如何工作的基本理解有关。

希望您的讲师还将允许您使用lex和yacc(除非它是研究生级别的课程,并且您可以编写lex和yacc)。


0

晚会:-)令牌与正则表达式匹配。由于它们很多,因此您具有多正则表达式引擎,而后者又是巨型DFA。

“更糟糕的是,如果该语言具有UTF功能,我还看不到它在远程实用。”

这是无关的(或透明的)。除了UTF具有不错的属性外,它的实体甚至不会部分重叠。例如,表示字符“ A”(来自ASCII-7表)的字节不再用于其他任何UTF字符。

因此,对于整个词法分析器,您只有一个DFA(即多正则表达式)。比2D数组更好地写下来吗?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.