如何设置可以处理歧义的语法


9

我正在尝试创建一种语法来解析我设计的一些类似于Excel的公式,其中字符串开头的特殊字符表示其他来源。例如,$可以表示一个字符串,因此“ $This is text”将被视为程序中的字符串输入,并且&可以表示一个函数,因此&foo()可以被视为对内部函数的调用foo

我面临的问题是如何正确构建语法。例如,这是MWE的简化版本:

grammar = r'''start: instruction

?instruction: simple
            | func

STARTSYMBOL: "!"|"#"|"$"|"&"|"~"
SINGLESTR: (LETTER+|DIGIT+|"_"|" ")*
simple: STARTSYMBOL [SINGLESTR] (WORDSEP SINGLESTR)*
ARGSEP: ",," // argument separator
WORDSEP: "," // word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: STARTSYMBOL SINGLESTR "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.WORD
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''
parser = lark.Lark(grammar, parser='earley')

因此,这种语法之类的东西:$This is a string&foo()&foo(#arg1)&foo($arg1,,#arg2)&foo(!w1,w2,w3,,!w4,w5,w6)都如预期解析。但是,如果我想为simple终端增加更多的灵活性,则需要开始摆弄SINGLESTR不方便的令牌定义。

我尝试了什么

我无法逾越的部分是,如果我想让一个包含括号的字符串(是的文字func),那么在当前情况下我将无法处理它们。

  • 如果在中添加括号SINGLESTR,则会得到Expected STARTSYMBOL,因为它与func定义混在一起,并且认为应该传递函数自变量,这很有意义。
  • 如果我重新定义语法以仅为函数保留和号并在其中添加括号SINGLESTR,则可以用括号解析字符串,但是我要解析的每个函数都给出Expected LPAR

我的意图是将任何以a开头的内容$都解析为SINGLESTR令牌,然后再解析&foo($first arg (has) parentheses,,$second arg)

目前,我的解决方案是在字符串中使用LEFTPAR和RIGHTPAR之类的“转义”词,并且编写了辅助函数以在处理树时将其更改为括号。所以,$This is a LEFTPARtestRIGHTPAR产生正确的树,当我对其进行处理时,它将被翻译为This is a (test)

提出一个一般性问题:我可以这样定义我的语法吗:在某些情况下,某些特殊的语法字符被视为普通字符,而在其他情况下,某些特殊字符被视为普通字符?


编辑1

根据jbndlr我的评论,我修订了语法以根据开始符号创建单独的模式:

grammar = r'''start: instruction

?instruction: simple
            | func

SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"("|")")*
FUNCNAME: (LETTER+) (LETTER+|DIGIT+|"_")* // no parentheses allowed in the func name
DB: "!" SINGLESTR (WORDSEP SINGLESTR)*
TEXT: "$" SINGLESTR
MD: "#" SINGLESTR
simple: TEXT|DB|MD
ARGSEP: ",," // argument separator
WORDSEP: "," // word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: "&" FUNCNAME "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.WORD
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''

这(有点)属于我的第二个测试案例。我可以解析所有simple类型的字符串(可以包含括号的TEXT,MD或DB令牌)和为空的函数;例如,&foo()&foo(&bar())正确解析。将参数放入函数(无论是哪种类型)的那一刻,我得到一个UnexpectedEOF Error: Expected ampersand, RPAR or ARGSEP。作为概念证明,如果我在上面的新语法中从SINGLESTR的定义中删除括号,则一切正常,但是我回到正题。


您确实有一些字符来标识它们之后的内容(您的STARTSYMBOL),并在需要清楚的地方添加了分隔符和括号;我在这里看不到任何歧义。您仍然必须将STARTSYMBOL列表分成多个单独的项才能区分。
jbndlr

我将很快发布一个真正的答案,已经从事了几天。
iliar

我提供了答案。尽管赏金仅剩2个小时,但您仍可以在接下来的24小时宽限期内手动授予赏金。如果我的回答不好,请尽快告诉我,我会解决。
iliar

Answers:


3
import lark
grammar = r'''start: instruction

?instruction: simple
            | func

MIDTEXTRPAR: /\)+(?!(\)|,,|$))/
SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"("|MIDTEXTRPAR)*
FUNCNAME: (LETTER+) (LETTER+|DIGIT+|"_")* // no parentheses allowed in the func name
DB: "!" SINGLESTR (WORDSEP SINGLESTR)*
TEXT: "$" SINGLESTR
MD: "#" SINGLESTR
simple: TEXT|DB|MD
ARGSEP: ",," // argument separator
WORDSEP: "," // word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: "&" FUNCNAME "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.WORD
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''

parser = lark.Lark(grammar, parser='earley')
parser.parse("&foo($first arg (has) parentheses,,$second arg)")

输出:

Tree(start, [Tree(func, [Token(FUNCNAME, 'foo'), Tree(simple, [Token(TEXT, '$first arg (has) parentheses')]), Token(ARGSEP, ',,'), Tree(simple, [Token(TEXT, '$second arg')])])])

我希望这就是您想要的。

那些日子已经疯了。我试过百灵鸟,但失败了。我也试过persimoniouspyparsing。所有这些不同的解析器都存在相同的问题,即“参数”令牌消耗了函数的右括号,最终由于函数的括号未关闭而失败。

诀窍是弄清楚如何定义“不特殊”的右括号。请参见MIDTEXTRPAR上面的代码中的正则表达式。我将其定义为右括号,后面没有参数分隔符或字符串结尾。我使用正则表达式扩展名做到了这一点,该扩展名(?!...)仅在不跟随...但不消耗字符的情况下才匹配。幸运的是,它甚至允许在此特殊正则表达式扩展名内匹配字符串的结尾。

编辑:

上面提到的方法仅在没有以)结尾的参数时才有效,因为这样MIDTEXTRPAR正则表达式将无法捕获该),即使有更多要处理的参数,它也会认为函数的结尾。此外,可能存在歧义,例如.... asdf),...,它可能是参数内部的函数声明的结尾,或者参数内部的'text-like'),然后函数声明继续进行。

此问题与以下事实有关:您在问题中描述的内容不是上下文无关的语法(https://en.wikipedia.org/wiki/Context-free_grammar),存在诸如百灵鸟之类的解析器。相反,它是上下文相关的语法(https://en.wikipedia.org/wiki/Context-sensitive_grammar)。

之所以成为上下文敏感的语法,是因为您需要解析器“记住”它嵌套在一个函数内,以及嵌套的层次数,并以某种方式在语法的语法内提供此内存。

编辑2:

还请看一下以下上下文敏感的解析器,该解析器似乎可以解决问题,但嵌套函数的数量却呈指数时间复杂度,因为它尝试解析所有可能的函数障碍,直到找到可行的障碍。我相信它必须具有指数级的复杂性,因为它不是上下文无关的。


_funcPrefix = '&'
_debug = False

class ParseException(Exception):
    pass

def GetRecursive(c):
    if isinstance(c,ParserBase):
        return c.GetRecursive()
    else:
        return c

class ParserBase:
    def __str__(self):
        return type(self).__name__ + ": [" + ','.join(str(x) for x in self.contents) +"]"
    def GetRecursive(self):
        return (type(self).__name__,[GetRecursive(c) for c in self.contents])

class Simple(ParserBase):
    def __init__(self,s):
        self.contents = [s]

class MD(Simple):
    pass

class DB(ParserBase):
    def __init__(self,s):
        self.contents = s.split(',')

class Func(ParserBase):
    def __init__(self,s):
        if s[-1] != ')':
            raise ParseException("Can't find right parenthesis: '%s'" % s)
        lparInd = s.find('(')
        if lparInd < 0:
            raise ParseException("Can't find left parenthesis: '%s'" % s)
        self.contents = [s[:lparInd]]
        argsStr = s[(lparInd+1):-1]
        args = list(argsStr.split(',,'))
        i = 0
        while i<len(args):
            a = args[i]
            if a[0] != _funcPrefix:
                self.contents.append(Parse(a))
                i += 1
            else:
                j = i+1
                while j<=len(args):
                    nestedFunc = ',,'.join(args[i:j])
                    if _debug:
                        print(nestedFunc)
                    try:
                        self.contents.append(Parse(nestedFunc))
                        break
                    except ParseException as PE:
                        if _debug:
                            print(PE)
                        j += 1
                if j>len(args):
                    raise ParseException("Can't parse nested function: '%s'" % (',,'.join(args[i:])))
                i = j

def Parse(arg):
    if arg[0] not in _starterSymbols:
        raise ParseException("Bad prefix: " + arg[0])
    return _starterSymbols[arg[0]](arg[1:])

_starterSymbols = {_funcPrefix:Func,'$':Simple,'!':DB,'#':MD}

P = Parse("&foo($first arg (has)) parentheses,,&f($asdf,,&nested2($23423))),,&second(!arg,wer))")
print(P)

import pprint
pprint.pprint(P.GetRecursive())

1
谢谢,这按预期工作!授予赏金,因为您无需以任何方式逃脱括号。您付出了额外的努力,它显示了!仍然存在一个以括号结尾的“文本”参数的极端情况,但是我只需要忍受那个。您还以清楚的方式解释了歧义,我只需要再测试一下,但就我的目的而言,这将非常有效。感谢您提供有关上下文相关语法的更多信息。我真的很感激!
Dima1982 '19

@ Dima1982非常感谢!
iliar

@ Dima1982看一下编辑,我做了一个解析器,也许可以用成倍的时间复杂度来解决您的问题。另外,我考虑过,如果您的问题具有实用价值,则转义括号可能是最简单的解决方案。或使函数带有其他括号,例如&用来分隔函数参数列表的末尾。
iliar

1

问题是函数的参数包含在括号中,其中一个参数可能包含括号。
一种可能的解决方案是当它是String的一部分时在(或)之前使用Backspace \

  SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"\("|"\)")*

C使用的类似解决方案,包括双引号(“)作为字符串常量的一部分,其中字符串常量用双引号引起来。

  example_string1='&f(!g\()'
  example_string2='&f(#g)'
  print(parser.parse(example_string1).pretty())
  print(parser.parse(example_string2).pretty())

输出是

   start
     func
       f
       simple   !g\(

   start
     func
      f
      simple    #g

我认为这与OP自己用LEFTPAR和RIGHTPAR替换“(”和“)”的解决方案几乎相同。
iliar
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.