Flex:REJECT 一次拒绝一个字符? 注意事项

问题描述

我正在解析 C++ 风格的范围名称,例如 A::B。如果之前声明为类型,我想将这样的名称解析为类型名称的单个标记;否则,我想将其解析为三个标记 A::B

给定:

L             [A-Za-z_]
D             [0-9]
S             [ \f\r\t\v]

identifier    {L}({L}|{D})*
sname         {identifier}({S}*::{S}*{identifier})+

%%

{sname}       {
                if ( find_type( yytext ) )
                  return Y_TYPE_NAME;
                REJECT;
              }

{identifier}  {
                // ...
                return Y_NAME;
              }

然而,对于输入序列:

A::BB -> Not a type; returned as "A","::","BB"
(Declare A::B as a type.)
A::BB

A::BB 的第二次解析中发生了什么,REJECT调用并且 flex 从末尾丢弃 1 个字符并尝试匹配 A::B一个B)。这与之前在错误A::B 规则中声明的 {sname} 匹配。

我假设 REJECT 所做的是使用相同的输入进行次优匹配规则。因此,我希望它在 A 中单独匹配 {identifier} 并且只将 ::BB 留在输入流中。但是,如图所示,情况并非如此。它从输入的末尾一次剥离一个字符并重新尝试匹配。

添加 yyless() 来切断 ::BB 没有帮助:

{sname}       {
                if ( find_type( yytext ) )
                  return Y_TYPE_NAME;
                char const *const sep = strpbrk( yytext,": \t" );
                size_t keep_len = (size_t)(sep - yytext);
                yyless( keep_len );
                REJECT;
              }

我发现唯一有效的是:

{sname}       {
                if ( find_type( yytext ) )
                  return Y_TYPE_NAME;
                char const *const sep = strpbrk( yytext,": \t" );
                size_t keep_len = (size_t)(sep - yytext);
                yyless( keep_len );
                goto identifier;
              }

{identifier}  {
    identifier:
                // ...
                return Y_NAME;
              }

但是这些看起来有点像hack-ish。有没有更规范的“灵活方式”来做我想做的事?

尽管存在黑客问题,但我的解决方案实际上有什么错误吗?即,在某些情况下它会不起作用吗?或者在某些版本的 Flex 中不起作用?


是的,我知道,即使它按照我想要的方式工作,也不会解析所有人为的 C++ 范围名称;但它已经足够好了。

是的,我知道通常解析这些事情应该作为单独的标记来完成,但是类 C 语言很难解析,因为它们不是上下文无关的。知道什么时候有类型真的很有帮助。

是的,我知道 REJECT 会减慢词法分析器的速度。这(主要)用于终端中的交互式和命令行工具,因此人是最慢的组件。

我想专注于手头的代码问题,因为它主要是。我的更多是关于如何使用 REJECTyyless() 等来获得所需行为的问题。谢谢。

解决方法

REJECT 不区分不同的规则;它只是回退到下一个可能的接受模式(它甚至可能不会更短,如果有匹配相同标记的较低优先级规则。)这可能是相同模式的较短匹配。 (通常,Flex 会从正则表达式的可能匹配项中选择最长的匹配项。使用 REJECT,还会考虑较短的匹配项。)

因此您可以通过使用尾随上下文避免 A::B 对输入 A::BB 的错误匹配:[注 1]

{sname}/[^[:alnum:]_]  {...}

(在 Flex 中,您不能在宏中放置尾随上下文,因为扩展被括号包围。)

如果您想尝试所有完整的 id1::id2::id3::id4 前缀,从最长的前缀 (id1::id2::id3::id4) 开始并回退到转 (id1::id2::id3,id1::id2,id1)。尾随上下文可防止标识符中间出现中间匹配。

不过,我不确定这是否是您要解决的问题。即使是这样,它也不能真正解决在您退回到可接受的解决方案后要做什么的问题,因为您可能不想在序列中间重新搜索前缀。换句话说,如果原始是 A::B::A::B,而 A::B 是“已知前缀”,则返回的标记应该(大概)是 A::B,::,{{1 }}、A:: 而不是 BA::B::

另一方面,您可能不关心以前定义的前缀;只有完整序列是否是已知类型。在这种情况下,A::B 的可能标记序列是 [A::B::C::D] 和 [TYPENAME(A::B::C::D),ID(A),ID(B),{ {1}},::]。对于这种情况,您不想限制 ID(C) 回退;你想消除它们。回退后,您只想尝试不同规则的匹配。

有多种替代解决方案(不使用 ID(D)),主要依赖于使用启动条件的可能性。 (您仍然需要考虑回退后会发生什么。启动条件对此也很有用。)

