【问题标题】:PEG for Python style indentation用于 Python 样式缩进的 PEG
【发布时间】:2011-05-11 11:09:22
【问题描述】:

您将如何在以下任何可以处理 Python/Haskell/CoffeScript 样式缩进的解析器生成器(PEG.jsCitrusTreetop)中编写 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 是一个简洁的小工具,但它对于这种解释来说太短了,IMO。另外,我不认为有人会发布一个(相当)简单的语法文件(嵌入了动作),能够解释您描述的这种语言,因为除了定义语法之外还涉及很多代码:遍历 AST,将数据保存在不同的作用域,解析作用域中的变量,如果在其中找不到某个变量,甚至可能弹出作用域。
  • P.S.你问问题的方式就好像你自己有答案一样。这是一个真正的问题,还是更多的谜题?如果这是一个真正的问题,我建议您尝试Language Implementation Patterns: Create Your Own Domain-Specific and General Programming Languages:它还解释了如何解释像 Python 这样的语言(至少是“缩进敏感”部分)。
  • 嗨,巴特,感谢您提供图书链接。不幸的是,我没有答案。我知道为上面示例中给出的语言创建解释器并非易事,但这不是我所期望的。我只对如何处理缩进部分/解析问题的部分感兴趣。事实上,我能够编写一个手写的解析器来跟踪缩进级别,但是我不知何故无法将这个概念映射到 PEG。任何帮助表示赞赏。马特

标签: parsing syntax language-design treetop peg


【解决方案1】:

纯 PEG 无法解析缩进。

但是 peg.js 可以。

我做了一个快速而肮脏的实验(受到 Ira Baxter 关于作弊的评论的启发)并编写了一个简单的标记器。

如需更完整的解决方案(完整的解析器),请查看此问题:Parse indentation level with 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",
   "",
   ""
]

这个分词器甚至可以捕捉到不好的缩进。

【讨论】:

  • 非常聪明!我花了一些时间来理解那里发生了什么,但我必须承认我并不完全理解如何扩展它来做任何有用的事情。如果您有时间,介意看看my question 吗?
  • 我现在很忙,不能投资超过几分钟。因此我只给你两个小提示: 1. 替换 s:text 行生产!假设您想要带有缩进的 JSON,然后执行 s:definition 和 definition = name "=" value 之类的操作。 2. 你得到一个像这样的数组:[ [ ...definition...], "INDENT", .... ]。遍历此数组并以递归形式对其进行转换。
  • 非常好的解决方案。我只想指出,这种类型的保存状态可能会失败如果(我相信只有当)你使用 PEG.js 的返回 null 的能力来指示解析器不应该匹配
  • 实际上,我收回了这一点。它与返回 null 表示失败无关。如果 peg 解析器在运行一个动作函数后必须回溯,它可能会导致你的缩进失败。当您有两个以相同方式开始的构造时,可能会发生这种情况
  • 嗯,这不只是一个分词器吗?您实际上可以一步将其解析为链表和字典,请参阅here
【解决方案2】:

所以我们在这里真正用缩进做的是创建类似 C 风格的块,这些块通常有自己的词法范围。如果我正在为这样的语言编写编译器,我想我会尝试让词法分析器跟踪缩进。每次缩进增加时,它都可以插入一个“{”标记。同样,每次它减少时,它都可以插入一个“}”标记。然后用显式花括号编写表达式语法来表示词法范围就变得更直接了。

【讨论】:

    【解决方案3】:

    我认为像这样的缩进敏感语言是上下文敏感的。我相信 PEG 只能做上下文无关的语言。

    请注意,虽然 nalply 的回答肯定是正确的,因为 PEG.js 可以通过外部状态(即可怕的全局变量)来实现,但它可能是一条危险的路径(比全局变量的常见问题更糟糕)。一些规则最初可以匹配(然后运行它们的操作),但父规则可能会失败,从而导致操作运行无效。如果在此类操作中更改了外部状态,则最终可能会出现无效状态。这非常可怕,可能导致震颤、呕吐和死亡。这里的 cmets 中有一些问题和解决方案:https://github.com/dmajda/pegjs/issues/45

    【讨论】:

    • 大多数解析器生成器工具最多只能使用上下文无关语言。 (LALR 工具只做上下文无关的子集!)。你为构建真正的解析器所做的就是在某个地方作弊。对 python/haskell 样式缩进的通常检查是使词法分析器从左边距开始计数空白,并为与前一行的左边距距离的每次变化插入 标记。有了这个技巧,缩进式语言现在很容易解析,或者至少不比通常的块结构语言差。
    • 大声笑,我尝试对自己的帖子投反对票(在我意识到这是我的帖子之前),因为 nalply 的回答更酷。
    【解决方案4】:

    您可以在 Treetop 中使用语义谓词来做到这一点。在这种情况下,您需要一个语义谓词来检测由于出现具有相同或更少缩进的另一行而关闭空白缩进块。谓词必须从开始行计算缩进,如果当前行的缩进以相同或更短的长度结束,则返回 true(块关闭)。因为结束条件是上下文相关的,所以它不能被记忆。 这是我将要添加到 Treetop 文档中的示例代码。请注意,我已经覆盖了 Treetop 的 SyntaxNode 检查方法,以便更轻松地可视化结果。

    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
    

    【讨论】:

      【解决方案5】:

      我知道这是一个旧线程,但我只是想在答案中添加一些 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"
      

      【讨论】:

        猜你喜欢
        • 2012-01-30
        • 1970-01-01
        • 1970-01-01
        • 2011-10-03
        • 1970-01-01
        • 2016-08-23
        • 2016-09-05
        • 2021-07-08
        • 1970-01-01
        相关资源
        最近更新 更多