Class: Parslet::Transform

Inherits:
Object
  • Object
show all
Extended by:
Parslet
Includes:
Parslet
Defined in:
lib/parslet/transform.rb

Overview

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)
    StringLiteral.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.

Direct Known Subclasses

Expression::Treetop::Transform

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Parslet

any, dynamic, exp, included, infix_expression, match, scope, sequence, simple, str, subtree

Constructor Details

#initialize(raise_on_unmatch: false, &block) ⇒ Transform

Returns a new instance of Transform



143
144
145
146
147
148
149
150
# File 'lib/parslet/transform.rb', line 143

def initialize(raise_on_unmatch: false, &block) 
  @raise_on_unmatch = raise_on_unmatch
  @rules = []
  
  if block
    instance_eval(&block)
  end
end

Class Method Details

.inherited(subclass) ⇒ Object



137
138
139
140
# File 'lib/parslet/transform.rb', line 137

def inherited(subclass)
  super
  subclass.instance_variable_set(:@__transform_rules, rules.dup)
end

.rule(expression, &block) ⇒ Object

Define a rule for the transform subclass.



125
126
127
128
129
# File 'lib/parslet/transform.rb', line 125

def rule(expression, &block)
  @__transform_rules ||= []
  # Prepend new rules so they have higher precedence than older rules
  @__transform_rules.unshift([Parslet::Pattern.new(expression), block])
end

.rulesObject

Allows accessing the class' rules



133
134
135
# File 'lib/parslet/transform.rb', line 133

def rules 
  @__transform_rules ||= []
end

Instance Method Details

#apply(obj, context = nil) ⇒ Object

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.

Using the context parameter, you can inject bindings for the transformation. This can be used to allow access to the outside world from transform blocks, like so:

document = # some class that you act on
transform.apply(tree, document: document)

The above will make document available to all your action blocks:

# Variant A
rule(...) { document.foo(bar) }
# Variant B
rule(...) { |d| d[:document].foo(d[:bar]) }

Parameters:

  • obj

    PORO ast to transform

  • context (defaults to: nil)

    start context to inject into the bindings.



184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/parslet/transform.rb', line 184

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

#call_on_match(bindings, block) ⇒ Object

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] }


211
212
213
214
215
216
217
218
219
220
# File 'lib/parslet/transform.rb', line 211

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

#recurse_array(ary, ctx) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



260
261
262
# File 'lib/parslet/transform.rb', line 260

def recurse_array(ary, ctx) 
  ary.map { |elt| apply(elt, ctx) }
end

#recurse_hash(hsh, ctx) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



252
253
254
255
256
257
# File 'lib/parslet/transform.rb', line 252

def recurse_hash(hsh, ctx) 
  hsh.inject({}) do |new_hsh, (k,v)|
    new_hsh[k] = apply(v, ctx)
    new_hsh
  end
end

#rule(expression, &block) ⇒ Object

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*



158
159
160
161
# File 'lib/parslet/transform.rb', line 158

def rule(expression, &block)
  # Prepend new rules so they have higher precedence than older rules
  @rules.unshift([Parslet::Pattern.new(expression), block])
end

#rulesObject

Allow easy access to all rules, the ones defined in the instance and the ones predefined in a subclass definition.



225
226
227
# File 'lib/parslet/transform.rb', line 225

def rules 
  self.class.rules + @rules
end

#transform_elt(elt, context) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/parslet/transform.rb', line 231

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
  if @raise_on_unmatch && elt.is_a?(Hash)
    elt_types = elt.map do |key, value|
      [ key, value.class ]
    end.to_h
    raise NotImplementedError, "Failed to match `#{elt_types.inspect}`"
  else
    return elt
  end
end