Pegjs: 支持解析基于缩进的语言

创建于 2013-10-16  ·  34评论  ·  资料来源: pegjs/pegjs

我使用了一堆基于缩进的语言,比如 CoffeeScript、Jade,并且想自己创建 DSL。
我通过搜索发现了一些在 pegjs 中保持缩进的技巧,想知道是否有一致的解决方案:
http://stackoverflow.com/questions/11659095/parse-indentation-level-with-peg-js
http://stackoverflow.com/questions/4205442/peg-for-python-style-indentation
https://gist.github.com/jakubkulhan/3192844
https://groups.google.com/forum/#!searchin/pegjs/indent/pegjs/RkbAB4rPlfU/xxafrY5wGCEJ
但是 pegjs 会支持这个功能吗?

feature

最有用的评论

依靠在自定义处理程序中添加的副作用来解析基于缩进的语法是非常危险的。 只是不要这样做。 Pegjs 必须添加一些推送和弹出条件状态的能力,以便使解析缩进(和其他上下文相关的语法)安全。

这就是我现在所做的,我建议您这样做:预处理输入文件并插入您自己的缩进/缩进标记。 我分别使用 {{{{ 和 }}}}。 那么你的语法是上下文无关的,可以正常解析。 它可能会弄乱您的行/列值,但您可以在后处理器中更正这些值。

所有34条评论

依靠在自定义处理程序中添加的副作用来解析基于缩进的语法是非常危险的。 只是不要这样做。 Pegjs 必须添加一些推送和弹出条件状态的能力,以便使解析缩进(和其他上下文相关的语法)安全。

这就是我现在所做的,我建议您这样做:预处理输入文件并插入您自己的缩进/缩进标记。 我分别使用 {{{{ 和 }}}}。 那么你的语法是上下文无关的,可以正常解析。 它可能会弄乱您的行/列值,但您可以在后处理器中更正这些值。

如果您不需要以 javascript 为目标, Pegasus (我的 C# 的 pegjs 克隆)支持推送/弹出状态。 这是一篇关于如何做你想做的事的维基文章: https :

我想建议 pegjs 使用我的语法作为基于状态解析的起点。

安全地推送和弹出状态的能力很好。 如果它是基于 Javascript 的,我会使用它。 只是为了解析而集成 CLR 是不值得的。

我就是这么想的。 我认为,在那种情况下,我可能应该尝试将我的改进移植到 pegjs 中。

但是,如果不与 @dmajda 对话,我不一定想这样做。

@otac0n很好。 我不写 C# 。 JavaScript 对我来说要好得多。

基于缩进的语言很重要。 我想看看在 1.0.0 之后简化他们的解析。

我认为这个问题最好通过允许状态来解决,就像 Pegasus 和 #285 中建议的那样。 这是一个想法(以下是 Pegasus 的重要空白语法翻译为 pegjs 并添加了我的语法想法):

{var indentation = 0}

program
  = s:statements eof { return s }

statements
  = line+

line
  = INDENTATION s:statement { return s }

statement
  = s:simpleStatement eol { return s }
  / "if" _ n:name _? ":" eol INDENT !"bar " s:statements UNDENT {
      return { condition: n, statements: s }
    }
  / "def" _ n:name _? ":" eol INDENT s:statements UNDENT {
      return { name: n, statements: s }
    }

simpleStatement
  = a:name _? "=" _? b:name { return { lValue: a, expression: b } }

name
  = [a-zA-Z] [a-zA-Z0-9]* { return text() }

_ = [ \t]+

eol = _? comment? ("\r\n" / "\n\r" / "\r" / "\n" / eof)

comment = "//" [^\r\n]*

eof = !.

INDENTATION
  = spaces:" "* &{ return spaces.length == indentation }

INDENT
  = #STATE{indentation}{ indentation += 4 }

UNDENT
  = #STATE{indentation}{ indentation -= 4 }

注意底部附近的#STATE{indentation}块(显然受到 Pegasus 的启发)。 我称这些状态块。 这个想法是在动作之前允许一个状态块。 这是一个更复杂的状态块:

#STATE{a, b, arr: {arr.slice()}, obj: {shallowCopy(obj)}, c}

它是以下的简写:

#STATE{a: {a}, b: {b}, arr: {arr.slice()}, obj: {shallowCopy(obj)}, c: {c}}

换句话说,在应用速记扩展之后,状态块的内容是identifier ":" "{" code "}" 。 在动作之前添加一个状态块告诉 pegjs 这个动作将修改列出的标识符,如果规则被回溯,这些标识符应该被重置为大括号之间的代码。

以下是上述语法中 INDENT 和 UNDENT 的编译函数,并添加了indentation变量的重置:

    function peg$parseINDENT() {
      var s0, s1, t0;

      s0 = peg$currPos;
      t0 = indentation;
      s1 = [];
      if (s1 !== peg$FAILED) {
        peg$reportedPos = s0;
        s1 = peg$c41();
      } else {
        indentation = t0;
      }
      s0 = s1;

      return s0;
    }

    function peg$parseUNDENT() {
      var s0, s1, t0;

      s0 = peg$currPos;
      t0 = indentation;
      s1 = [];
      if (s1 !== peg$FAILED) {
        peg$reportedPos = s0;
        s1 = peg$c42();
      } else {
        indentation = t0;
      }
      s0 = s1;

      return s0;
    }

下面是如何编译上面的“复杂状态块”的一些内容:

s0 = peg$currPos;
t0 = a;
t1 = b;
t2 = arr.slice();
t3 = shallowCopy(obj);
t4 = c;
// ...
if (s1 !== peg$FAILED) {
  // ...
} else {
  peg$currPos = s0;
  a = t0;
  b = t1;
  arr = t2;
  obj = t3;
  c = t4;
}

您如何看待这种能够:

  • 告诉 pegjs 哪些有状态变量将被操作修改。
  • 如果需要重置这些变量,请提供存储这些变量所需的代码。 (包括变量是原始值的简单情况的速记语法。)

你如何看待语法?

编辑:这是建议的语法语法(只是为了好玩):

diff --git a/src/parser.pegjs b/src/parser.pegjs
index 08f6c4f..09e079f 100644
--- a/src/parser.pegjs
+++ b/src/parser.pegjs
@@ -116,12 +116,31 @@ ChoiceExpression
     }

 ActionExpression
