Pegjs: 实施可参数化的规则

创建于 2011-08-25  ·  29评论  ·  资料来源: pegjs/pegjs

能够用变量参数化规则会很棒;

   = '\"' parse_contents '\"' ->
   / '\'' parse_contents('\'') '\'' ->
   / '+' parse_contents('+') '+' -> /* sure why not :) */

parse_contents(terminator='\"')
    = ('\\' terminator / !terminator .)+ -> return stuff
feature

最有用的评论

感谢您花时间解释

十年后我可能会再问你一个问题。 有一个美好的2020年代

所有29条评论

您是否有特定的用例可以节省大量工作或使当前不可能的事情成为可能?

通过调用将级别作为参数传递的规则,它使解析缩进级别变得更加容易。

此外,在纯粹的 DRY 逻辑中,当执行诸如“由该字符用转义序列分隔的东西”之类的事情时,调用类似delimited('\'', '\\')的东西比只执行规则(及其操作!)三次更好.

我应该更清楚。 通过“具体”,我正在寻找类似“我正在研究一种语言 X 的语法,其中有 5 条规则可以合并为一个,它们是:”也就是说,我想看看真实世界用例和真实世界的代码。 从中我可以更好地判断此功能在什么情况下有用以及对多少人有用。

请不要接受这个,因为我反对这个功能本身。 由于复杂性和实现成本,我通常不想实现仅对一小部分语言或开发人员有用的功能。 而在这种情况下,成本相对较高。

只是为javascript编写一个解析器,我可以有string = delimited_by('\'') / delimited_by('\"') ,然后是regexp = delimited_by('/')

最近,我一直在为自己的语言编写解析器。 我在为 python 编写的 PEG 框架中有类似的东西:

LeftAssociative(op, subrule): l:subrule rest:(op subrule)* -> a_recursive_function_that_reverses_rest_and_makes_bintrees(l, op, subrule)

然后我可以写:

...
exp_mult = LeftAssociative(/[\*\/%]/, exp_paren)
exp_add = LeftAssociative(/[\+-]/, exp_mult)

由于我的优先级与 C++ 中的级别差不多(所有运算符加上更多),我会让您想象 in 的用处有多大。 我还没有完成解析表达式,但我已经使用了 12 次。

如果与“导入”功能相结合,那就太好了

require(CommaSeparatedList.pegjs)
require(StringLiteral.pegjs)
require(IntegerLiteral.pegjs)

...

Function
 = name:FuncName "(" args:CommaSeparatedList(Literal)  ")" 

Hash
= "{"   props:CommaSeparatedList(Prop)   "}"

Prop
= Key ":" Literal

Literal =
  StringLiteral / IntegerLiteral

(这比 OP 的要求要复杂一些,但它似乎太接近证明它自己的线程是合理的。)

我正在 PEG.js 的帮助下构建一个 R5RS Scheme 解析器。 除了需要上下文感知解析的准引用之外,一切都很顺利。 能够参数化规则以便从模板即时生成规则,避免大量尴尬的后处理,这将是有用的。 例如,简化的 quasiquotation 语法可能如下所示:

    quasiquotation = qq[1]
    qq[n] = "`" qq_template[n]
    qq_template[0] = expression
    qq_template[n] = simple_datum / list_qq_template[n] / unquotation[n]
    list_qq_template[n] = "(" qq_template[n]* ")" / qq[n+1]
    unquotation[n] = "," qq_template[n-1]

如果有兴趣将其添加到工具中,我有兴趣为该功能的开发做出贡献。

这样做的主要原因是支持对上下文敏感的语法,如果我没记错的话,最流行的语言是(我确信 C 和 python 有上下文特定的东西)。 根据 Trevor Jim 的说法,Haskell 也不是上下文无关的,并且断言大多数语言都不是:

http://trevorjim.com/haskell-is-not-context-free/
http://trevorjim.com/how-to-prove-that-a-programming-language-is-context-free/

在可以回溯(如 PEG 可以)的解析器中使用外部状态是危险的,并且可能会产生在此解析器中可以看到的问题:

{   var countCs = 0;
}

start = ((x/y) ws*)* { return countCs }

x = "ab" c "d"
y = "a" bc "e"

