kschiess / parslet

A small PEG based parser library. See the Hacking page in the Wiki as well.
kschiess.github.com/parslet
MIT License
809 stars 95 forks source link

Composable transfoms #129

Closed ravinggenius closed 9 years ago

ravinggenius commented 9 years ago

Small parsers can be composed into larger parsers:

module NumberParser
  include Parslet

  rule(:number) { match['[0-9]'].repeat.as(:number) }
end

module StringParser
  include Parslet

  rule(:string) { match['[a-z]'].repeat.as(:string) }
end

class Parser < Parslet::Parser
  include NumberParser
  include StringParser
end

I can test Parser as a complete parser, but I can also test NumberParser and StringParser in isolation.


Is there a way to structure transformers so smaller transformers can be composed into a larger transformer? I haven't seen an example of this anywhere.

kschiess commented 9 years ago

I recall vaguely that you should be able to do either of these:

Mixing transformers into transformers wont work, transform rules aren't methods, unfortunately.Since transformer execution is highly order dependent, I am not sure this would even be a good idea.

ravinggenius commented 9 years ago

I ended up defining the rules in classes and just copying the rules as needed:

# syntax_tree/transforms/rational.rb
require 'parslet'

module SyntaxTree::Transforms
  class Rational < Parslet::Transform
    rule(integer: simple(:integer)) do |locals|
      SyntaxTree::Nodes::Rational.new(locals[:integer])
    end

    rule(integer: simple(:integer), sign: simple(:sign)) do |locals|
      SyntaxTree::Nodes::Rational.new(locals[:integer], sign: locals[:sign])
    end

    rule(location: simple(:location), integer: simple(:integer)) do |locals|
      SyntaxTree::Nodes::Rational.new(locals[:integer], location: locals[:location])
    end

    rule(location: simple(:location), integer: simple(:integer), sign: simple(:sign)) do |locals|
      SyntaxTree::Nodes::Rational.new(locals[:integer], location: locals[:location], sign: locals[:sign])
    end
  end
end

# syntax_tree/syntax_tree.rb
require 'parslet'

require_relative 'nodes'
require_relative 'transforms'

module SyntaxTree
  class SyntaxTree < Parslet::Transform
    attr_reader :parse_tree

    def initialize(parse_tree)
      @parse_tree = parse_tree
    end

    def syntax_tree
      apply(parse_tree)
    end

    def self.rules
      [
        SyntaxTree::Transforms::Rational
      ].map(&:rules).inject(&:+)
    end
  end
end

Now I can reason and test SyntaxTree::Transforms::Rational in isolation. I can also write integration tests for SyntaxTree::SyntaxTree.

Are there any hidden gotchas I should look out for with this approach? It seems to work fine for what I need.

rubydesign commented 9 years ago

Well thanks for sharing. I do hope this works. I have my parser in 7 files, but just 1 for the transformation. Will try (in due time)