【问题标题】:Yapp / yacc / bison - syntaxYapp / yacc / bison - 语法
【发布时间】:2021-11-17 12:05:26
【问题描述】:

我正在尝试使用 Perl Yapp 编写解析器,据我了解,它与 yacc/bison 基本相同。 给我带来麻烦的输入如下所示:

interface someName
  description "bla"
  ip address 1.2.3.4 255.255.255.128
ip default-gateway 1.2.3.123

我的语法是这样的:

command:
   INTERFACE IDENTIFIER if_attrs
 | IP DEFAULT_GATEWAY IP_ADDRESS
;

if_attrs: # empty
 | if_attrs if_attr
;

if_attr:
  DESCRIPTION STRING
| IP ADDRESS IP_ADDRESS IP_ADDRESS
;

当然,输入和整个语法都要复杂得多,但这些都是必不可少的部分。

生成令牌有效(到目前为止我不关心缩进),但运行它会抱怨“DEFAULT_GATEWAY”。 在阅读“IP”后,Yapp 还报告了该州的 S/R 冲突,这对我来说有点道理,但是我找不到解决方案。 我已经阅读了很多关于优先技巧的文章并尝试了一些方法但没有成功。

非常感谢任何提示!

【问题讨论】:

    标签: bison yacc


    【解决方案1】:

    这里的基本问题是您的语法需要两个前瞻标记,这使其超出了 LALR(1) 解析器的范围。 (“1”表示前瞻仅限于一个标记。)

    这并不构成语言 LALR(2)。没有 LALR(2) 语言这样的东西,因为每种具有 LALR(2) 语法的语言也有 LALR(1) 语法。此外,还有一种算法可以将 LALR(2) 语法转换为 LALR(1) 语法,甚至可以恢复由 LALR(2) 语法生成的解析树,如果你有实现的话.

    不幸的是,这种转换生成的语法非常庞大且难以阅读,并且有很多具有相同语义动作的产生式。此外,转换的实现很少,因为更有效的策略是使用更通用的解析算法。可以要求 Bison 本身生成 GLR 解析器,在 Perl 中有 Jeffrey Kegler's Marpa parser 等。但我会假设您不想更改解析器生成器。

    LALR(2)⇒LALR(1) 转换的基本性质是将第一个前瞻标记存储为语义值的一部分。这有效地将解析器一个令牌转移到未来。如果您手动执行此操作,则仅需要对实际上需要两个前瞻标记的非终端进行转换,在您的情况下,这基本上是表示命令列表的顶级非终端。

    现在,我也不打算把它写出来。相反,我将提供两个更简单且可能更易于维护的 hack。

    在词法分析器中消除 ip 的歧义

    这是大多数类似 yacc 的处理器处理语法描述中可选的 ; 的方式。 (我不知道分号在 yapp 中是否是可选的,但它肯定在 yacc 和 bison 中。)在 BNF 语法中,如果标识符标记后跟 :,则它无疑是左侧,但是一个天真的实现是 LR(2),就像你的语法一样;在看到 : 之前,无法关闭之前的定义。为了解决这个问题,词法分析器将后跟 : 的标识符识别为不同的令牌类型。同样,您的词法分析器可以将两个标记序列ip default-gateway 识别为单个标记。这假设语法中没有其他地方可以在ip 后面紧跟default-gateway,或者如果是,则可以将其解析为一个单一的世界。

    这会使词法分析器有点复杂(特别是如果您允许在两个关键字之间使用 cmets),但如果您只有几个必须以这种方式识别的短语,那可能还不错。

    即时构建接口命令

    这里,顶层非终结符分为两种可能:

    • 关闭,表示最后一条命令不能扩展,
    • open,表示最后一个命令是接口命令,并应附加以下if_attr 子句。 open 非终端的语义值包括可能不完整的接口命令作为其语义值的一部分。

    这也是基于解析 BNF 的策略,其中单个标识符仅保留在产生式列表的语义值中,直到明确是否将其添加到最后一个的右侧生产或它是新生产的左侧。

    我已经很多年没有用 Perl 编程了,所以我把语义动作写成了伪代码。对不起。

    # A list of commands
    closed_program:
       %empty
     | closed_program unambiguous_command {
           push($_[1], $_[2]); # Add the command to the end of the list
       }
     | open_program unambigous_command {
           push($_[1]{"list"}, $_[1]{"saved"});
                               # The saved command is now complete
           push($_[1]{"list"}, $_[2]);
                               # Also add the new complete command
       }
    
    # Semantic value has two attributes:
    # * "list" is a list of commands
    # * "saved" is an interface command 
    open_program:
       ​closed_program interface_identifier {
        ​# return {list => $_[1], saved => $_[2]}
      ​ }
     ​| open_program if_attr {
        ​# add $_[2] to the "saved" entry of $_[1]
      ​ }
    
    unambiguous_command:
       ​default_gateway_command
     | ... # other commands which cannot be clauses
    ;
    
    default_gateway_command:
       ​IP DEFAULT_GATEWAY IP_ADDRESS
    ;
    
    # Action needs to create a new interface command
    # with an empty list of attributes
    interface_identifier:
      ​INTERFACE IDENTIFIER
    ;
    
    # Action creates an attribute (however that is represented)
    # which will be added to the attribute list
    if_attr:
       ​DESCRIPTION STRING
     | IP ADDRESS IP_ADDRESS IP_ADDRESS
    ;
    

    【讨论】:

    • 不幸的是不起作用。我将输入和语法都剥离为这个加上一个递归规则(有多个输入行): line: # empty |行命令;仍然失败。
    • 你说得对,我在想一个更简单的问题。对不起。我将用更复杂的解决方案替换它。 :-)
    • @tge12345 我希望这会有所帮助。再次对混乱感到抱歉。
    • 首先非常感谢!我可能会采用基于词法分析器的方式——更简单。另一个选项(诚然,我不完全理解它 - 尽管这里已经很晚了)有效,但在输入中有两个界面部分时不再有效。除非我搞砸了。挑战? :-)
    • @tge12345:是的,这是一种简化,取决于冲突的特殊性质。所以更多的选择可能意味着更多的工作。 lexer hack 确实更简单,这就是它更常见的原因;如果您需要做的就是识别多字关键词,它几乎总是首选选项。我喜欢其他解决方案的主要原因是它扩展到更大的前瞻。例如,野牛让你写expr[result]: expr[numer] '/' expr[denom] { $result = $numer / $denom; };使用词法分析器技巧来做到这一点很痛苦(尽管野牛就是这样做的)。
    【解决方案2】:

    另一种可能比 rici 的方法更简单的替代方法是独立解析这些行,然后尝试将它们组合在一起。你可能有类似的东西:

    input: /*empty*/ | input line ;
    
    line:
        INTERFACE IDENTIFIER '\n' {
            lines.push_back(Interface(...      }
      | IP DEFAULT_GATEWAY IP_ADDRESS '\n' {
            lines.push_back(DefaultGateway(...     }
      | DESCRIPTION STRING '\n' {
            if (lines.back().isInterface()) {
                lines.back().addDescription(...
            } else {
                error("description not associated with interface"); } }
      | IP ADDRESS IP_ADDRESS IP_ADDRESS '\n' {
            if (lines.back().isInterface()) {
                lines.back().addAddress(...
            } else {
                error("address not associated with interface"); } }
    ;
    

    请原谅没有特定语言的伪代码操作,但希望它们能说明这里需要什么。这特别适用于强烈的面向行的语法——因此这里显式的 '\n' 标记,在这种情况下实际上不是必需的,但在其他情况下可能有用

    【讨论】:

    • 有趣的观点-谢谢!就我而言,尚不清楚,缩进可能表示某种逻辑块(尚未确认)-如果不是,它将是面向行的。我还尝试了一个 INDENT 标记,但最后我选择了基于词法分析器的方式,因为它最简单且安全(无论是否缩进)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-27
    相关资源
    最近更新 更多