一个相当通用的解决方案是定义一个开始条件,该条件排除您想要回退的规则。一旦确定要回退到不同的规则,就更改为排除刚刚匹配的规则的开始条件,然后调用 {sname}(不返回)。这将导致 Flex 在新的开始条件下从头开始重新扫描当前令牌。 (如果您有多个可能匹配的可能规则,您将需要多个启动条件。​​这可能会失控,但在大多数情况下,可能的匹配规则集非常有限。)

在某些时候,您需要返回到之前的开始条件。 (如果确定哪个是前一个开始条件不是一件容易的事,您可以使用开始条件堆栈。)理想情况下,为了避免错误的内部匹配,您只希望在到达第一个结束时返回到前一个开始条件(错误)匹配。如果您要跟踪每个令牌的输入位置,这很容易做到;您只需要在调用 REJECT 之前保存位置。 (您还需要通过在 yyless(0)yyless(0) 设置为 0 之前减去 yyleng 来更正令牌输入位置)。

从令牌的开头重新扫描可能看起来效率低下,但它的效率低于 yyless 所施加的开销。并且 yyleng 的开销会影响整个扫描器操作,而重新扫描解决方案基本上是免费的,除了您碰巧重新扫描的令牌。 [注2]

(REJECT) 是一个相当常见的弹性习语;这不是唯一的用例。另一个问题是“我如何运行启动条件,直到到达一个不消耗该令牌就无法识别的令牌?”)

但我认为在这种情况下有一个更简单的解决方案,使用 REJECT 来累积令牌。 (这避免了必须自己动态扩展令牌缓冲区。)同样,有两种可能性,取决于您是允许初始前缀匹配还是将可能性限制为完整序列或第一个标识符。

但大纲是一样的:首先匹配最短的有效可能性,记住当时令牌有多长,然后使用开始条件启用扩展令牌的模式,或者到下一个可能性或到序列的末尾,取决于您的需要。在继续扫描器循环之前,您调用 BEGIN(some_state); yyless(0);,它向 Flex 指示下一个模式扩展令牌而不是替换它。

在每个可能的匹配结束时,您测试它是否真的是一个有效的标记,如果是,记录位置(以及您可能需要回忆的任何其他内容,例如标记类型)。当到达无法再扩展匹配的点时,您可以使用 yymore 回退到最后一个有效匹配点,并返回令牌。

这比纯 yymore() 解决方案效率稍低,因为它避免了重新扫描返回的令牌。 (所有这些解决方案,包括 yyless,如果所选匹配项比可能的最长扩展匹配项短,则重新扫描文本。这在理论上是可以避免的,但由于它的开销并不大,因此似乎不值得建立一个复杂的机制来避免它。) 同样,您可能希望避免在回退后尝试扩展令牌匹配,直到达到最长范围。这可以通过记录最长匹配范围的相同方式解决,但起始条件处理要简单一些。

以下是一些针对较简单问题的未经充分测试的代码,其中只有第一个标识符和完整匹配是可能的:

yyless()

注意事项

  1. 至少,我认为你可以做到。我从未尝试过,REJECT 确实对其他扫描仪功能施加了许多限制。确实,这是一个历史文物,它大大减慢了词法分析的速度,通常应该避免。

  2. 在规则中的任何地方使用 %{ /* Code to track current token position and the fallback position. * It's a simple byte count; line-ends are not dealt with. */ static int token_pos = 0; static int token_max_pos = 0; /* This is done before every action,even empty actions */ #define YY_USER_ACTION token_pos += yyleng; /* FALLBACK needs to undo YY_USER_ACTION */ #define FALLBACK(to) do { token_pos -= yyleng - to; yyless(to); } while (0) /* SET_MORE needs to pre-undo the next YY_USER_ACTION */ #define SET_MORE(X) do { token_pos -= yyleng; yymore(); } while(0) %} %x EXTEND_ID ident [[:alpha:]_][[:alnum:]_]* %% int fallback_leng = 0; /* The trailing context here is to avoid triggering EOF in * the EXTEND_ID state. */ {ident}/[^[:alnum:]_] { /* In the fallback region,don't attempt to extend the match. */ if (token_pos <= token_max_pos) return Y_IDENT; fallback_leng = yyleng; BEGIN(EXTEND_ID); SET_MORE(yymore); } {ident} { return find_type(yytext) ? Y_TYPE_NAME : Y_IDENT; } <EXTEND_ID>{ ([[:space:]]*"::"[[:space:]]*{ident})*|.|\n { BEGIN(INITIAL); if (yyleng == fallback_leng + 1) FALLBACK(fallback_leng); if (find_type(yytext)) return Y_TYPE_NAME; else { FALLBACK(fallback_leng); return Y_IDENT; } } } 会导致 Flex 切换到不同的模板,其中保留了端点列表,这在时间和空间上都有成本。另一个后果是 Flex 无法再调整输入缓冲区的大小。