c = "c" { countCs++; }
bc = "bc" { countCs++; }

ws = " " / "\n"

上面返回 2 而不是 1 的正确答案。这样的问题很难推理,可能会产生难以发现的隐蔽错误,而且一旦发现就很难解决,更不用说优雅地解决了. 我不清楚如何在不对 PEG 返回的数据进行后处理的情况下做到这一点。 如果以某种方式您的解析器本身需要计数,那简直是不走运。

目前,(危险地)使用外部状态是解析对上下文敏感的语法的唯一方法。 使用参数化规则,解析器可以解析它而不会冒无效状态的风险:

start = countCs:((x<0>/y<0>) ws*)* { return countCs.reduce(function(a,b){return a+b[0];}, 0); }

x<count> = "ab" theCount:c<count> "d" { return theCount; }
y<count> = "a" theCount:bc<count> "e" { return theCount; }

c<count> = "c" { return count++; }
bc<count> = "bc" { return count++; }

ws = " " / "\n"

大卫,你问的是真实情况,python 的空格缩进语法显然是这里的一个例子。 我想在 Lima(我用 PEG 制作的编程语言)中做类似的空格缩进语法。 但是,当我无意中创建了将一切都搞砸的无效状态时,我不想实现类似的东西。 我可以命名任何需要上下文的解析构造,例如 C 的 x* y(是 x 乘以 y 还是 y 被定义为指向 x 类型值的指针?)。

请注意,对于对上下文敏感的语法是可解析的,必须将已经匹配的子表达式返回的信息传递到参数化规则中 - 否则解析器实际上无法使用任何上下文。 这是我正在为 Lima 考虑的字符串类型的真实示例,它仅在参数化解析可用并且可以访问(作为变量)先前匹配的表达式的标签时才有效:

literalStringWithExplicitLength = "string[" n:number ":" characters<n> "]"
number = n:[0-9]* {return parseInt(n.join(''));}
characters<n> = c:. { // base case
  if(n>0) return null; // don't match base case unless n is 0
  else return c;
}
/ c:. cs:characters<n-1> {
  ret c+cs
}

这将能够解析像 string[10:abcdefghij] 这样的字符串。 就目前而言,您无法使用纯正的 PEG.js 来做到这一点。 你做了一些可怕的事情,比如:

{ var literalStringLengthLeft=undefined;
}
literalStringWithExplicitLength = "string[" n:specialNumber ":" characters "]"
specialNumber = n:number {
  literalStringLengthLeft = n;
  return n;
}
number = n:[0-9]* {return parseInt(n.join(''));}
characters = c:character cs:characters? {
  return c + cs
}
character = c:. {
  if(literalStringLengthLeft > 0) {
    literalStringLengthLeft--;
    return c;
  } else {
    literalStringLengthLeft = undefined;
    return null; // doesn't match
  }
}

许多协议都有这种解析需求——例如,IPv4 数据包有一个描述其总长度的字段。 您需要该上下文来正确解析数据包的其余部分。 IPv6、UDP 以及可能任何其他基于数据包的协议也是如此。 大多数使用 TCP 的协议也需要这样的东西,因为需要能够使用相同的概念字符流传输多个完全独立的对象。

无论如何,我希望我已经给出了一些很好的例子和理由,为什么我认为这不仅是一个很好的特性,不仅是一个强大的特性,而且确实是许多解析器缺少的一个基本特性(目前包括 PEG.js )。

Pegasus (一个与 peg.js 共享其大部分语法的项目)通过使用#STATE{}表达式来解决这个问题,该表达式能够改变状态字典。 当规则被回溯时,这个状态字典被回溯。 这允许它支持重要的空白解析(有关详细信息,请参阅关于重要空白的 wiki 条目)。

此外,通过与解析光标一起回溯状态,也可以为有状态规则完成记忆。

我认为 Peg.js 可以轻松做到这一点。

规则回溯时,Pegasus 如何管理回溯状态? 我可以想象你可以保留整个程序状态发生变化的快照,然后将其恢复,但这会很昂贵。 我可以想象只保留更改的变量的快照,但这要么需要用户指定它,这会增加创建解析器的复杂性,要​​么需要解析器以某种方式找出在某些代码中更改的所有状态。 这些听起来都不理想,那么 Pegasus 是如何做到的呢?