-  = expression:SequenceExpression code:(__ CodeBlock)? {
+  = expression:SequenceExpression code:((__ StateBlock)? __ CodeBlock)? {
       return code !== null
-        ? { type: "action", expression: expression, code: code[1] }
+        ? {
+            type:       "action",
+            expression: expression,
+            code:       code[2],
+            stateVars:  (code[0] !== null ? code[0][1] : [])
+          }
         : expression;
     }

+StateBlock "state block"
+  = "#STATE{" __ first:StateBlockItem rest:(__ "," __ StateBlockItem)* __ "}" {
+      return buildList(first, rest, 3);
+    }
+
+StateBlockItem
+  = varName:Identifier expression:(__ ":" __ CodeBlock)? {
+      return {
+        type:       "stateVar",
+        name:       varName,
+        expression: expression !== null ? expression[3] : varName
+      };
+    }
+
 SequenceExpression
   = first:LabeledExpression rest:(__ LabeledExpression)* {
       return rest.length > 0

嗨,大家好,
只是为了澄清一下,我是否正确,最好不要在基于缩进的语言中使用 PEG.js(从这个问题顶部开始的解决方法),直到这个问题关闭?
谢谢。

@hoho我不明白你的意思..但我后来找到了另一种解决方案来使用解析器组合器

我的意思是有解析缩进的变通方法,但评论说这些变通方法在某些情况下会失败。

让我澄清一下情况:在 PEG.js 中解析基于缩进的语言是可能的。 上面提到了各种解决方案,我刚刚创建了另一个解决方案,因为我试图对此有一种“感觉”(它是一种具有两个语句的简单语言的语法,其中一个可以包含缩进的子语句——类似于例如if在 Python 中)。

所有解决方案的共同点是它们需要手动跟踪缩进状态(因为 PEG.js 不能这样做)。 这意味着有两个限制:

  1. 您不能安全地编译带有缓存的语法(因为解析器可以使用缓存的结果而不是执行状态操作代码)。
  2. 您不能跨缩进级别回溯(因为当前无法在回溯时展开状态)。 换句话说,您无法解析一种语言,其中有两个有效构造,只有在换行和缩进级别更改后才能消除歧义。

在某些情况下,限制 1 可能会导致性能问题,但我认为没有多少语言会出现限制 2 的问题。

在 1.0.0 之前我对这种状态没问题,我计划在之后的某个时候回到这个话题。 第一级改进可能是使用更明确的状态跟踪(如上所述)或通过提供回溯钩子(以便人们可以正确展开状态)来摆脱限制 2。 第二级可以通过提供一些声明性的方式来摆脱手动跟踪缩进状态的需要。 这可能有助于限制 1。

H,我为支持正确回溯的 PEG.js 编写了一个(微小的、hacky 的)补丁,正如我在此处解释的: https :

抱歉磕磕碰碰😜

我只是想为我正在设计的语言创建 CSON 和 YAML 解析器,在寻找使用 PEG.js 创建基于缩进的解析器的方法时,我想出了一个简单的方法:

1) 不依赖于 push/pop 状态
2)通过动作中的代码断言缩进级别

