Algorithm 从PEG语法生成正确的短语

Algorithm 从PEG语法生成正确的短语,algorithm,parsing,peg,Algorithm,Parsing,Peg,我写了一个PEG语法分析器生成器只是为了好玩(有一天我会在NPM上发布),我想在它上面添加一个随机短语生成器会很容易。其思想是在给定语法的情况下自动获得正确的短语。因此,我设置了以下规则来从每种类型的解析器生成字符串: 序列p1p2。。。pn:为每个子Parser生成一个短语并返回串联 备选方案p1 | p2 |……|pn:随机选取一个子角色,并用它生成一个短语 重复p{n,m}:在[n,m]中选择一个数字x(或[n,n+2]是m===无穷大),并返回从p生成的短语的串联 终端:只需返回终端文

我写了一个PEG语法分析器生成器只是为了好玩(有一天我会在NPM上发布),我想在它上面添加一个随机短语生成器会很容易。其思想是在给定语法的情况下自动获得正确的短语。因此,我设置了以下规则来从每种类型的解析器生成字符串:

  • 序列
    p1p2。。。pn
    :为每个子Parser生成一个短语并返回串联
  • 备选方案
    p1 | p2 |……|pn
    :随机选取一个子角色,并用它生成一个短语
  • 重复
    p{n,m}
    :在
    [n,m]
    中选择一个数字
    x
    (或
    [n,n+2]
    m===无穷大
    ),并返回从
    p
    生成的短语的串联
  • 终端:只需返回终端文本
当我学习以下语法时:

S: NP VP
PP: P NP
NP: Det N | Det N PP | 'I'
VP: V NP | VP PP
V: 'shot' | 'killed' | 'wounded'
Det: 'an' | 'my' 
N: 'elephant' | 'pajamas' | 'cat' | 'dog'
P: 'in' | 'outside'
它工作得很好。一些例子:

my pajamas killed my elephant
an pajamas wounded my pajamas in my pajamas
an dog in I wounded my cat in I outside my elephant in my elephant in an pajamas outside an cat
I wounded my pajamas in my dog
此语法有一个递归(
PP:pnp
NP:Det N PP
)。当我采用另一种递归语法时,对于这次的数学表达式:

expr: term (('+' | '-') term)*
term: fact (('*' | '/') fact)*
fact: '1' | '(' expr ')'
几乎有两次,我收到一个“超过最大调用堆栈大小”错误(在NodeJS中)。另一半时间,我得到了正确的表达:

( 1 ) * 1 + 1
( ( 1 ) / ( 1 + 1 ) - ( 1 / ( 1 * 1 ) ) / ( 1 / 1 - 1 ) ) * 1
( ( ( 1 ) ) )
1
1 / 1
我猜
fact
的递归产品调用太频繁,调用堆栈太深,这使得整个过程都失败了


我如何使我的方法不那么幼稚,以避免那些导致调用堆栈爆炸的情况?谢谢。

当然,如果语法描述任意长的输入,那么很容易陷入非常深的递归。避免这种陷阱的一种简单方法是保持部分扩展句子形式的优先级队列,其中键是长度。删除最短的终端,并以各种可能的方式替换每个非终端,释放现在所有的终端,并将剩余的终端添加回队列。您可能还希望维护一个“已发出”集,以避免发出重复项。如果语法没有类似于epsilon productions的语句形式派生较短字符串,那么该方法将以非递减长度顺序生成语法描述的所有字符串。也就是说,一旦看到长度为N的输出,所有长度为N-1或更短的字符串都已经出现

由于OP询问了详细信息,下面是表达式语法的一个实现。通过将PEG重写为CFG,可以简化它

import heapq

def run():
  g = {
    '<expr>': [
      ['<term>'],
      ['<term>', '+', '<expr>'],
      ['<term>', '-', '<expr>'],
    ],
    '<term>': [
      ['<fact>'],
      ['<fact>', '*', '<term>'],
      ['<fact>', '/', '<term>'],
    ],
    '<fact>': [
      ['1'],
      ['(', '<expr>', ')']
    ],
  }
  gen(g)

def is_terminal(s):
  for sym in s:
    if sym.startswith('<'):
      return False;
  return True;

def gen(g, lim = 10000):
  q = [(1, ['<expr>'])]
  n = 0;
  while n < lim:
    _, s = heapq.heappop(q)
    # print("pop: " + ''.join(s))
    a = []
    b = s.copy()
    while b:
      sym = b.pop(0)
      if sym.startswith('<'):
        for rhs in g[sym]:
          s_new = a.copy()
          s_new.extend(rhs)
          s_new.extend(b)
          if is_terminal(s_new):
            print(''.join(s_new))
            n += 1
          else:
            # print("push: " + ''.join(s_new))
            heapq.heappush(q, (len(s_new), s_new))
        break # only generate leftmost derivations
      a.append(sym)

run()

你有关于这项技术的更多细节吗,比如一篇论文?它有名字吗?没有。但我在过去为CFGs实现了它来生成测试数据。它很好用。如果您真的需要代码,我可能会编写一个示例。@Strebler好的,我添加了一个快速而肮脏的实现。非常有趣!这在生成所有句子方面做得很好,但可能需要一段时间才能生成一个有趣的中等长度的句子(这是OP的一种反问题,其中样本主要是长句)。请参阅一篇论文的参考资料,该论文提供了一种从(无epsilon)CFG生成随机句子的算法。@Strebler:是的,我引用的论文中的算法有些相似,但它的工作是计算更精确的权重,因此您确实得到了一个统一的样本(给定大小)。在Python中,它很容易实现,因为您不必担心算术溢出。请注意,对于PEG,“Alternative…:随机选取一个子parser并使用它生成一个短语”不一定会生成有效的输入。这是因为PEG备选方案是有序的,早期备选方案可能会影响后期备选方案前缀的子集。换句话说,您可以随机选择一个由子parser生成的短语,该短语以某个可以由较早的子parser生成的短语开头,因此解析器永远无法识别该短语。我认为有一个与重复相关的问题。@rici-True,有什么方法可以解决这个问题吗?简单的方法是在返回之前验证生成的句子是否可以被解析。根据你的语法,可能需要也可能不需要多次重试;我甚至不敢猜测。除此之外,我不知道;在我看来,几乎所有关于PEG的有趣的理论问题都是难以解决的,我从来没有努力去克服这一点。
1
(1)
1*1
1/1
1+1
1-1
((1))
(1*1)
(1/1)
(1)*1
(1)+1
(1)-1
(1)/1
(1+1)
(1-1)
1*(1)
1*1*1
1*1/1
1+(1)
1+1*1
1+1/1
1+1+1
1+1-1
1-(1)
1-1*1
1-1/1
1-1+1
1-1-1
1/(1)
1/1*1
1/1/1
1*1+1
1*1-1
1/1+1
1/1-1
(((1)))
((1*1))
((1/1))
((1))*1
((1))+1
((1))-1
((1))/1
((1)*1)
((1)+1)
((1)-1)
((1)/1)
((1+1))
((1-1))
(1)*(1)
(1)*1*1
(1)*1/1
(1)+(1)
(1)+1*1