require 'parslet/pattern' # Transforms an expression tree into something else. The transformation # performs a depth-first, post-order traversal of the expression tree. During # that traversal, each time a rule matches a node, the node is replaced by the # result of the block associated to the rule. Otherwise the node is accepted # as is into the result tree. # # This is almost what you would generally do with a tree visitor, except that # you can match several levels of the tree at once. # # As a consequence of this, the resulting tree will contain pieces of the # original tree and new pieces. Most likely, you will want to transform the # original tree wholly, so this isn't a problem. # # You will not be able to create a loop, given that each node will be replaced # only once and then left alone. This means that the results of a replacement # will not be acted upon. # # Example: # # class Example < Parslet::Transform # rule(:string => simple(:x)) { # (1) # StringConstant.new(x) # } # end # # A tree transform (Parslet::Transform) is defined by a set of rules. Each # rule can be defined by calling #rule with the pattern as argument. The block # given will be called every time the rule matches somewhere in the tree given # to #apply. It is passed a Hash containing all the variable bindings of this # pattern match. # # In the above example, (1) illustrates a simple matching rule. # # Let's say you want to parse matching parentheses and distill a maximum nest # depth. You would probably write a parser like the one in example/parens.rb; # here's the relevant part: # # rule(:balanced) { # str('(').as(:l) >> balanced.maybe.as(:m) >> str(')').as(:r) # } # # If you now apply this to a string like '(())', you get a intermediate parse # tree that looks like this: # # { # l: '(', # m: { # l: '(', # m: nil, # r: ')' # }, # r: ')' # } # # This parse tree is good for debugging, but what we would really like to have # is just the nesting depth. This transformation rule will produce that: # # rule(:l => '(', :m => simple(:x), :r => ')') { # # innermost :m will contain nil # x.nil? ? 1 : x+1 # } # # = Usage patterns # # There are four ways of using this class. The first one is very much # recommended, followed by the second one for generality. The other ones are # omitted here. # # Recommended usage is as follows: # # class MyTransformator < Parslet::Transform # rule(...) { ... } # rule(...) { ... } # # ... # end # MyTransformator.new.apply(tree) # # Alternatively, you can use the Transform class as follows: # # transform = Parslet::Transform.new do # rule(...) { ... } # end # transform.apply(tree) # # = Execution context # # The execution context of action blocks differs depending on the arity of # said blocks. This can be confusing. It is however somewhat intentional. You # should not create fat Transform descendants containing a lot of helper methods, # instead keep your AST class construction in global scope or make it available # through a factory. The following piece of code illustrates usage of global # scope: # # transform = Parslet::Transform.new do # rule(...) { AstNode.new(a_variable) } # rule(...) { Ast.node(a_variable) } # modules are nice # end # transform.apply(tree) # # And here's how you would use a class builder (a factory): # # transform = Parslet::Transform.new do # rule(...) { builder.add_node(a_variable) } # rule(...) { |d| d[:builder].add_node(d[:a_variable]) } # end # transform.apply(tree, :builder => Builder.new) # # As you can see, Transform allows you to inject local context for your rule # action blocks to use. # class Parslet::Transform # FIXME: Maybe only part of it? Or maybe only include into constructor # context? include Parslet class << self # FIXME: Only do this for subclasses? include Parslet # Define a rule for the transform subclass. # def rule(expression, &block) @__transform_rules ||= [] @__transform_rules << [Parslet::Pattern.new(expression), block] end # Allows accessing the class' rules # def rules @__transform_rules || [] end end def initialize(&block) @rules = [] if block instance_eval(&block) end end # Defines a rule to be applied whenever apply is called on a tree. A rule # is composed of two parts: # # * an *expression pattern* # * a *transformation block* # def rule(expression, &block) @rules << [ Parslet::Pattern.new(expression), block ] end # Applies the transformation to a tree that is generated by Parslet::Parser # or a simple parslet. Transformation will proceed down the tree, replacing # parts/all of it with new objects. The resulting object will be returned. # def apply(obj, context=nil) transform_elt( case obj when Hash recurse_hash(obj, context) when Array recurse_array(obj, context) else obj end, context ) end # Executes the block on the bindings obtained by Pattern#match, if such a match # can be made. Depending on the arity of the given block, it is called in # one of two environments: the current one or a clean toplevel environment. # # If you would like the current environment preserved, please use the # arity 1 variant of the block. Alternatively, you can inject a context object # and call methods on it (think :ctx => self). # # # the local variable a is simulated # t.call_on_match(:a => :b) { a } # # no change of environment here # t.call_on_match(:a => :b) { |d| d[:a] } # def call_on_match(bindings, block) if block if block.arity == 1 return block.call(bindings) else context = Context.new(bindings) return context.instance_eval(&block) end end end # Allow easy access to all rules, the ones defined in the instance and the # ones predefined in a subclass definition. # def rules self.class.rules + @rules end # @api private # def transform_elt(elt, context) rules.each do |pattern, block| if bindings=pattern.match(elt, context) # Produces transformed value return call_on_match(bindings, block) end end # No rule matched - element is not transformed return elt end # @api private # def recurse_hash(hsh, ctx) hsh.inject({}) do |new_hsh, (k,v)| new_hsh[k] = apply(v, ctx) new_hsh end end # @api private # def recurse_array(ary, ctx) ary.map { |elt| apply(elt, ctx) } end end require 'parslet/context'