Skip to content

Commit

Permalink
allow to call transform's method from the given block
Browse files Browse the repository at this point in the history
  • Loading branch information
taichi-ishitani committed Oct 5, 2023
1 parent f61daf0 commit 0ec7ab2
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 90 deletions.
33 changes: 29 additions & 4 deletions lib/parslet/context.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,46 @@
# Provides a context for tree transformations to run in. The context allows
# accessing each of the bindings in the bindings hash as local method.
#
# Example:
# Example:
#
# ctx = Context.new(:a => :b)
# ctx.instance_eval do
# ctx.instance_eval do
# a # => :b
# end
#
# @api private
class Parslet::Context
include Parslet

def initialize(bindings)
def initialize(bindings, transform = nil)
@__transform = transform if transform
bindings.each do |key, value|
singleton_class.send(:define_method, key) { value }
instance_variable_set("@#{key}", value)
end
end
end

def respond_to_missing?(method, include_private)
super || @__transform&.respond_to?(method, true) || false
end

if RUBY_VERSION >= '3'
def method_missing(method, *args, **kwargs, &block)
if @__transform&.respond_to?(method, true)
@__transform.__send__(method, *args, **kwargs, &block)
else
super
end
end
else
def method_missing(method, *args, &block)
if @__transform&.respond_to?(method, true)
@__transform.__send__(method, *args, &block)
else
super
end
end

ruby2_keywords :method_missing
end
end
124 changes: 62 additions & 62 deletions lib/parslet/transform.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
# 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.
# 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.
# will not be acted upon.
#
# Example:
# Example:
#
# class Example < Parslet::Transform
# rule(:string => simple(:x)) { # (1)
Expand All @@ -30,35 +30,35 @@
# 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.
# 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:
# 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:
# tree that looks like this:
#
# {
# l: '(',
# l: '(',
# m: {
# l: '(',
# m: nil,
# r: ')'
# },
# r: ')'
# 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:
# is just the nesting depth. This transformation rule will produce that:
#
# rule(:l => '(', :m => simple(:x), :r => ')') {
# rule(:l => '(', :m => simple(:x), :r => ')') {
# # innermost :m will contain nil
# x.nil? ? 1 : x+1
# }
Expand All @@ -67,9 +67,9 @@
#
# 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.
# omitted here.
#
# Recommended usage is as follows:
# Recommended usage is as follows:
#
# class MyTransformator < Parslet::Transform
# rule(...) { ... }
Expand All @@ -78,7 +78,7 @@
# end
# MyTransformator.new.apply(tree)
#
# Alternatively, you can use the Transform class as follows:
# Alternatively, you can use the Transform class as follows:
#
# transform = Parslet::Transform.new do
# rule(...) { ... }
Expand All @@ -87,12 +87,12 @@
#
# = 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,
# 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:
# scope:
#
# transform = Parslet::Transform.new do
# rule(...) { AstNode.new(a_variable) }
Expand All @@ -109,28 +109,28 @@
# transform.apply(tree, :builder => Builder.new)
#
# As you can see, Transform allows you to inject local context for your rule
# action blocks to use.
# action blocks to use.
#
class Parslet::Transform
# FIXME: Maybe only part of it? Or maybe only include into constructor
# context?
include Parslet
include Parslet

class << self
# FIXME: Only do this for subclasses?
include Parslet
# Define a rule for the transform subclass.

# Define a rule for the transform subclass.
#
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

# Allows accessing the class' rules
#
def rules
def rules
@__transform_rules ||= []
end

Expand All @@ -139,47 +139,47 @@ def inherited(subclass)
subclass.instance_variable_set(:@__transform_rules, rules.dup)
end
end
def initialize(raise_on_unmatch=false, &block)

def initialize(raise_on_unmatch=false, &block)
@raise_on_unmatch = raise_on_unmatch
@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:
#
# is composed of two parts:
#
# * an *expression pattern*
# * a *transformation block*
#
def rule(expression, &block)
# Prepend new rules so they have higher precedence than older rules
@rules.unshift([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.
# 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:
#
# 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]) }
#
# @param obj PORO ast to transform
# @param context start context to inject into the bindings.
# @param context start context to inject into the bindings.
#
def apply(obj, context=nil)
transform_elt(
Expand All @@ -190,52 +190,52 @@ def apply(obj, context=nil)
recurse_array(obj, context)
else
obj
end,
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
# 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
# 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 }
# 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)
return instance_exec(bindings, &block)
else
context = Context.new(bindings)
context = Context.new(bindings, self)
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.

# Allow easy access to all rules, the ones defined in the instance and the
# ones predefined in a subclass definition.
#
def rules
def rules
self.class.rules + @rules
end
# @api private

# @api private
#
def transform_elt(elt, context)
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|
Expand All @@ -247,19 +247,19 @@ def transform_elt(elt, context)
end
end

# @api private
# @api private
#
def recurse_hash(hsh, ctx)
def recurse_hash(hsh, ctx)
hsh.inject({}) do |new_hsh, (k,v)|
new_hsh[k] = apply(v, ctx)
new_hsh
end
end
# @api private
# @api private
#
def recurse_array(ary, ctx)
def recurse_array(ary, ctx)
ary.map { |elt| apply(elt, ctx) }
end
end

require 'parslet/context'
require 'parslet/context'
22 changes: 18 additions & 4 deletions spec/parslet/transform/context_spec.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
require 'spec_helper'

describe Parslet::Context do
def context(*args)
described_class.new(*args)
let(:transform) do
flexmock('transform')
end


def context(bindings)
described_class.new(bindings, transform)
end

it "binds hash keys as variable like things" do
context(:a => 'value').instance_eval { a }.
should == 'value'
end
it "responds transform's methods" do
transform.should_receive(:foo).and_return { :foo }
transform.should_receive(:bar).and_return { :bar }

c = context(:a => 'value')
assert c.respond_to?(:foo)
c.foo.should == :foo
assert c.respond_to?(:bar)
c.bar.should == :bar
end
it "one contexts variables aren't the next ones" do
ca = context(:a => 'b')
cb = context(:b => 'c')
Expand Down Expand Up @@ -53,4 +67,4 @@ def foo
end
end
end
end
end
Loading

0 comments on commit 0ec7ab2

Please sign in to comment.