我突然想到,上述 2 个解决方案中的任何一个实际上都会给生成的解析器增加性能问题。 另外在我看来:

1) 依赖状态不仅会增加丑陋的 PEG.js 语法,还会影响可以生成的解析器类型,因为它们需要支持基于动作的状态处理。
2) 有时在动作中添加一些代码会导致语言依赖规则,对于一些开发人员来说,这意味着他们不能使用插件来为其他语言(如 C 或 PHP)生成解析器,而无需借助更多插件来处理规则上的动作,这只是意味着更大的构建系统只是为了支持 1 或 2 个更改。

一段时间后,我开始创建自己的 PEG.js 解析器变体并思考:为什么不只使用增量(“++”)和减量(“--”)前缀运算符(__++ 表达式__ 和 __-- 表达式__ ) 来处理匹配表达式(__expression *__ 或 __expression +__)的结果。

以下是基于@dmajdaSimple Intation-based language的示例语法,重写后使用新的 __++ 表达式__ 和 __-- 表达式__ 而不是 __& { 谓词 }__:

Start
  = Statements

Statements
  = Statement*

Statement
  = Indent* statement:(S / I) { return statement; }

S
  = "S" EOS {
      return "S";
    }

I
  = "I" EOL ++Indent statements:Statements --Indent { return statements; }
  / "I" EOS { return []; }

Indent "indent"
  = "\t"
 / !__ "  "

__ "white space"
 = " \t"
 / " "

EOS
  = EOL
  / EOF

EOL
  = "\n"

EOF
  = !.

更赏心悦目,不是吗? 对于人类和软件来说也更容易理解。

它是如何工作的? 简单的:

1) Indent*告诉解析器我们想要Indent返回的 0 或更多
2) ++Indent告诉解析器增加Indent所需的最小匹配数
3) 现在任何时候解析器将要返回Indent的匹配项,它首先期望它比之前多 __1 个 __ 匹配项,否则会抛出 _peg$SyntaxError_。
4) --Indent告诉解析器减少Indent所需的最小匹配数
5) 现在任何时候解析器查找Indent并返回它期望 __1 less__ 匹配之前的匹配项,否则抛出 _peg$SyntaxError_。

此解决方案是添加对“重要空白解析”支持的最佳方式,无需向 PEG.js 语法添加难看的语法或阻止 3rd 方生成器。

这是在 _src/parser.pegjs_ 中添加对解析它的支持的更改规则:

{
  const OPS_TO_PREFIXED_TYPES = {
    "$": "text",
    "&": "simple_and",
    "!": "simple_not",
    "++": "increment_match",
    "--": "decrement_match"
  };
}

PrefixedOperator
  = "$"
  / "&"
  / "!"
  / "++"
  / "--"

SuffixedOperator
  = "?"
  / "*"
  / "+" !"+"

我是否正确假设要支持它编译器/生成器方面,我们将必须:

1) 添加编译器传递以确保 __++ 表达式__ 或 __-- 表达式__ 仅用于 __expression *__ 或 __expression +__,其中 __expression__ 必须是以下类型:choice、sequence 或 rule_ref
2) 在生成的解析器中为 __expression *__ 或 __expression +__ 添加一个基于缓存的检查,在返回匹配之前断言满足最低要求的匹配
3) 可选地为生成的解析器添加一个辅助方法,以实现返回给定规则所需的匹配数,例如。 nMatches( name: String ): Number

@futagoza ,这很干净而且很聪明。 我喜欢。 我正在研究处理状态的解析器,但我们真正需要的唯一状态是缩进级别。 我可能会使用这个想法,并为此称赞你。 跟踪缩进级别仍然有效地需要推送/弹出状态,因此它仍然可能会阻止一些优化,但其语义非常好。

如果您要向语法中添加运算符,我建议也添加 @ 前缀运算符。 它的目的是简单地从序列中提取单个规则结果。 使用它,示例语法变得更加清晰。 没有更多琐碎的 { return x } 操作。

Start
  = Statements

Statements
  = Statement*

Statement
  = Indent* @(S / I)

S
  = "S" EOS {
      return "S";
    }