从理论上讲,如果 A. 动作在闭包中排队并且仅在解析器完全完成后执行,并且 B. 因为它们在解析器完成后执行,它们无法取消规则匹配,则解析器可以避免无效执行的动作。 也许该方案会比在 pegasus 中进行的状态回溯更优化?

此外,修复无效状态的问题确实非常好,但它并没有解决我提出的与字符串 [10:abcdefghij] 等字符串文字相关的可表达性问题,但我绝对对它的工作原理感兴趣

它不会回溯整个程序的状态。 它维护着一个不可变的状态字典。 它将当前状态字典与光标一起保存,并且每当光标回溯时,状态字典就会随之回溯。 字典在#STATE{}动作之外的任何地方都是不可变的,并且在每次状态更改之前被复制。

每次移动光标时设置一个额外的变量都会带来很小的性能损失,但这远远超过了记忆有状态规则的能力。 此外,这不会导致大量的内存分配,因为状态字典的不可变特性允许它被共享,直到它被改变。 例如,如果您的解析器中没有状态,则只有一个分配:一个(空)状态字典。

JavaScript(据我所知)不具备使对象不可变的能力,但这主要是一项安全功能。 Peg.js 只需要在处理每个#STATE{}代码块之前复制一个状态字典,并在光标被回溯时回溯。

哦,好的,所以用户基本上必须指定他们正在改变的状态。 这很酷。 但我仍然认为它并没有真正涵盖参数化所带来的相同好处。 听起来它本身可能对其他事情有用。

我刚刚写了一个提供环境的叉子,可以使用变量env访问: https ://github.com/tebbi/pegjs
这与上面建议的#STATE{}对象相同。
这是一个快速破解,使用(包)全局变量,只要留下解析函数就会恢复。 env的复制是通过 Object.create() 完成的。

这是一个示例语法,它使用它来解析由 Python 定义的空白块:

{
  env.indLevel = -1
}

block =
  empty
  ind:ws* &{
    if (ind.length <= env.indLevel) return false;
    env.indLevel = ind.length;
    return true;
  }
  first:statement? rest:indStatement*
  {
    if (first) rest.unshift(first);
    return rest;
  }

indStatement =
  "\n" empty ind:ws* &{ return env.indLevel === ind.length; }
  stm:statement
  {return stm; }

statement =
    id:identifier ws* ":" ws* "\n"
    bl:block { return [id, bl]; }
  / identifier

identifier = s:[a-z]* { return s.join(""); }

empty = (ws* "\n")*

ws = [ \t\r]

这是生成的解析器的示例输入:

b:
   c
   d:
       e
   f
g

我的印象是 PEG.js 不支持任何类型的规则参数——这令人惊讶。 这个功能对我来说非常重要。

我需要的比 OP 的请求更简单——OP 想要根据参数修改语法本身,但至少我只需要将整数传递给规则。 基本上我想翻译一个看起来像这样的LLLPG规则(其中PrefixExpr是一个高优先级表达式,例如像-x这样的前缀表达式,或者一个标识符......):

@[LL(1)]
rule Expr(context::Precedence)::LNode @{
    {prec::Precedence;}
    e:PrefixExpr(context)
    greedy
    (   // Infix operator
        &{context.CanParse(prec=InfixPrecedenceOf(LT($LI)))}
        t:(NormalOp|BQString|Dot|Assignment)
        rhs:Expr(prec)
        { ... }
    |   // Method_calls(with arguments), indexers[with indexes], generic!arguments
        &{context.CanParse(P.Primary)}
        e=FinishPrimaryExpr(e)
    |   // Suffix operator
        ...
    )*
    {return e;}
};
// Helper rule that parses one of the syntactically special primary expressions
@[private] rule FinishPrimaryExpr(e::LNode)::LNode @{
(   // call(function)
    "(" list:ExprList(ref endMarker) ")"
    { ... }
    |   // ! operator (generic #of)
        "!" ...
    |   // Indexer / square brackets
        {var args = (new VList!LNode { e });}
        "[" args=ExprList(args) "]"
        { ... }
    )
    {return e;}
};

