换行敏感语言中 if 语句的语法 上一个答案针对原始问题,仅在 edit history 中可见

问题描述

我正在研究一种读起来很像英语的语言,但在 if 语句的语法方面有问题。如果您好奇,该语言的灵感来自 HyperTalk,所以我试图确保我匹配该语言中的所有有效结构。我正在使用的示例输入演示了所有可能的 if 构造,可以查看 here。有很多,所以我不想内联代码

我已经从语法中删除了大多数其他结构以使其更易于阅读,但基本上语句如下所示:

start
    : statementList
;

statementList
    : '\n'
    | statement '\n'
    | statementList '\n'
    | statementList statement '\n'
;

statement
    : ID
    | ifStatement
;

我看到的 shift/reduce 冲突在 ifStatement 规则中:

ifStatement
    : ifCondition THEN statement
    | ifCondition THEN statement ELSE statement
    | ifCondition THEN statement ELSE '\n' statementList END IF
    | ifCondition THEN '\n' statementList END IF
    | ifCondition THEN '\n' END IF
    | ifCondition THEN '\n' ELSE statement
    | ifCondition THEN '\n' ELSE '\n' statementList END IF
    | ifCondition THEN '\n' statementList ELSE statement
    | ifCondition THEN '\n' statementList ELSE '\n' statementList END IF
// The following rules cause issues,but should be legal:
    | ifCondition THEN statement newlines ELSE statement
    | ifCondition THEN statement newlines ELSE '\n' statementList END IF
;

ifCondition
    : IF expression
    | IF expression '\n'
;

expression
    : TRUE
    | FALSE
;

newlines
    : '\n'
    | newlines '\n'
;

问题是我需要支持这个结构:

if true then statement # <- Any number of newlines
else statement

问题(据我所知)是没有足够的上下文来正确确定是移动 else 还是只减少 if true then statement 部分而不知道后面会发生什么(结束语句列表,或另一个语句)。这甚至可以解析吗?

我有 parserscannersample input 的要点可以尝试。

解决方法

要做到这一点出奇的困难,所以我试图对这些步骤进行注释。有很多烦人的细节。

从本质上讲,这只是悬空 else 歧义的一种表现,其解决方案是众所周知的(强制解析器始终移动 else)。下面的解决方案解决了语法本身的歧义,这是无歧义的。

我在这里使用的基本原则是几十年前由 Alfred Aho 和 Jeffrey Ullman 在 Principles of Compiler Design 中概述的原则(所谓的“龙之书”,我提到它是因为它的作者最近granted the Turing award {3}} 正是为了那个和他们其他有影响力的作品)。特别是,我使用术语“匹配”和“不匹配”(而不是“开放”和“封闭”,这也很流行),因为这是我学习的方式。

也可以使用优先级声明来解决这个语法问题;事实上,这往往要简单得多。但在这种特殊情况下,使用运算符优先级并不容易,因为相关标记(else)前面可以有任意数量的换行标记。我很确定你仍然可以构建一个基于优先级的解决方案,但是使用明确的语法有很多优点,包括易于移植到不使用相同优先级算法的解析器生成器,以及它是可以进行机械分析。

解决方案的基本轮廓是将所有语句分为两类:

  • “匹配的”(或“关闭的”)语句,在无法用 else 子句扩展语句的意义上,它们是完整的。 (换句话说,每个 if…then 都与相应的 else 匹配。)这些
  • “unmatched”(或“open”)语句,可以用 else 子句进行扩展。 (换句话说,至少有一个 if…then 子句与 else 不匹配。)由于未匹配的语句是一个完整的语句,因此它不能紧跟一个 else 标记;如果出现了 else 标记,它将有助于扩展语句。

一旦我们设法为这两类语句构造了语法,只需要弄清楚statement在歧义语法中的哪些用法可以跟在else后面。在所有这些上下文中,非终结符 statement 必须替换为非终结符 matched-statement,因为只有匹配的语句后才能跟 else 而不与之交互。在 else 不能是下一个标记的其他上下文中,任一类别的语句都是有效的。

所以基本的语法风格是(取自龙书):

           stmt → matched_stmt
                | unmatched_stmt
   matched_stmt → "if" expr "then" matched_stmt "else" matched_stmt
                | other_stmt
unmatched_stmt  → "if" expr "then" matched_stmt "else" unmatched_stmt
                | "if" expr "then" stmt

other_stmt 不是条件语句。或者,更准确地说,不是以 stmt 结尾的复合语句。

在 Hypertalk 中,据我所知,if 语句是唯一可以以语句结尾的复合语句。其他复合语句以 end X 精确终止,这有效地关闭了语句。但是在其他语言中,例如 C 语言中,存在多种复合语句,其中大部分需要根据其终止子语句是(递归地)匹配还是不匹配来分为“匹配”和“不匹配”。>

在这里我要注意的一件事是,如果您稍微侧视一下,从大纲语法中可以明显看出,if…then…else 语句的 if 部分在语法上类似于括号前缀运算符。也就是说,matched_stmtunmatched_stmt 都类似于一元减法的右递归规则:

unary → '-' unary
      | atom

反过来又可以用扩展的 BNF 方言编写,允许 Kleene 星为

unary → ('-')* atom

如果我们要对 Aho&Ullman 的语法进行这种转换,我们会得到:

  if_then_else → "if" expr "then" matched_stmt "else"
  matched_stmt → (if_then_else)* other_stmt
unmatched_stmt → (if_then_else)* "if" expr "then" stmt

这使得如何使用自上而下的递归下降解析器实现这种语法变得相当清楚。 (需要一点左因子分解,但它最终仍然类似于一元减语法。)我不打算在这个答案中进一步发展这个想法,但我认为 EBNF 转换有助于指导关于这种语法实际上是如何解决 else 问题的。

这对弄清楚如何处理换行也很有帮助。关键见解(对我而言)是语句必须以换行符结尾。一个例外是 if 命令的精简单行版本。但该异常仅发生在 else 标记之前(并且仅当它在同一行中匹配 then 时)。在这个文法中,这种情况是用 inner-matched 非终结符实现的,并辅以单行语句(如 do-statement)缺少终止换行符这一事实。在 matched (single-statement NL) 的递归基本情况中添加终止单行语句的换行符;这是唯一需要处理的地方。多行复合语句均使用终止换行符定义(例如,请参见 repeat-statement)。

其余的大多数并发症都涉及各种句法形式。唯一真正有趣的是在行尾处理 then 标记之后的块。该块可以通过两种方式终止:

  • 带有 end if 行,没有 else 子句。这被视为“匹配”案例,因为它显然不能用 else 子句进行扩展。
  • 带有 else 子句(可以是单行 else 或块 else,其中 else 标记位于行尾)。但这里可能存在歧义;如果块中的最后一条语句是不匹配的 if,那么 else 行应该扩展该语句,而不是终止该块。这与其他匹配/不匹配逻辑并没有什么不同;为了实现它,我创建了两个不同的 block 非终结符,一个以匹配的语句结尾,另一个以不匹配的语句结尾。然后,像往常一样,只能在 else 之前使用匹配的块。

(我发现 bison 3.7.6 中的新反例生成器在这里非常有用;我最初的尝试只是使用 block,因为我没有注意到歧义。但这是一个真正的歧义,它导致一个shift-reduce冲突,其起源似乎很神秘。一旦我看到反例生成器产生的反例——它显示了在block之后的if-then内发生的冲突——问题变得更加明显.)

matched-blockunmatched-block 之间的交替是语法产生式和状态机之间对应关系的一个简单示例。两个非终结符代表一个非常简单的状态机中的两个状态,其状态记录一个位:最后一条语句是否匹配。非终结符必须是右递归才能使其工作,这与构建 LALR(1) 语法的通常“首选左递归”启发式有所不同。

好的,前言过长,这里是语法。为了紧凑化,我将表达式简化为变量和布尔常量,只包含一个简单的语句(do expr),只包含一个其他的复合语句(do expr) >repeat until expr / block / end repeat)。 (最后一个用作占位符。)

program : block

block   : %empty
        | matched-block
        | unmatched-block

NL      : '\n'
        | NL '\n'

matched-block
        : block matched

unmatched-block
        : block unmatched

simple-statement
        : "do" expression

repeat-statement
        : "repeat" "until" expression NL block "end" "repeat" NL

matched : if-then matched else-matched
        | if-then inner-matched else-matched
        | if-then NL matched-block else-matched
        | if-then NL else-matched
        | if-then NL block "end" "if" NL
        | repeat-statement
        | simple-statement NL

inner-matched
        : %empty
        | simple-statement
        | if-then inner-matched "else" inner-matched

unmatched
        : if-then matched
        | if-then unmatched
        | if-then inner-matched "else" unmatched
        | if-then matched "else" unmatched

if-then : "if" expression NL "then"
        | "if" expression "then"

else-matched
        : "else" NL block "end" "if" NL
        | "else" matched

expression
        : ID
        | "true"
        | "false"

上一个答案(针对原始问题,仅在 edit history 中可见)

两者之间存在明显的歧义

ifCondition THEN statement EOL ELSE statement

ifCondition THEN EOL statementList ELSE statement

回忆一下

statement: %empty
statementList: statement

结果 statementstatementList 都可以导出空序列。因此,ifStatement 的上述两个产生式都可以得出:

ifCondition THEN EOL ELSE statement

解析器无法知道 statement 之前是空的 EOL 还是之后是空的 statementList。 (您可能不关心选择了哪一个,但解析器会关注这种决定。)

可空的产生式通常是有问题的。在可能的情况下,避免它们。不要让 statement 派生为空,而是通过添加省略可选语句的规则来明确指示空语句的位置。并考虑重写 statementList,使其必须以 EOL 结尾,我认为无论如何这是您的意图(但也许我错了)。