В Parslet, как восстановить подстроки из поддерева разобрать?

Я пишу парсер для строк с интерполированными аргументами имя-значение, например: '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 для возврата подстроки?

2 ответа

Решение

Вот хак, с которым я закончил. Есть лучшие способы сделать это, но они потребуют более масштабных изменений. Parser#parse теперь возвращает Result, Result#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

Произведенное дерево основано на использовании as в вашем парсере.

Вы можете попробовать удалить их из чего-либо в выражении, чтобы получить совпадение с одной строкой для выражения. Похоже, это то, что вы после.

Если вы хотите проанализировать дерево и для этих выражений, то вам нужно либо:

  • Преобразуйте деревья выражений обратно в сопоставленный текст.
  • Пересмотрите сопоставленный текст обратно в дерево выражений.

Ни один из них не идеален, но если скорость не важна, я бы выбрал вариант повторного анализа. то есть. удалить as атомы, а затем, при необходимости, повторно анализировать выражения для деревьев.

Так как вы по праву хотите повторно использовать те же правила, но на этот раз вам нужно as захватывает по всем правилам, тогда вы могли бы реализовать это путем извлечения парсера из существующего парсера и реализации правил с одинаковыми именами в терминах rule :x { super.x.as(:x)}

ИЛИ ЖЕ

Вы можете иметь общее правило для выражения, которое соответствует всему выражению, не зная, что в нем.

например. "#{" >> (("}".absent >> any) | "\\}").repeat(0) >> "}"

Затем вы можете при необходимости анализировать каждое выражение в дереве. таким образом, вы не повторяете свои правила. Предполагается, что вы можете сказать, когда ваше выражение завершено, без разбора всего поддерева выражения.

Если это не удастся, то у нас останется взломанный петрушка.

У меня нет решения здесь, только некоторые подсказки.

В Parslet есть модуль CanFlatten, который реализует flatten и используется as преобразовать захваченное дерево обратно в одну строку. Вы захотите сделать что-то подобное.

В качестве альтернативы вам нужно изменить succ метод в Atom::Base возвращать "[success/fail, result, потребляется_upto_position]", чтобы каждое совпадение знало, где оно использовалось. Затем вы можете читать из источника между начальной и конечной позициями, чтобы получить необработанный текст обратно. current position источника в том месте, где парсер соответствует, должно быть значение, которое вы хотите.

Удачи.

Примечание: мой пример синтаксического анализатора выражений не обрабатывает экранирование escape-символа.. (оставлено как упражнение для читателя)

Другие вопросы по тегам