I
  = "I" EOL ++Indent <strong i="8">@Statements</strong> --Indent
  / "I" EOS { return []; }

Indent "indent"
  = "\t"
 / !__ "  "

__ "white space"
 = " \t"
 / " "

EOS
  = EOL
  / EOF

EOL
  = "\n"

EOF
  = !.

@kodyjking你怎么看?

@futagoza你有启用缩进补丁和小样本语法的 fork/branch 吗?

我正在处理这个 fork/branch缩进

@krinye “我建议也添加@前缀运算符。它的目的是简单地从序列中提取单个规则结果”

你们中的一个人可以看看并发表评论或对修复进行公关。 谢谢 :)

自述文件:fork 更改

啊,没注意到警告:

我是否正确假设要支持它编译器/生成器方面,我们将必须:

  • 添加确保 ++ 表达式或 -- 表达式仅用于表达式 * 或表达式 + 的编译器传递,其中表达式的类型必须为:choice、sequence 或 rule_ref
  • 在生成的解析器中为表达式 * 或表达式 + 添加一个基于缓存的检查,在返回匹配之前断言满足最低要求的匹配
  • 可选地为生成的解析器添加一个辅助方法,以实现返回给定规则所需的匹配数,例如。 nMatches(名称:字符串):数字

只是为了踢球,我尝试将其添加到visitor.js

      increment_match: visitExpression,
      decrement_match: visitExpression,

现在我得到Invalid opcode: undefined.

@kristianmandrup关于从序列中提取单个值的 @ 运算符,我有一个只将该功能添加到 PegJS 的 fork,可在此处获得:

https://github.com/krisnye/pegjs

这是一个非常简单的添加。

@krisnye +1 用于基于语法的实现,很好很简单。 如果你不介意,我会把它添加到我的 PEG.js 变体中 😄

@kristianmandrup看到你对我的建议做出承诺

@futagoza请这样做。

我正在与一位同事讨论缩进逻辑,我们建议使用以下句法元素

// 增加命名状态变量
标识符++
// 递减命名状态变量
标识符--

// 重复常量或状态变量(如果变量尚未增加,则默认为零)
规则{整数 | 标识符}
// 使用常量或状态变量重复最小值/最大值
规则{整数 | 标识符,整数 | 标识符}

我们正在开发的解析器可以处理任意状态,但老实说,以上就是缩进解析所需的全部内容。

非常感谢伙计们! 如果它很容易做到,为什么不制作一个“专用”叉子,您提到的东西“可以正常工作”;)
干杯!

@克里斯尼

  1. 如果开发人员的意思是 __identifier+__,那么使用 __identifier++__ 很容易导致混乱的错误,这就是我选择使用 __++identifier__ 的原因,为了一致性,使用 __--identifier__
  2. 正如在关于范围的不同问题中提到的, rule{ STATE_REPEAT / RANGE }可能与rule{ ACTION }混淆,特别是如果您正在为 PEG.js 构建语法高亮,因此这种方法已被@dmajda拒绝

@kristianmandrup _(OFF TOPIC)_ 我擅长设计功能,有时也擅长实现,但在测试和基准测试方面很糟糕,所以通常我在我的电脑上的私人非 repo 目录中制作工作变体(没有任何测试或基准) ,然后忘记他们🤣。 对于我当前的 PEG.js 变体(名为 ePEG.js 😝,PEG.js 的扩展重写),我添加了这里提到的内容,以及其他功能(范围、导入、模板等),所以我添加测试和基准测试,但我目前也在做一个 C++ 项目,这需要我的时间,所以没有 ETA。

@futagoza谢谢伙伴 :) 查看功能扩展,但没有提到缩进支持。 这是否包括在内但没有记录或即将到来?

此列表中的其他人向我指出了我也可能会研究的其他解析器构建器/生成器解决方案。 随时关注我! 干杯!

@kristianmandrup据我所知,它还没有包括在内,但@dmajda 3 年前说他会在发布 PEG.js v1 后研究它,但据我所知,这不会再持续 2 年,除非他计划发布 PEG.js v0 的更多次要版本(_0.12_、_0.13_、_etc_)

我的意思是你是否已经在 ePEG 中或路线图中包含了缩进支持?

@kristianmandrup哦😆,它在路线图上。 我已经有一段时间没有更新 ePEG.js 存储库了,最近才决定将其完全重写为 PEG.js 而不是插件。

@futagoza同意 ++/-- 作为预操作,我忘记了动作语法,我们将其更改为 [min,max]

所以

++identifier
--identifier
rule[integer | identifier]
rule[integer | identifier, integer | identifier]