我的语言有 25 个优先级,通过这些规则,我将几乎所有这些规则都折叠为由单个规则处理(您可以将Precedence视为围绕几个整数的包装器,这些整数描述了一个操作员)。 此外,我的语言有无限数量的运算符(基本上是任何标点符号序列),运算符的优先级是在解析后决定的。 虽然_技术上_可能以通常的方式解析语言,为 25 种运算符中的每一种都设置一个单独的规则,但这将是一种可怕的方式。

您还可以在此处看到内部规则FinishPrimaryExpr构造了一个语法树,其中包含从封闭规则传递的参数。

那么......有没有办法将参数传递给 PEG.js 规则?

+1! 就我而言,我只想为语法生成一个解析器,其中一些分隔符是全局可配置的。 在这种情况下,我可以通过匹配与谓词组合的任何表达式来替换分隔符文字来实现这一点,但如果可以简单地用变量替换匹配所有内容,那将更加优雅(也更有效)。

+1,在可预见的将来有机会看到这个实现吗?

另一个用例。 这是来自您的javascript.pegjs示例:

(...)

RelationalExpression
  = head:ShiftExpression
    tail:(__ RelationalOperator __ ShiftExpression)*
    { return buildBinaryExpression(head, tail); }

RelationalOperator
  = "<="
  / ">="
  / $("<" !"<")
  / $(">" !">")
  / $InstanceofToken
  / $InToken

RelationalExpressionNoIn
  = head:ShiftExpression
    tail:(__ RelationalOperatorNoIn __ ShiftExpression)*
    { return buildBinaryExpression(head, tail); }

RelationalOperatorNoIn
  = "<="
  / ">="
  / $("<" !"<")
  / $(">" !">")
  / $InstanceofToken

(...)

  (...)
  / ForToken __
    "(" __
    init:(ExpressionNoIn __)? ";" __
    test:(Expression __)? ";" __
    update:(Expression __)?
    ")" __
    body:Statement
  (...)

(...)

所有这些...NoIn规则(其中有很多)都是必需的,仅仅是因为for in语句。 没有更好的方法是这样的:

(...)

RelationalExpression<allowIn>
  = head:ShiftExpression
    tail:(__ RelationalOperator<allowIn> __ ShiftExpression)*
    { return buildBinaryExpression(head, tail); }

RelationalOperator<allowIn>
  = "<="
  / ">="
  / $("<" !"<")
  / $(">" !">")
  / $InstanceofToken
  / &{ return allowIn; } InToken
    {return "in";}

(...)

  (...)
  / ForToken __
    "(" __
    init:(Expression<false> __)? ";" __
    test:(Expression<true> __)? ";" __
    update:(Expression<true> __)?
    ")" __
    body:Statement
  (...)

(...)

这感觉与指定 JavaScript 语法的方式非常相似: https: //tc39.github.io/ecma262/#prod -IterationStatement(注意~In

我目前正在开发的一种语言有这个确切的问题:我只想在某些点禁用/启用一些规则。 我非常希望避免像您对 JavaScript 语法所做的那样复制每个受影响的规则。

有没有其他方法可以在不复制规则的情况下实现这一目标?

+1,在可预见的将来有机会看到这个实现吗?

此问题分配了一个里程碑(1.0.0 之后)。 PEG.js 的当前版本是 0.10.0。 显然,发布 1.0.0 之后会处理 1.0.0 后的问题,根据路线图,这将在发布 0.11.0 之后的某个时间点发生。

这应该回答你的问题。 加速整个过程的最佳方法是帮助解决针对0.11.01.0.0的问题。

有没有其他方法可以在不复制规则的情况下实现这一目标?

一种可能的方法是手动跟踪状态,然后使用语义谓词。 但是这种方法存在回溯问题,我不推荐它(换句话说,当在规则复制和手动状态跟踪之间进行选择时,我会选择复制)。

有两种类型的参数可以传递给解析器:

  1. 价值观。 Python、Nim 和 Haskell 等语言的语法(以及不同方式的 Scheme)取决于树内部表达式的“深度”。 编写正确的语法需要以某种方式传递此上下文。
  2. 解析器。 leftAssociative(element, separator)escapedString(quote)withPosition(parser)就是很好的例子。

应该有办法以某种方式标记参数是解析器还是值。 当我试图找出正确的方法时,我最终使用全局变量作为上下文,这显然是一条死胡同。 有人对此有任何想法吗?

