在 Parslet 中,如何从解析子树中重建子字符串?

in Parslet, how to reconstruct substrings from parse subtrees?

我正在为带有内插名称-值参数的字符串编写解析器,例如:'This sentence #{x: 2, y: (2 + 5) + 3} has stuff in it.'参数值是代码,它有自己的一组解析规则。

这是我的解析器的一个版本,经过简化,只允许将基本算术作为代码:

require 'parslet'
require 'ap'
class TestParser < Parslet::Parser
  rule :integer do match('[0-9]').repeat(1).as :integer end
  rule :space do match('[\s\n]').repeat(1) end
  rule :parens do str('(') >> code >> str(')') end
  rule :operand do integer | parens end
  rule :addition do (operand.as(:left) >> space >> str('+') >> space >> operand.as(:right)).as :addition end
  rule :code do addition | operand end
  rule :name do match('[a-z]').repeat 1 end
  rule :argument do name.as(:name) >> str(':') >> space >> code.as(:value) end
  rule :arguments do argument >> (str(',') >> space >> argument).repeat end
  rule :interpolation do str('#{') >> arguments.as(:arguments) >> str('}') end
  rule :text do (interpolation.absent? >> any).repeat(1).as(:text) end
  rule :segments do (interpolation | text).repeat end
  root :segments
end
string = 'This sentence #{x: 2, y: (2 + 5) + 3} has stuff in it.'
ap TestParser.new.parse(string), index: false

由于代码有自己的解析规则(以确保语法有效),参数值被解析到子树中(括号等替换为子树中的嵌套):

[
    {
        :text => "This sentence "@0
    },
    {
        :arguments => [
            {
                 :name => "x"@16,
                :value => {
                    :integer => "2"@19
                }
            },
            {
                 :name => "y"@22,
                :value => {
                    :addition => {
                         :left => {
                            :addition => {
                                 :left => {
                                    :integer => "2"@26
                                },
                                :right => {
                                    :integer => "5"@30
                                }
                            }
                        },
                        :right => {
                            :integer => "3"@35
                        }
                    }
                }
            }
        ]
    },
    {
        :text => " has stuff in it."@37
    }
]

但是,我想将参数值存储为字符串,所以这将是理想的结果:

[
    {
        :text => "This sentence "@0
    },
    {
        :arguments => [
            {
                 :name => "x"@16,
                :value => "2"
            },
            {
                 :name => "y"@22,
                :value => "(2 + 5) + 3"
            }
        ]
    },
    {
        :text => " has stuff in it."@37
    }
]

如何使用 Parslet 子树重建参数值子字符串?我可以编写一个代码生成器,但这似乎有点过分了——Parslet 显然可以在某些时候访问子字符串位置信息(尽管它可能会丢弃它)。

是否可以利用或破解 Parslet 来 return 子字符串?

生成的树是基于在您的解析器中使用 as

您可以尝试从表达式中的任何内容中删除它们,以便获得表达式的单个字符串匹配项。这似乎就是你所追求的。

如果你也想要这些表达式的解析树,那么你需要:

  • 将表达式树转换回匹配的文本。
  • 将匹配的文本重新解析回表达式树。

这些都不是理想的,但如果速度不是最重要的,我会选择重新解析选项。 IE。删除 as 个原子,然后根据需要将表达式重新解析为树。

因为您确实想重用相同的规则,但这次您需要 as 捕获整个规则,那么您可以通过从现有解析器派生解析器并实现具有相同名称的规则来实现这一点在 rule :x { super.x.as(:x)}

方面

您可以有一个通用的表达式规则来匹配整个表达式而不知道其中的内容。

例如。 "#{" >> (("}".absent >> any) | "\}").repeat(0) >> "}"

然后您可以根据需要将每个表达式解析为一棵树。这样你就不会重复你的规则。它假设您可以在不解析整个表达式子树的情况下判断表达式何时完成。

如果做不到这一点,我们就只能破解 parslet。

我这里没有解决方案,只有一些提示。

Parslet 有一个名为 "CanFlatten" 的模块,它实现了 flatten 并被 as 用于将捕获的树转换回单个字符串。你会想要做这样的事情。

或者您需要将 Atom::Base 中的 succ 方法更改为 return "[success/fail, result, consumed_upto_position]" 这样每个匹配项都知道它消耗到哪里。然后你可以从开始位置和结束位置之间的源代码中读取原始文本。解析器匹配的源的 current position 应该是你想要的值。

祝你好运。

注意:我的示例表达式解析器不处理转义字符的转义。(留作 reader 的练习)

这是我最终得到的技巧。有更好的方法可以实现这一点,但它们需要进行更广泛的更改。 Parser#parse 现在 returns 一个 ResultResult#tree 给出了正常的解析结果,而 Result#strings 是一个将子树结构映射到源字符串的哈希。

module Parslet

  class Parser
    class Result < Struct.new(:tree, :strings); end
    def parse(source, *args)
      source = Source.new(source) unless source.is_a? Source
      value = super source, *args 
      Result.new value, source.value_strings
    end
  end

  class Source
    prepend Module.new{
      attr_reader :value_strings
      def initialize(*args)
        super *args
        @value_strings = {}
      end
    }
  end

  class Atoms::Base
    prepend Module.new{
      def apply(source, *args)
        old_pos = source.bytepos
        super.tap do |success, value|
          next unless success
          string = source.instance_variable_get(:@str).string.slice(old_pos ... source.bytepos)
          source.value_strings[flatten(value)] = string
        end
      end    
    }
  end

end