@krisnye [ ... ]用于字符类,参见https://github.com/pegjs/pegjs#characters

我在 ePEG.js 中所做的是添加范围(也在路线图上)以实现我认为您的描述:

space = [ \t]*
rule = range|expression
  • __expression__ 可以是以下任何一种:__++space__、__--space__ 或 __space__
  • __range__ 可以是 __ min.. __、__ min..max __、__ ..max __ 或 __ exact __
  • __min__、__max__ 或 __exact__ 只能是 __unsigned integer__
  • 使用带有 __expression__ 的 __range__(例如 2|expression)可以让我们设置 __rule__ 成功解析所需的 __expression__ 总量。
  • 将 __exact__ 范围与 __++expression__ 或 __--expression__ (例如 3|++expression)一起使用可以允许我们为 __++__ 或 __--__ 设置 __integer__ 数量,默认情况下为 __1__
  • 将 __min__ 或 __max__ 范围与 __++expression__ 或 __--expression__ 一起使用会引发语法错误。

我没有使用状态变量,因为那只会与规则标识符混淆。

使用 __range__、__++__、__--__ 或 __ @__ 的组合我希望创建 PEG 语法文件,其结果依赖于规则 __action__ 较少,这应该会增加空格的开发时间(例如缩进基于、ASCII 艺术等)语言,因为语言设计者和/或实现者不必担心尝试确认是否解析了正确数量的空格。
这也应该允许插件开发人员创建解析器生成器,这些生成器可以在优化我们的 __action__ 语言是什么(默认为 JavaScript,但插件可以将其更改为 CoffeeScript、PHP 等)的情况下进行优化。

所以今天似乎仍然不可能用 PEG.js 开箱即用地解析 Python,是吗?

如果没有,这是即将到来的事情吗? 是否有一组人们可以贡献的任务来完成这项工作?

我有一个项目,我希望能够在 JS 中获得 Python AST,对其进行修改,然后将其转换回具有相同格式的源代码,所以如果有明确的路线图。

@mindjuice还没有,计划在 1.0 之后发布。

如果您迫不及待,我的侄子和我创建了一个用 TypeScript 编写的解析器,它使用相同的语法并处理缩进。 它尚未记录在案,但它非常简单。 由于我们将其用作新语言设计的解析器,因此仍在进行中。

https://github.com/krisnye/pegs

您还可以尝试使用其他支持的解析器生成器,例如chevrotain

Python 缩进示例

我相信用 PEGjs 解析类似 python 的缩进是很有可能的。
下面的示例仅使用基于四个空格的缩进,但它可以扩展以涵盖任意间距的制表符和其他空白字符。
事实上,我正在研究的语言比 Python 有一个稍微复杂的缩进故事,这种语法很适合它。

{
    let prevIndentCount = 0;
    function print(...s) { console.log(...s); }
}

Indent 'indent'
    = i:("    "+) { 
        let currentIndentCount = i.toString().replace(/,/g, "").length;
        if (currentIndentCount === prevIndentCount + 4) { 
            // DEBUG //
            print("=== Indent ===");
            print("    current:"+currentIndentCount); 
            print("    previous:"+prevIndentCount);
            print("    lineNumber:"+location().start.line); 
            // DEBUG //
            prevIndentCount += 4;
            return "[indent]";
        }
        error("error: expected a 4-space indentation here!")
    } // 4 spaces 

Samedent 'samedent'
    = s:("    "+ / "") &{
        let currentIndentCount = s.toString().replace(/,/g, "").length;
        if (currentIndentCount === prevIndentCount) {
            print("=== Samedent ===");
            return true;
        }
        return false;
    }

Dedent 'dedent'
    = d:("    "+ / "") {
        let currentIndentCount = d.toString().replace(/,/g, "").length;
        if (currentIndentCount < prevIndentCount) {
            // DEBUG //
            print("=== Dedent ===");
            print("    current:"+currentIndentCount); 
            print("    previous:"+prevIndentCount);
            print("    lineNumber:"+location().start.line); 
            // DEBUG //
            prevIndentCount -= 4;
            return "[dedent]";
        }
        error("error: expected a 4-space dedentation here!");
    }

使用上面的语法,您可以创建这样的缩进块规则:

FunctionDeclaration 
    = 'function' _ Identifier _ FunctionParameterSection _ ":" _ FunctionBody

FunctionBody
    = Newline Indent FunctionSourceCode (Newline Samedent FunctionSourceCode)* Dedent 
此页面是否有帮助?
0 / 5 - 0 等级