Python风格缩进的PEG

35

如何在以下任意一种解析器生成器(PEG.js, Citrus, Treetop)中编写一个可以处理Python/Haskell/CoffeScript风格缩进的Parsing Expression Grammar

尚不存在的编程语言示例:

square x =
    x * x

cube x =
    x * square x

fib n =
  if n <= 1
    0
  else
    fib(n - 2) + fib(n - 1) # some cheating allowed here with brackets

更新: 不要试图为上面的示例编写解释器。我只关心缩进问题。另一个示例可能是解析以下内容:

foo
  bar = 1
  baz = 2
tap
  zap = 3

# should yield (ruby style hashmap):
# {:foo => { :bar => 1, :baz => 2}, :tap => { :zap => 3 } }

我不熟悉Citrus和Treetop,但是我认为PEG.js虽然是一个很好的小工具,但对于这种类型的解释来说还是有些不足。此外,我不认为有人会发布一个(相当)简单的语法文件(带有嵌入式操作),能够解释你所描述的这种语言,因为除了定义语法之外,还涉及到相当多的代码:遍历AST,在不同的作用域中保存数据,在作用域中解析变量,甚至可能弹出作用域,如果某个变量在其中找不到的话。 - Bart Kiers
2
请注意,您提出问题时似乎已经知道答案。这是一个真正的问题还是更像是谜题?如果这是一个真正的问题,我建议您尝试阅读《语言实现模式:创建自己的领域特定语言和通用编程语言》(http://www.pragprog.com/titles/tpdsl/language-implementation-patterns)。它也解释了Python这样的语言如何被解释(至少是“缩进敏感”的部分)。 - Bart Kiers
嗨Bart,感谢书籍链接。不幸的是我没有答案。我知道像上面例子中所给出的语言创建解释器并不容易,但这不是我在这里期望的。我只对如何处理缩进部分/解析问题感兴趣。事实上,我能够编写手写解析器来跟踪缩进级别,但我无法将该概念映射到PEGs上。任何帮助都将不胜感激。Matt - Matt
5个回答

23

纯 PEG 无法解析缩进。

但是 peg.js 可以。

我进行了一个简单而快速的实验(受到 Ira Baxter 的评论“作弊”的启发),并编写了一个简单的标记器。

对于更完整的解决方案(完整的解析器),请参阅此问题:使用 PEG.js 解析缩进级别

/* Initializations */
{
  function start(first, tail) {
    var done = [first[1]];
    for (var i = 0; i < tail.length; i++) {
      done = done.concat(tail[i][1][0])
      done.push(tail[i][1][1]);
    }
    return done;
  }

  var depths = [0];

  function indent(s) {
    var depth = s.length;

    if (depth == depths[0]) return [];

    if (depth > depths[0]) {
      depths.unshift(depth);
      return ["INDENT"];
    }

    var dents = [];
    while (depth < depths[0]) {
      depths.shift();
      dents.push("DEDENT");
    }

    if (depth != depths[0]) dents.push("BADDENT");

    return dents;
  }
}

/* The real grammar */
start   = first:line tail:(newline line)* newline? { return start(first, tail) }
line    = depth:indent s:text                      { return [depth, s] }
indent  = s:" "*                                   { return indent(s) }
text    = c:[^\n]*                                 { return c.join("") }
newline = "\n"                                     {}

depths 是一个缩进堆栈。 indent() 返回缩进标记的数组,start() 解开数组,使解析器的行为类似于流。

peg.js 生成以下文本:

alpha
  beta
  gamma
    delta
epsilon
    zeta
  eta
theta
  iota

这些结果:

[
   "alpha",
   "INDENT",
   "beta",
   "gamma",
   "INDENT",
   "delta",
   "DEDENT",
   "DEDENT",
   "epsilon",
   "INDENT",
   "zeta",
   "DEDENT",
   "BADDENT",
   "eta",
   "theta",
   "INDENT",
   "iota",
   "DEDENT",
   "",
   ""
]

这个分词器甚至能捕捉到不良缩进。


1
非常聪明! 花了我一些时间才理解发生了什么,但我必须承认我并不完全明白如何将其扩展以执行任何有用的操作。 如果您有几分钟时间,是否介意看一下我的问题 - Trevor Dixon
1
我现在非常忙,无法投入超过几分钟的时间。因此,我只给你两个小提示:1. 在生产线中替换s:text!假设您想要带有缩进的JSON,则可以执行类似于s:definition和definition = name“=”value的操作。2. 您会得到一个像这样的数组:[[...定义...],“INDENT”,....]。遍历该数组并将其转换为递归形式。 - nalply
非常好的解决方案。我只想指出,如果(我相信只有如果)您使用PEG.js的返回null的能力来指示解析器不匹配,那么这种保存状态的类型可能会失败。 - B T
1
实际上,我撤回了之前的说法。这与返回null来表示失败无关。如果Peg解析器在运行操作函数后需要回溯,它可能会导致缩进失败。当您有两个以相同方式开头的结构时,就可能发生这种情况。 - B T
嗯,这不仅仅是一个分词器吗?你实际上可以一步将它解析为链接的列表和字典,参见[此处](https://dev59.com/WGgu5IYBdhLWcg3wASaO#11700527)。 - Nils Lindemann
@Nils,你是对的。我编辑了我的回答。谢谢你指出来。 - nalply

8
我认为类似缩进敏感的语言是上下文敏感的。我相信 PEG 只能处理上下文无关的语言。
需要注意的是,尽管 nalply 的回答肯定是正确的,即 PEG.js 可以通过外部状态(即可怕的全局变量)来实现,但这可能是一条危险的道路(比通常问题更糟糕)。有些规则可能会最初匹配(然后运行它们的操作),但父规则可能会失败,从而导致运行的操作无效。如果在此类操作中更改了外部状态,则可能会出现无效状态。这非常糟糕,可能会导致颤抖、呕吐和死亡。这里的评论中提到了一些问题和解决方案:https://github.com/dmajda/pegjs/issues/45

16
大多数解析器生成工具最多只能处理上下文无关语言(CFG)。LALR 工具只能处理 CFG 的子集!为了构建真正的解析器,您需要在某个地方做手脚。对于 Python/Haskell 风格的缩进检查,通常是让词法分析器从左边界计算空格,并在左边距离与前一行不同时插入 <INDENT> 或 <DEDENT> 令牌。使用这种技巧,缩进风格的语言现在变得非常容易解析,或者至少不比具有块结构的普通语言更糟糕。 - Ira Baxter
3
哈哈,我试图给自己的帖子点踩(当然在意识到这是我的帖子之前),因为 nalply 的回答太棒了。 - B T

7

所以,我们在这里实际上使用缩进创建了类似于C风格块的东西,它们通常具有自己的词法作用域。如果我要为这样的语言编写编译器,我认为我会尝试让词法分析器跟踪缩进。每次缩进增加时,它都可以插入'{'标记。同样,每当缩进减少时,它都可以插入'}'标记。然后,使用显式花括号编写表达式语法来表示词法作用域变得更加直观。


2
您可以使用Treetop中的语义谓词来实现此操作。在这种情况下,您需要一个语义谓词来检测由于出现了具有相同或更少缩进的另一行而关闭缩进的空格块。该谓词必须从开头行开始计算缩进,并且如果当前行的缩进已经达到相同或更短的长度,则返回true(块已关闭)。由于关闭条件是上下文相关的,因此不能进行记忆化。以下是我即将添加到Treetop文档中的示例代码。请注意,我已经覆盖了Treetop的SyntaxNode inspect方法,以使结果更容易可视化。
grammar IndentedBlocks
  rule top
    # Initialise the indent stack with a sentinel:
    &{|s| @indents = [-1] }
    nested_blocks
    {
      def inspect
        nested_blocks.inspect
      end
    }
  end

  rule nested_blocks
    (
      # Do not try to extract this semantic predicate into a new rule.
      # It will be memo-ized incorrectly because @indents.last will change.
      !{|s|
        # Peek at the following indentation:
        save = index; i = _nt_indentation; index = save
        # We're closing if the indentation is less or the same as our enclosing block's:
        closing = i.text_value.length <= @indents.last
      }
      block
    )*
    {
      def inspect
        elements.map{|e| e.block.inspect}*"\n"
      end
    }
  end

 rule block
    indented_line       # The block's opening line
    &{|s|               # Push the indent level to the stack
      level = s[0].indentation.text_value.length
      @indents << level
      true
    }
    nested_blocks       # Parse any nested blocks
    &{|s|               # Pop the indent stack
      # Note that under no circumstances should "nested_blocks" fail, or the stack will be mis-aligned
      @indents.pop
      true
    }
    {
      def inspect
        indented_line.inspect +
          (nested_blocks.elements.size > 0 ? (
            "\n{\n" +
            nested_blocks.elements.map { |content|
              content.block.inspect+"\n"
            }*'' +
            "}"
          )
          : "")
      end
    }
  end

  rule indented_line
    indentation text:((!"\n" .)*) "\n"
    {
      def inspect
        text.text_value
      end
    }
  end

  rule indentation
    ' '*
  end
end

这是一个小型的测试驱动程序,让您可以轻松尝试它:
require 'polyglot'
require 'treetop'
require 'indented_blocks'

parser = IndentedBlocksParser.new

input = <<END
def foo
  here is some indented text
    here it's further indented
    and here the same
      but here it's further again
      and some more like that
    before going back to here
      down again
  back twice
and start from the beginning again
  with only a small block this time
END 

parse_tree = parser.parse input

p parse_tree

0

我知道这是一个老帖子,但是我想在答案中添加一些 PEGjs 代码。该代码将解析文本并将其“嵌套”到一种“AST-ish”结构中。它只深入一层,看起来很丑陋,而且它没有真正使用返回值来创建正确的结构,但是它会保留您的语法的内存树,并在最后返回它。这可能会变得难以控制并导致一些性能问题,但至少它能够完成预期的功能。

注意:请确保使用制表符而非空格!

{ 
    var indentStack = [], 
        rootScope = { 
            value: "PROGRAM",
            values: [], 
            scopes: [] 
        };

    function addToRootScope(text) {
        // Here we wiggle with the form and append the new
        // scope to the rootScope.

        if (!text) return;

        if (indentStack.length === 0) {
            rootScope.scopes.unshift({
                text: text,
                statements: []
            });
        }
        else {
            rootScope.scopes[0].statements.push(text);
        }
    }
}

/* Add some grammar */

start
  = lines: (line EOL+)*
    { 
        return rootScope;
    }


line
  = line: (samedent t:text { addToRootScope(t); }) &EOL
  / line: (indent t:text { addToRootScope(t); }) &EOL
  / line: (dedent t:text { addToRootScope(t); }) &EOL
  / line: [ \t]* &EOL
  / EOF

samedent
  = i:[\t]* &{ return i.length === indentStack.length; }
    {
        console.log("s:", i.length, " level:", indentStack.length);
    }

indent
  = i:[\t]+ &{ return i.length > indentStack.length; }
    {
        indentStack.push(""); 
        console.log("i:", i.length, " level:", indentStack.length);
    }

dedent
    = i:[\t]* &{ return i.length < indentStack.length; }
      {
          for (var j = 0; j < i.length + 1; j++) {
              indentStack.pop();
          } 
          console.log("d:", i.length + 1, " level:", indentStack.length);  
      }

text
    = numbers: number+  { return numbers.join(""); } 
    / txt: character+   { return txt.join(""); }


number
    = $[0-9] 

character 
    = $[ a-zA-Z->+]  
__
    = [ ]+

_ 
    = [ ]*

EOF 
    = !.

EOL
    = "\r\n" 
    / "\n" 
    / "\r"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接