Compiler construction 创建lexer最有效的方法是什么?

Compiler construction 创建lexer最有效的方法是什么?,compiler-construction,lexer,finite-automata,Compiler Construction,Lexer,Finite Automata,我目前正试图学习如何手工创建自己的词汇分析器。我一直在使用Flex(和Bison一起)进行大量的实践,学习它如何在内部工作,但我目前看到至少有3种不同的解决方案可以开发自己的解决方案 使用一个REs列表,检查每个REs,当一个REs匹配时,只需返回关联的令牌(请参阅python文档中关于REs的内容) 从REs创建DFA(比如Flex:基于REs,创建一个大型状态机) 创建我自己的“状态机”,其中包含大量的switch case或if语句(我认为Lua就是这样做的) 我相信我可以尝试每种解决方案

我目前正试图学习如何手工创建自己的词汇分析器。我一直在使用Flex(和Bison一起)进行大量的实践,学习它如何在内部工作,但我目前看到至少有3种不同的解决方案可以开发自己的解决方案

  • 使用一个REs列表,检查每个REs,当一个REs匹配时,只需返回关联的令牌(请参阅python文档中关于REs的内容)
  • 从REs创建DFA(比如Flex:基于REs,创建一个大型状态机)
  • 创建我自己的“状态机”,其中包含大量的switch case或if语句(我认为Lua就是这样做的)
  • 我相信我可以尝试每种解决方案,但是:

    • 是否存在一种解决方案无法解决的情况
    • 在什么情况下,您会使用一种解决方案而不是另一种
    • 正如标题所说:哪一种代码产生的效率最高

    提前谢谢

    第二个和第三个选项是等效的,如果并且仅当您能够在没有任何bug的情况下编写状态机和flex词法描述。但我的经验是,编写(和阅读)flex词汇描述要容易得多

    第一个备选方案可能并不等效,在一般情况下,使其等效也不是一件小事

    问题是如果有多个模式与正则表达式匹配,会发生什么情况。(在编写上述大量switch语句时,这个问题也会导致微妙的错误。)在这种情况下,普遍接受的词汇策略是使用规则:选择导致最长匹配的模式,如果有多个这样的模式,则选择在词汇定义中首先出现的模式

    作为这个规则为什么很重要的一个简单例子,考虑一个具有关键字“代码> do> /CODE”和“代码>双< /代码>的语言。注意,理想的行为是:

    do {         => First token is keyword do
    double d;    => First token is keyword double
    doubt = 0.9; => First token is identifier doubt
    
    在标准(f)lex文件中,这将实现为:

    "do"       {  return T_FOR; }
    "double"   {  return T_FOREACH; }
    [[:alpha:]_][[:alnum:]_]*  { yyval.str = strdup(yytext); return T_ID; }
    
    (F) 如果前两条规则的顺序不同,lex将生成完全相同的扫描器,尽管第三条规则肯定必须在最后。但是,能够对前两条规则进行重新排序就不那么容易出错。当然,有些人会按字母顺序编写词汇规则,如上所述,但其他人可能会选择按语法功能组织关键字,以便将
    do
    for
    while
    done
    等和
    double
    int
    char
    合并,对于后一种组织,程序员将很难确保重叠的关键字以任何特定的顺序出现,因此flex不在乎是有用的;在这种情况下(和许多其他情况一样),选择最长的匹配肯定是正确的

    如果创建正则表达式列表并只选择第一个匹配项,则需要确保正则表达式按匹配长度的相反顺序排列,以便匹配最长关键字的正则表达式排在第一位。(这会将
    double
    置于
    do
    之前,因此按字母顺序排列的关键字将失败。)

    更糟糕的是,哪一个正则表达式的匹配时间最长,可能无法立即确定。很明显,对于关键字--您可以按长度对文字模式进行反向排序--但在一般情况下,最大munch规则可能不是正则表达式的偏序:可能是这样的情况,对于某些标记,一个正则表达式具有最长的匹配,而另一个正则表达式为不同的标记提供更长的匹配

    或者,您可以尝试所有正则表达式,并跟踪匹配时间最长的正则表达式。这将正确地实现maximalmunch(如下所示),但效率更低,因为每个模式都必须与每个标记匹配

    您链接到的Python文档中使用的实际代码实际上是通过在各个正则表达式之间插入
    |
    运算符,从提供的模式创建一个正则表达式。(这使得无法使用带编号的捕获,但这可能不是问题。)

    如果Python正则表达式具有Posix最长匹配语义,这将等同于Maximum munch,但事实并非如此:Python替换将首选第一个匹配,除非后续匹配需要继续正则表达式:

    >>> pat = re.compile("(wee|week)(night|knight)")
    >>> pat.match("weeknight").group(1)
    'wee'
    >>> pat.match("weekknight").group(1)
    'week'
    
    要做到这一点,您必须小心谨慎,以确保正则表达式的顺序正确,并且不会干扰彼此的匹配。(并非所有正则表达式库的工作方式都与Python相同,但许多正则表达式库都是如此。您需要查看文档,或许还需要做一些实验。)

    简言之,对于一种语言来说,如果你准备在其中投入一些工作,你将能够手工构建工作“正确”的词汇量(假设该语言坚持最大限度的咀嚼,就像大多数标准化语言所做的那样),但这肯定是一件苦差事。不仅仅是对您而言:对于任何想要理解或验证您的代码的人来说,这都是额外的工作

    所以就编写代码(包括调试)的效率而言,我认为像(f)lex这样的词法生成器是一个明显的赢家

    长期以来,手工构建(或开放编码)的词法生成器速度更快。如果你想尝试一下,你可以尝试使用
    re2c
    ,它可以产生高度优化的开放编码词汇扫描器。(通过开放编码,我的意思是他们不使用转换表。)对于给定的词法规则集,这一理论可能是正确的,也可能不是正确的,因为基于表的词法器(由(f)lex生成)通常在代码大小上要小得多,因此可以更有效地使用处理器缓存。如果选择flex的快速(但更大)表选项,则扫描仪的内部循环非常短,并且只包含一个条件分支。(但分支预测在