【问题标题】:Does a priority declaration disambiguate between alternative lexicals?优先级声明是否可以消除替代词汇之间的歧义?
【发布时间】:2018-07-10 20:35:14
【问题描述】:

在我的previous question 中,示例中有一个优先级> 声明。事实证明这并不重要,因为那里的解决方案实际上并没有调用优先级,而是通过使替代方案脱节来避免它。在这个问题中,我在问是否可以使用优先级来选择一个词汇产生而不是另一个。在下面的示例中,产生式WordInitialDigit 的语言故意是WordAny 的语言的子集。产生式Word 看起来应该正确地消除两者之间的歧义,但生成的解析树在顶部有一个歧义节点。优先声明是否能够在不同的词汇缩减之间做出决定,还是需要有共同词汇元素的基础?还是别的什么?

这个例子是人为的(语法中没有动作),但它产生的情况不是。例如,我想使用这样的东西来进行错误恢复,我可以识别语法单元的自然边界并为其编写产生式。这种通用生产将是优先链中的最后一个元素;如果它减少,则意味着没有有效的解析。更一般地说,我需要能够根据句法上下文选择词汇元素。我曾希望,因为 Rascal 没有扫描仪,所以这将是无缝的。也许是吧,虽然我现在没看到。

我在不稳定的分支上,版本 0.10.0.201807050853。

编辑:这个问题不是关于> 用于定义表达式语法。 documentation for priority declarations 主要谈论表达式,但第一句话提供了一个看起来非常清晰的定义:

优先级声明定义了在单个非终结符中的产生之间的部分排序

所以这个例子有两个产生式,它们之间声明了一个排序,但是解析器仍然在明确存在消歧规则的情况下生成一个歧义节点。因此,为了更好地说明我的问题,看起来我不知道两种情况中的哪一种有关。 (1)如果这不应该工作,那么文档中的语言定义存在缺陷,编译器错误报告的缺陷,以及介于违反直觉和用户敌意之间的语言设计决策。或者 (2) 如果这应该可以工作,那么编译器和/或解析器中存在缺陷(可能是因为最初关注的是表达式),并且在某些时候该示例将通过其测试。

module ssce

import analysis::grammars::Ambiguity;
import ParseTree;
import IO;
import String;

lexical WordChar = [0-9A-Za-z] ;
lexical Digit = [0-9] ;
lexical WordInitialDigit = Digit WordChar* !>> WordChar;
lexical WordAny = WordChar+ !>> WordChar;
syntax Word =
    WordInitialDigit
    > WordAny
    ;