呢?

鉴于:

Add <Expression, Add>
  = left:Expression _ '+' _ right:Add
    { return { type: 'add', left, right } }
  / Expression

什么时候:

  = Add <MyExpression, MyAdd>

MyExpression
  = [0-9]+

然后:

  = left:MyExpression _ '+' _ right:MyAdd
    { return { type: 'add', left, right } }
  / MyExpression

MyExpression
  = [0-9]+

这使我们能够自下而上地构建规则 :smirk:

我同意,我建议开发人员添加此功能 :)

我真的需要这个功能来更新我正在编写的 JavaScript 语法,所以这在我的愿望清单上很重要。 会试一试,看看效果如何。

@samvv我从一条非常不同的路线遇到了这个问题,还没有阅读整个线程。
但是,在#572 中,我在这里提到过,我展示了一种可以模拟参数化规则的技术。

也就是说,本质上是:将函数作为中间解析结果返回。

那个“把戏”绝不是我的发明,我猜对你的目的来说可能相当笨拙。 但这对您来说可能是一种解决方法。 我的意思是直到“发布 v1.0”... :)

@meisl酷,感谢您的提示! 当我有时间时会尝试一下。

@samvv哦,啊……恐怕我忽略了一些相当重要的事情:

是否希望参数化规则

  • 只能产生,这取决于参数
  • 或(也)使其解析决策取决于参数

我提出的只是对前者有帮助——而后者是 OP 的实际问题......
对此感到抱歉。

然而,即使是后者,也一种解决方法,尽管它更笨重。
而且,“相关决策”部分与返回函数没有任何关系......

我正在附加一个示例供您在https://pegjs.org/online中试用

基本思路是:用全局状态来记住当前的“终结者”。 诚然,这是一种相当的技巧,而且是重复的。
但是:添加另一个分隔符,比如|只是意味着添加一个替代str

  / (t:'|' {term = t}) c:conts t:.&{ return isT(t) }  { return c }

它与其他的不同之处在于一个字符|


{
  var term;
  function isT(ch) { return ch === term }
  function isE(ch) { return ch === '\\' }
}
start = str*

str
  = (t:'\"' {term = t}) c:conts t:.&{ return isT(t) }  { return c }
  / (t:'\'' {term = t}) c:conts t:.&{ return isT(t) }  { return c }

conts
  = c:(
        '\\' t:.&{ return isT(t) || isE(t) } { return t }
      /      t:.!{ return isT(t)           } { return t }
    )*
    { return c.join('') }

...关于输入

  • "abc" -> ["abc"]
  • "a\"bc" -> ["a\"bc"]
  • "a\\bc" -> ["a\bc"]
  • "a\\b\"c"'a\\b\'' -> ["a\b\"c", "a\b'c"]

ps:我知道,这真的不是一个人想要手写的东西。 但是,嘿,想象一下它会为你生成......我认为原则上就是这样

@ceymard - 我意识到这是十年后的事了,但我很好奇这与 #36 有何不同

哇,我花了一段时间才记起来。 10年 !

在这个 PR 中,规则接受参数并且可以参数化。 这意味着语法本身使用它来避免重复相似但不同的规则。

在#36 中,规则是在语法本身之外指定的。 因此,语法本身是参数化的。

我认为范围是不同的,尽管有人可能会争辩说语法本身就是一条规则,因此这是同一个问题。 我认为不是,因为 #36 可能意味着一些轻微的 API 更改,而这个 PR 不会。

那么,以一种非常不正确的方式滥用 C++ ish 术语,前者是模板静态,而后者是构造函数调用?

我想这个类比有点工作,是的。

感谢您花时间解释

十年后我可能会再问你一个问题。 有一个美好的2020年代

这对于消除我的解析器定义的冗余非常有用。 我有一个自定义语法,故意非常轻松,并且需要在稍微不同的上下文中应用一些规则。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

alanmimms picture alanmimms  ·  10评论

doersino picture doersino  ·  15评论

StoneCypher picture StoneCypher  ·  8评论

Coffee2CodeNL picture Coffee2CodeNL  ·  13评论

marek-baranowski picture marek-baranowski  ·  6评论