test bool WordInitialDigit_0() = parseAccept( #Word, "4foo" );
test bool WordInitialDigit_1() = parseAccept( #WordInitialDigit, "4foo" );
test bool WordInitialDigit_2() = parseAccept( #WordAny, "4foo" );

bool verbose = false;

bool parseAccept( type[&T<:Tree] begin, str input )
{
    try
    {
        parse(begin, input, allowAmbiguity=false);
    }
    catch ParseError(loc _):
    {
        return false;
    }
    catch Ambiguity(loc l, str a, str b):
    {
        if (verbose)
        {
            println("[Ambiguity] #<a>, \"<b>\"");
            Tree tt = parse(begin, input, allowAmbiguity=true) ;
            iprintln(tt);
            list[Message] m = diagnose(tt) ;
            println( ToString(m) );
        }
        fail;
    }
    return true;
}

bool parseReject( type[&T<:Tree] begin, str input )
{
    try
    {
        parse(begin, input, allowAmbiguity=false);
    }
    catch ParseError(loc _):
    {
        return true;
    }
    return false;
}

str ToString( list[Message] msgs ) =
    ( ToString( msgs[0] ) | it + "\n" + ToString(m) | m <- msgs[1..]  );

str ToString( Message msg)
{
    switch(msg)
    {
        case error(str s, loc _): return "error: " + s;
        case warning(str s, loc _): return "warning: " + s;
        case info(str s, loc _): return "info: " + s;
    }
    return "";
}

【问题讨论】:

  • 我可以从你的描述中看出你是如何被引导认为相同非终结符的备选方案之间的规则选择直接受优先级排序的影响。但事实并非如此。从优先级声明生成的部分顺序定义了排除某种嵌套的约束,即加法规则不会在乘法规则下扩展。我们应该更改文档以避免这种解释。

标签: rascal


【解决方案1】:

很好的问题。

TL;DR:

  • 规则优先级机制无法对非终端的备选方案进行算法排序。尽管优先级声明生成的附加语法约束中涉及某种偏序,但没有先“尝试”一个规则,然后再“尝试”另一个规则。所以它根本无法做到这一点。好消息是,优先级机制具有独立于任何解析算法的形式语义,它只是根据上下文无关语法规则和归约轨迹定义的。
  • 使用模糊规则进行错误恢复或“稳健解析”是个好主意。但是,如果这样的规则太多,解析器最终会开始表现出二次甚至三次行为,并且解析后的树构建甚至可能具有更高的多项式。我相信生成的解析器算法应该有一个(参数化的)错误恢复模式,而不是在语法层面表达。
  • 在解析时接受歧义,并在解析后过滤/选择树是推荐的方法。
  • 文档中所有关于“排序”的讨论都是误导性的。消歧是令人困惑的术语的雷区。现在,我推荐这篇 SLE 论文,它有一些定义:https://homepages.cwi.nl/~jurgenv/papers/SLE2013-1.pdf

详情

优先机制无法在备选方案中进行选择

&gt; 运算符和leftright 的使用会在相互递归的规则之间产生偏序,例如在表达式语言中,并限制在每个规则中的特定项目位置:即最左边和重叠的最右边的递归位置。不允许层次结构中较低的规则在语法上扩展为层次结构中较高的规则的“子”。所以在E "*" E 中,如果E "*" E &gt; E "+" E,则E 都不能扩展为E "+" E

附加约束不会为任何E 选择先尝试哪个替代方案。不,他们只是不允许某些扩展,假设 other 扩展仍然有效并且因此解决了歧义。

在特定位置进行限制的原因是,对于这些位置,解析器生成器可以“证明”它们会产生歧义,因此通过禁止某些嵌套来过滤两个备选方案之一不会导致额外的解析错误。 (考虑一个数组索引规则:E "[" E "]",它不应该对第二个E 有额外的约束。这是一种所谓的“语法安全”消歧机制。

总而言之,它在算法上是一个相当薄弱的机制,专门为相互递归的组合器/表达式类语言量身定制。该机制的最终目标是确保我们使用的整个表达式语言只需要使用 1 个非终结符,并且解析树在形状上看起来非常类似于抽象语法树。顺便说一句,Rascal 通过 SDF2 从 SDF 继承了所有这些考虑因素。

当前的实现实际上以某种不可见的方式“分解”语法或解析表以获得相同的效果,就好像有人会完全分解语法一样;然而,这些底层实现非常特定于所讨论的解析算法。 GLR 版本与 GLL 版本完全不同,而 GLL 版本又与 DataDependent 版本完全不同。

解析后过滤

当然,任何树,包括解析器生成的模糊解析林,都可以被 Rascal 程序使用模式匹配、访问等操作。你可以编写任何算法来删除你想要的树。但是,这需要首先构建整个森林。这是可能的,而且通常足够快,但还有更快的替代方案。

由于树是在解析后从解析图以自底向上的方式构建的,因此我们还可以在树的构建过程中应用“重写规则”,并删除某些替代项。

例如:

Tree amb({Tree a, *Tree others}) = amb(others) when weDoNotWant(a);
Tree amb({Tree a}) = a;  

第一条规则将匹配所有树的歧义簇,并删除weDoNotWant 的所有替代项。如果只剩下一个备选方案,则第二条规则将删除集群,让最后一棵树“获胜”。

如果您想在备选方案中进行选择:

Tree amb({Tree a, Tree b, *Tree others}) = amb({a, others} when weFindPeferable(a, b);

如果您不想使用 Tree 而是更具体的非终端,例如 Statement,它也应该可以工作。

此示例模块在语法定义中使用@prefer 标记来“优先”那些已标记为优于其他规则的规则,作为解析后重写规则:

https://github.com/usethesource/rascal/blob/master/src/org/rascalmpl/library/lang/sdf2/filters/PreferAvoid.rsc

使用额外的词法约束来解决问题

除了优先消歧和解析后重写之外,我们在工具包中还有词汇级别的消歧机制:

  • `NT \ 关键字" - 拒绝来自非终端的有限(关键字)语言
  • CC &lt;&lt; NTNT &gt;&gt; CCCC !&lt;&lt; NTNT !&gt;&gt; CC 遵循和遵循限制(其中 CC 代表字符类,NT 代表非终端)

除了运算符优先级之外,还可以尝试解决其他类型的歧义,特别是如果不同子句的长度在不同备选方案之间更短/更长,!&gt;&gt; 可以做“最大咀嚼”或“最长匹配”的事情。所以我在大声思考:

lexical C = A? B?;

其中A 是一种词汇选择,B 是另一种。通过对A!&lt;&lt;B 的适当!&gt;&gt; 限制和!&lt;&lt;B 的限制,语法可能会被欺骗,总是希望将所有字符放在A 中,除非它们不适合A 作为一种语言,在在这种情况下,他们将默认为B

明显/烦人的建议

多想一个明确且简单的语法。

有时这意味着在语法中抽象并允许更多句子,避免使用语法对树进行“类型检查”。通常最好过度近似语言的语法,然后使用(静态)语义分析(通过更简单的树)来获得你想要的东西,而不是盯着复杂的模棱两可的语法。

一个典型的例子:仅在开头声明的 C 块比在任何地方都允许声明的 C 块更难明确定义。对于C90 模式,您所要做的就是标记不在块开头的声明。

这个特殊的例子

lexical WordChar = [0-9A-Za-z] ;
lexical Digit = [0-9] ;
lexical WordInitialDigit = Digit WordChar* !>> WordChar;
lexical WordAny = WordChar+ !>> WordChar;
syntax Word =
    WordInitialDigit
    | [0-9] !<< WordAny // this would help!
    ;

结束

很好的问题,感谢您的耐心等待。希望这会有所帮助!

【讨论】:

  • 作为顶级建议,我建议将“优先级”重命名为“优先级”。 “优先级”这个词已经在数学中被标准使用,通常在“运算符优先级”这个短语中。至少对我来说,使用“优先级”这个词是在声称,比运算符优先级更普遍的东西在幕后起作用。
  • 现在提供更多技术建议。由于您内部有一个证明者,它可以确定 &gt; 规则何时消除对某些表达式的解析的歧义,如果 &gt; 规则 not so apply 会生成,那将是最有用的编译器警告。在上面的示例中,您已经掌握了执行此类警告的测试用例的核心。
  • 我得到了我想要的效果,该模式使用具体模式识别首选替代方案并在没有歧义节点的情况下重写它:Word amb({w:(Word)`&lt;WordInitialDigit _&gt;`, *value _}) = w; 现在唯一的问题是parse 仍在抛出一个歧义异常,即使解析树中不再存在歧义;我将此描述为我应该提交报告的缺陷。
  • 顺便说一句,我还发现,当你有一个重写函数也是标识函数时,你会得到堆栈溢出。
  • 关于编译器警告:它已经在工作中!我们有这方面的原型。顺便说一句,当前的解析器生成器已经在 stderr 上打印了这样的警告(糟糕的 UX,但仍然如此)。当前的警告仅在语法仍然模棱两可时打印(因为缺少使用&gt;),但我认为我们也应该牢记您的建议,警告&gt; 完全无用的应用程序。
【解决方案2】:

&gt; 消歧机制适用于递归定义,例如表达式语法。

所以要解决下面的歧义:

syntax E 
   = [0-9]+
   | E "+" E
   | E "-" E
   ;

字符串1 + 3 - 4不能解析为1 + (3 - 4)(1 + 3) - 4

&gt; 给这个语法一个顺序,产生式应该在树的顶部。

layout L = " "*;
syntax E 
   = [0-9]+
   | E "+" E
   > E "-" E
   ;

这现在只允许(1 + 3) - 4 树。

要完成这个故事,1 + 1 + 1 怎么样?那可能是1 + (1 + 1)(1 + 1) + 1

这就是我们有leftrightnon-assoc 的用途。它们定义了如何处理同一生产中的递归。

syntax E 
   = [0-9]+
   | left E "+" E
   > left E "-" E
   ;

现在将强制执行:1 + (1 + 1)

当您使用运算符优先表时,例如c operator precedance table,您几乎可以从字面上复制它们。

请注意,这两个消歧特征并不完全相反。第一个歧义也可以通过将两个产生式放在左组中来解决,如下所示:

syntax E 
   = [0-9]+
   | left ( 
         E "+" E
        | E "-" E
     )
   ;

由于树的左侧受到青睐,您现在将获得另一棵树1 + (3 - 4)。所以它会有所不同,但这一切都取决于你想要什么。

更多详情请见tutor pages on disambiguation

【讨论】:

  • 是的。优先级消歧构造增加了语法规则可以嵌套和不能嵌套的约束。通过消除可能的嵌套之一,解决了歧义。因此,就相同非终结符的替代规则之间的有序偏好而言,它绝对没有做任何事情。
  • 我们在 Lang::sdf2 中有一个用于这种偏好的库模块,偏好/避免机制是解析后的,当它在同一个语法中多次出现时会产生奇怪的效果。跨度>
  • 我的建议是:在实现语义时让语法模棱两可,并在备选方案中做出选择
  • 最后,为错误恢复引入这种歧义是可行的,但结果表明这会使解析时间从线性变为二次或更糟。恢复功能首选算法更改。
  • 如果你现在真的需要在词汇之间消除歧义,我会使用更多的跟随和前置限制。 IE。 '词汇C = A?乙?其中 A 派生出首选词汇并使用跟随限制吃掉所有字符,除非它无法匹配然后 B 可以处理剩余部分。这需要一些修修补补!
猜你喜欢
  • 2012-03-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-23
  • 2011-11-14
  • 2014-02-20
  • 2021-06-16
  • 2017-04-14
相关资源
最近更新 更多