sexp_builder 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +22 -0
- data/README.rdoc +259 -0
- data/examples/andand.rb +67 -0
- data/lib/sexp_builder.rb +100 -0
- data/lib/sexp_builder/context.rb +168 -0
- data/lib/sexp_builder/query_builder.rb +65 -0
- metadata +69 -0
data/COPYING
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2009 Magnus Holm
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without restriction,
|
6
|
+
including without limitation the rights to use, copy, modify, merge,
|
7
|
+
publish, distribute, sublicense, and/or sell copies of the Software,
|
8
|
+
and to permit persons to whom the Software is furnished to do so,
|
9
|
+
subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
15
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
16
|
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
17
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
18
|
+
SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
19
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
|
20
|
+
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
+
SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
= SexpBuilder
|
2
|
+
|
3
|
+
SexpBuilder is an alternative to SexpProcessor which allows you to match and
|
4
|
+
rewrite S-expressions based on recursive descent SexpPaths. You probably want
|
5
|
+
to read http://github.com/adamsanderson/sexp_path before you proceed.
|
6
|
+
|
7
|
+
SexpBuilder works on any S-expressions, but all of these examples uses Ruby's
|
8
|
+
S-expressions as given from ParseTree and RubyParser.
|
9
|
+
|
10
|
+
== Synopsis
|
11
|
+
|
12
|
+
# Inherit SexpBuilder:
|
13
|
+
class Andand < SexpBuilder
|
14
|
+
|
15
|
+
## Rules
|
16
|
+
#
|
17
|
+
# Rules are simply snippets of SexpPath which can refer to each other
|
18
|
+
# and itself. They are basically method definition, so they can take
|
19
|
+
# arguments too.
|
20
|
+
|
21
|
+
# This matches foo.andand:
|
22
|
+
rule :andand_base do
|
23
|
+
s(:call, # a method call
|
24
|
+
_ % :receiver, # the receiver
|
25
|
+
:andand, # the method name
|
26
|
+
s(:arglist)) # the arguments
|
27
|
+
end
|
28
|
+
|
29
|
+
# This matches foo.andand.bar
|
30
|
+
rule :andand_call do
|
31
|
+
s(:call, # a method call
|
32
|
+
andand_base, # foo.andand
|
33
|
+
_ % :name, # the method name
|
34
|
+
_ % :args) # the arguments
|
35
|
+
end
|
36
|
+
|
37
|
+
# This matches foo.andand.bar { |args| block }
|
38
|
+
rule :andand_iter do
|
39
|
+
s(:iter, # a block
|
40
|
+
andand_call, # the method call
|
41
|
+
_ % :blockargs, # the arguments passed to the block
|
42
|
+
_ % :block) # content of the block
|
43
|
+
end
|
44
|
+
|
45
|
+
## Rewriters
|
46
|
+
#
|
47
|
+
# Rewriters take one or more rules and defines replacements when they
|
48
|
+
# match. The data-object from SexpPath is given as an argument.
|
49
|
+
|
50
|
+
# This will rewrite:
|
51
|
+
#
|
52
|
+
# foo.andand.bar => (tmp = foo) && tmp.bar
|
53
|
+
# foo.andand.bar { } => (tmp = foo) && tmp.bar { }
|
54
|
+
#
|
55
|
+
rewrite :andand_call, :andand_iter do |data|
|
56
|
+
# get a tmpvar (see below for definition)
|
57
|
+
tmp = tmpvar
|
58
|
+
|
59
|
+
# tmp = foo
|
60
|
+
assign = s(:lasgn, tmp, process(data[:receiver]))
|
61
|
+
|
62
|
+
# tmp.bar
|
63
|
+
call = s(:call, s(:lasgn, tmp), data[:name], process(data[:args]))
|
64
|
+
|
65
|
+
# tmp.bar { }
|
66
|
+
if data[:block]
|
67
|
+
call = s(:iter,
|
68
|
+
call,
|
69
|
+
process(data[:blockargs]),
|
70
|
+
process(data[:block]))
|
71
|
+
end
|
72
|
+
|
73
|
+
# (tmp = foo) && tmp.bar
|
74
|
+
s(:and,
|
75
|
+
assign,
|
76
|
+
call)
|
77
|
+
end
|
78
|
+
|
79
|
+
## Other methods
|
80
|
+
|
81
|
+
def initialize
|
82
|
+
@tmp = 0
|
83
|
+
super # don't forget to call super!
|
84
|
+
end
|
85
|
+
|
86
|
+
# Generates a random variable.
|
87
|
+
def tmpvar
|
88
|
+
"__andand_#{@tmp += 1}".to_sym
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# instantiate a new processor
|
93
|
+
processor = Andand.new
|
94
|
+
|
95
|
+
# foo.andand.bar
|
96
|
+
example =
|
97
|
+
s(:call,
|
98
|
+
s(:call, s(:call, nil, :foo, s(:arglist)), :andand, s(:arglist)),
|
99
|
+
:bar,
|
100
|
+
s(:arglist))
|
101
|
+
|
102
|
+
# process it
|
103
|
+
result = processor.process(example)
|
104
|
+
pp result
|
105
|
+
|
106
|
+
# s(:and,
|
107
|
+
# s(:lasgn, :__andand_1, s(:call, nil, :foo, s(:arglist))),
|
108
|
+
# s(:call, s(:lasgn, :__andand_1), :bar, s(:arglist)))
|
109
|
+
|
110
|
+
# BONUS: turn it into Ruby with Ruby2Ruby
|
111
|
+
require 'ruby2ruby'
|
112
|
+
|
113
|
+
ruby = Ruby2Ruby.new.process(result)
|
114
|
+
puts ruby
|
115
|
+
|
116
|
+
# (__andand_1 = foo and (__andand_1).bar)
|
117
|
+
|
118
|
+
== More
|
119
|
+
|
120
|
+
SexpBuilder has four different concepts:
|
121
|
+
|
122
|
+
* Matchers
|
123
|
+
* Rules
|
124
|
+
* Rewriters
|
125
|
+
* Contexts
|
126
|
+
|
127
|
+
=== Matchers
|
128
|
+
|
129
|
+
A matcher is a bit of Ruby code which can be used in your rules. The
|
130
|
+
expression it should match is passed in, and it should return a true-ish value
|
131
|
+
if it matches. The matcher will be evaluated under the instantiated processor,
|
132
|
+
so you can use other instance methods and instance variables too.
|
133
|
+
|
134
|
+
class Example < SexpBuilder
|
135
|
+
matcher :five_arguments do |exp|
|
136
|
+
self # => the instance of Example
|
137
|
+
exp.length == 6 # the first will always be :arglist
|
138
|
+
end
|
139
|
+
|
140
|
+
rule :magic_call do
|
141
|
+
s(:call, # a method call
|
142
|
+
nil, # no receiver
|
143
|
+
:MAGIC!, # method name
|
144
|
+
five_arguments) # our matcher
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
=== Rules
|
149
|
+
|
150
|
+
You've heard it before, but let's repeat: Rules are simply snippets of
|
151
|
+
SexpPath which can refer to each other and itself. They are basically method
|
152
|
+
definition, so they can take arguments too.
|
153
|
+
|
154
|
+
The rule will be evaluated under a special scope, but if you really need it
|
155
|
+
you can access the instantiated processor using `instance`. You should however
|
156
|
+
move any specific Ruby code into a matcher and let the rules simply contain
|
157
|
+
other rules and matchers.
|
158
|
+
|
159
|
+
class Example < SexpBuilder
|
160
|
+
# Matches any number.
|
161
|
+
rule :number do |capture_as|
|
162
|
+
# Doesn't make very much sense to take an argument here,
|
163
|
+
# it's just an example
|
164
|
+
s(:lit, _ % capture_as)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Matches a sequence of plusses: 1 + 2 + 3
|
168
|
+
rule :plus_sequence do
|
169
|
+
s(:call, # a method call
|
170
|
+
number(:number) | # the receiver can be a number
|
171
|
+
plus_sequence, # or a sequence
|
172
|
+
:+,
|
173
|
+
s(:arglist,
|
174
|
+
number(:number) | # the argument can be a number
|
175
|
+
plus_sequence # or a sequence
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
=== Rewriters
|
180
|
+
|
181
|
+
Rewriters take one or more rules and defines replacements when they match. The
|
182
|
+
data-object from SexpPath is given as an argument. If you want some of the
|
183
|
+
sub-expressions matched too, you'll have to call process yourself.
|
184
|
+
|
185
|
+
class Example < SexpBuilder
|
186
|
+
|
187
|
+
# We want to rewrite the plus_sequence above
|
188
|
+
rewrite :plus_sequence do |data|
|
189
|
+
# sum the numbers
|
190
|
+
sum = data[:number].inject { |all, one| all + one }
|
191
|
+
# return a new number
|
192
|
+
s(:lit, sum)
|
193
|
+
end
|
194
|
+
|
195
|
+
rewrite :something_else do |data|
|
196
|
+
# process the block in case it also needs to be rewritten
|
197
|
+
block = process(data[:block])
|
198
|
+
do_funky_stuff(block)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
=== Contexts
|
203
|
+
|
204
|
+
Contexts allows you to group a set of rewriters together. It will inherit the
|
205
|
+
parents rules and matchers.
|
206
|
+
|
207
|
+
class Example < SexpBuilder
|
208
|
+
# Matches a class definition
|
209
|
+
rule :class_def do
|
210
|
+
s(:class,
|
211
|
+
_ % :name,
|
212
|
+
_ % :parent,
|
213
|
+
_ % :content)
|
214
|
+
end
|
215
|
+
|
216
|
+
rewrite :class_def do |data|
|
217
|
+
# NOTICE: we use process_class to enter the class-context.
|
218
|
+
content = process_class(data[:content])
|
219
|
+
do_funky_stuff(content)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Only for stuff inside a class:
|
223
|
+
context :class do
|
224
|
+
rule :method_definition do
|
225
|
+
s(:defn,
|
226
|
+
_ % :name,
|
227
|
+
_ % :args,
|
228
|
+
_ % :content)
|
229
|
+
end
|
230
|
+
|
231
|
+
rewrite :method_definition do |data|
|
232
|
+
# this will continue processing in the class-context.
|
233
|
+
# use process_main to enter the main context again.
|
234
|
+
content = process(data[:content])
|
235
|
+
do_funky_stuff(content)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
|
241
|
+
If you subclass your processor, it will also enter a new context:
|
242
|
+
|
243
|
+
class ModuleContext < Example
|
244
|
+
# use process_module to enter this context.
|
245
|
+
end
|
246
|
+
|
247
|
+
By default it takes the last part of the name, removes "Context" or "Builder"
|
248
|
+
at the end and turns it into snake case. If needed, you can easily override
|
249
|
+
this yourself (remember to turn it into a writable method name though):
|
250
|
+
|
251
|
+
def Example.context_name(mod)
|
252
|
+
"context#{rand(10)}"
|
253
|
+
end
|
254
|
+
|
255
|
+
== License
|
256
|
+
|
257
|
+
See COPYING for legal information. It's a MIT license which allows you to do
|
258
|
+
pretty much what you want with it, and *please* do!
|
259
|
+
|
data/examples/andand.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
class Andand < SexpBuilder
|
2
|
+
|
3
|
+
# This matches foo.andand:
|
4
|
+
rule :andand_base do
|
5
|
+
s(:call, # a method call
|
6
|
+
_ % :receiver, # the receiver
|
7
|
+
:andand, # the method name
|
8
|
+
s(:arglist)) # the arguments
|
9
|
+
end
|
10
|
+
|
11
|
+
# This matches foo.andand.bar
|
12
|
+
rule :andand_call do
|
13
|
+
s(:call, # a method call
|
14
|
+
andand_base, # foo.andand
|
15
|
+
_ % :name, # the method name
|
16
|
+
_ % :args) # the arguments
|
17
|
+
end
|
18
|
+
|
19
|
+
# This matches foo.andand.bar { |args| block }
|
20
|
+
rule :andand_iter do
|
21
|
+
s(:iter, # a block
|
22
|
+
andand_call, # the method call
|
23
|
+
_ % :blockargs, # the arguments passed to the block
|
24
|
+
_ % :block) # content of the block
|
25
|
+
end
|
26
|
+
|
27
|
+
# This will rewrite:
|
28
|
+
#
|
29
|
+
# foo.andand.bar => (tmp = foo) && tmp.bar
|
30
|
+
# foo.andand.bar { } => (tmp = foo) && tmp.bar { }
|
31
|
+
#
|
32
|
+
rewrite :andand_call, :andand_iter do |data|
|
33
|
+
# get a tmpvar (see below for definition)
|
34
|
+
tmp = tmpvar
|
35
|
+
|
36
|
+
# tmp = foo
|
37
|
+
assign = s(:lasgn, tmp, process(data[:receiver]))
|
38
|
+
|
39
|
+
# tmp.bar
|
40
|
+
call = s(:call, s(:lasgn, tmp), data[:name], process(data[:args]))
|
41
|
+
|
42
|
+
# tmp.bar { }
|
43
|
+
if data[:block]
|
44
|
+
call = s(:iter,
|
45
|
+
call,
|
46
|
+
process(data[:blockargs]),
|
47
|
+
process(data[:block]))
|
48
|
+
end
|
49
|
+
|
50
|
+
# (tmp = foo) && tmp.bar
|
51
|
+
s(:and,
|
52
|
+
assign,
|
53
|
+
call)
|
54
|
+
end
|
55
|
+
|
56
|
+
## Other methods
|
57
|
+
|
58
|
+
def initialize
|
59
|
+
@tmp = 0
|
60
|
+
super # don't forget to call super!
|
61
|
+
end
|
62
|
+
|
63
|
+
# Generates a random variable.
|
64
|
+
def tmpvar
|
65
|
+
"__andand_#{@tmp += 1}".to_sym
|
66
|
+
end
|
67
|
+
end
|
data/lib/sexp_builder.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'thread'
|
3
|
+
require 'sexp_path'
|
4
|
+
|
5
|
+
class SexpBuilder
|
6
|
+
VERSION = "0.1"
|
7
|
+
|
8
|
+
autoload :Context, 'sexp_builder/context'
|
9
|
+
autoload :QueryBuilder, 'sexp_builder/query_builder'
|
10
|
+
attr_reader :context, :scope
|
11
|
+
|
12
|
+
# Initializes the builder. If you redefine this in your subclass,
|
13
|
+
# it's important to call +super()+.
|
14
|
+
def initialize
|
15
|
+
@context = self.class.current_context
|
16
|
+
@main = @context.main_context
|
17
|
+
@scope = []
|
18
|
+
@rewriters = {}
|
19
|
+
@query_builders = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Process +sexp+ under the current context.
|
23
|
+
def process(sexp, options = {})
|
24
|
+
@scope.unshift(sexp.sexp_type)
|
25
|
+
|
26
|
+
rewriters.each do |query, method|
|
27
|
+
if data = query.satisfy?(sexp, QueryBuilder::Data.new)
|
28
|
+
return method.call(SexpPath::SexpResult.new(sexp, data))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# If none of the rewriters matched, process the children:
|
33
|
+
sexp.inject(Sexp.new) do |memo, exp|
|
34
|
+
memo << if exp.is_a?(Sexp)
|
35
|
+
process(exp)
|
36
|
+
else
|
37
|
+
exp
|
38
|
+
end
|
39
|
+
end
|
40
|
+
ensure
|
41
|
+
@scope.shift
|
42
|
+
end
|
43
|
+
|
44
|
+
# Process +sexp+ under the top context.
|
45
|
+
def process_main(sexp, options = {})
|
46
|
+
prev, @context = @context, @main
|
47
|
+
process(sexp, options)
|
48
|
+
ensure
|
49
|
+
@context = prev
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns an array in the format of:
|
53
|
+
#
|
54
|
+
# [<SexpPath::Matcher> rule, <Method> rewriter]
|
55
|
+
def rewriters(context = @context)
|
56
|
+
@rewriters[context] ||= context.rewriters.map do |rule|
|
57
|
+
[query_builder(context).send(rule), method("rewrite_#{rule}")]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the query builder for a given context.
|
62
|
+
def query_builder(context = @context)
|
63
|
+
@query_builders[context] ||= QueryBuilder.make(context, self)
|
64
|
+
end
|
65
|
+
|
66
|
+
class << self
|
67
|
+
extend Forwardable
|
68
|
+
# The Context which this builder will process under.
|
69
|
+
attr_accessor :current_context
|
70
|
+
# Delegates various methods to the current_context.
|
71
|
+
def_delegators :@current_context, :context, :matcher, :rule, :rewrite
|
72
|
+
|
73
|
+
# Sets up the +current_context+.
|
74
|
+
def inherited(mod)
|
75
|
+
if self == SexpBuilder
|
76
|
+
mod.current_context = Context.new(mod)
|
77
|
+
else
|
78
|
+
mod.current_context = current_context.context(context_name(mod))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Turns a module into a context name:
|
83
|
+
#
|
84
|
+
# * FooBar => foo_bar
|
85
|
+
# * FooBarContext => foo_bar
|
86
|
+
# * FooBarBuilder => foo_bar
|
87
|
+
def context_name(mod)
|
88
|
+
name = mod.name
|
89
|
+
name = name.split("::").last
|
90
|
+
name.gsub!(/(Context|Builder)$/, '')
|
91
|
+
name.scan(/.[^A-Z]+/).map { |part| part.downcase }.join("_")
|
92
|
+
end
|
93
|
+
|
94
|
+
# Generates a temporary id.
|
95
|
+
def tmpid
|
96
|
+
@tmpid ||= 0
|
97
|
+
@tmpid += 1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
class SexpBuilder
|
2
|
+
class Context
|
3
|
+
attr_reader :parent, :builder, :query_scope, :contexts, :rewriters
|
4
|
+
|
5
|
+
def initialize(builder, parent = nil)
|
6
|
+
@builder = builder
|
7
|
+
@parent = parent
|
8
|
+
|
9
|
+
@contexts = {}
|
10
|
+
@rewriters = []
|
11
|
+
|
12
|
+
@query_scope = Module.new do
|
13
|
+
include parent.query_scope if parent
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the top-most context.
|
18
|
+
def main_context
|
19
|
+
if @parent
|
20
|
+
@parent.main_context
|
21
|
+
else
|
22
|
+
self
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Defines or finds a sub-context, and evalutes the block under it. If you
|
27
|
+
# want to process something under this context, you would have to call
|
28
|
+
# <tt>process_{name}</tt>.
|
29
|
+
def context(name, &blk)
|
30
|
+
context = define_context(name.to_sym)
|
31
|
+
context.instance_eval(&blk) if blk
|
32
|
+
context
|
33
|
+
end
|
34
|
+
|
35
|
+
# Defines a matcher. A matcher is a bit of Ruby code which can be used in
|
36
|
+
# your rules. The expression it should match is passed in, and it should
|
37
|
+
# return a true-ish value if it matches. The matcher will be evaluated
|
38
|
+
# under the instatiated processor, so you can use other instance methods
|
39
|
+
# and instance variables too.
|
40
|
+
#
|
41
|
+
# matcher :five_arguments do |exp|
|
42
|
+
# self # => the instance of Example
|
43
|
+
# exp.length == 6 # the first will always be :arglist
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# Under the hood, this will define:
|
47
|
+
#
|
48
|
+
# * The block as +matcher_five_arguments+ on the builder.
|
49
|
+
# * +five_arguments+, which will invoke the matcher, on the query scope.
|
50
|
+
def matcher(name, &blk)
|
51
|
+
method_name = "matcher_#{name}"
|
52
|
+
define(method_name, &blk)
|
53
|
+
|
54
|
+
define_query_scope(name) do
|
55
|
+
block do |exp|
|
56
|
+
instance.send(method_name, exp)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Defines a rule. Rules are simply snippets of SexpPath which can refer to
|
62
|
+
# each other and itself. They can also take arguments too.
|
63
|
+
#
|
64
|
+
# # Matches any number.
|
65
|
+
# rule :number do |capture_as|
|
66
|
+
# # Doesn't make very much sense to take an argument here,
|
67
|
+
# # it's just an example
|
68
|
+
# s(:lit, _ % capture_as)
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# # Matches a sequence of plusses: 1 + 2 + 3
|
72
|
+
# rule :plus_sequence do
|
73
|
+
# s(:call, # a method call
|
74
|
+
# number(:number) | # the receiver can be a number
|
75
|
+
# plus_sequence, # or a sequence
|
76
|
+
# :+,
|
77
|
+
# s(:arglist,
|
78
|
+
# number(:number) | # the argument can be a number
|
79
|
+
# plus_sequence # or a sequence
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# Under the hood, this will define:
|
83
|
+
#
|
84
|
+
# * The blocks as +real_number+ and +real_plus_sequence+ on the
|
85
|
+
# query_scope.
|
86
|
+
# * +number+ and +plus_sequence+ which wraps the methods above as
|
87
|
+
# QueryBuilder::Deferred.
|
88
|
+
def rule(name, &blk)
|
89
|
+
real_name = "real_#{name}"
|
90
|
+
define_query_scope(real_name, &blk)
|
91
|
+
define_query_scope(name) do |*args|
|
92
|
+
QueryBuilder::Deferred.new(self, real_name, args, name)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Defines a rewriter. Rewriters take one or more rules and defines
|
97
|
+
# replacements when they match. The data-object from SexpPath is given as
|
98
|
+
# an argument. If you want some of the sub-expressions matched too, you'll
|
99
|
+
# have to call process() yourself.
|
100
|
+
#
|
101
|
+
# rewrite :plus_sequence do |data|
|
102
|
+
# # sum the numbers
|
103
|
+
# sum = data[:number].inject { |all, one| all + one }
|
104
|
+
# # return a new number
|
105
|
+
# s(:lit, sum)
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# Under the hood, this will define:
|
109
|
+
#
|
110
|
+
# * The block as +rewrite_plus_sequence+.
|
111
|
+
# * And @rewriters will now include :plus_sequence.
|
112
|
+
#
|
113
|
+
# You can also give this several rules, or none if you want it to match
|
114
|
+
# every single Sexp.
|
115
|
+
#
|
116
|
+
# == Context shortcut
|
117
|
+
#
|
118
|
+
# rewrite :foo, :in => :bar do
|
119
|
+
# ...
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# Is the same as:
|
123
|
+
#
|
124
|
+
# context :bar do
|
125
|
+
# rewrite :foo do
|
126
|
+
# ...
|
127
|
+
# end
|
128
|
+
# end
|
129
|
+
def rewrite(*rules, &blk)
|
130
|
+
options = rules.last.is_a?(Hash) ? rules.pop : {}
|
131
|
+
rules << :wild if rules.empty?
|
132
|
+
|
133
|
+
return context(options[:in]).rewrite(*rules, &blk) if options[:in]
|
134
|
+
|
135
|
+
rules.each do |rule|
|
136
|
+
@rewriters << rule
|
137
|
+
define("rewrite_#{rule}", &blk)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def define_context(name)
|
144
|
+
@contexts[name] ||= begin
|
145
|
+
context = Context.new(@builder, self)
|
146
|
+
|
147
|
+
define("process_#{name}") do |*args|
|
148
|
+
begin
|
149
|
+
prev, @context = @context, context
|
150
|
+
process(*args)
|
151
|
+
ensure
|
152
|
+
@context = prev
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def define_query_scope(name, &blk)
|
161
|
+
@query_scope.send(:define_method, name, &blk)
|
162
|
+
end
|
163
|
+
|
164
|
+
def define(name, &blk)
|
165
|
+
@builder.send(:define_method, name, &blk)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class SexpBuilder
|
2
|
+
class QueryBuilder < SexpPath::SexpQueryBuilder
|
3
|
+
class Data < Hash
|
4
|
+
def []=(key, value)
|
5
|
+
if current = self[key]
|
6
|
+
if current.class == Array
|
7
|
+
current << value
|
8
|
+
else
|
9
|
+
super(key, [current, value])
|
10
|
+
end
|
11
|
+
else
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Scope < SexpPath::Matcher::Base
|
18
|
+
def initialize(type, instance)
|
19
|
+
@type = type
|
20
|
+
@instance = instance
|
21
|
+
end
|
22
|
+
|
23
|
+
def satisfy?(o, data={})
|
24
|
+
if @instance.scope[1..-1].include?(@type)
|
25
|
+
capture_match o, data
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Deferred < SexpPath::Matcher::Base
|
31
|
+
def initialize(receiver, name, args, real_name)
|
32
|
+
@receiver = receiver
|
33
|
+
@name = name
|
34
|
+
@args = args
|
35
|
+
@real_name = real_name.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
def satisfy?(o, data={})
|
39
|
+
@receiver.send(@name, *@args).satisfy?(o, data)
|
40
|
+
end
|
41
|
+
|
42
|
+
def inspect
|
43
|
+
"rule(:#{@real_name})"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
attr_accessor :instance
|
49
|
+
|
50
|
+
def make(context, instance)
|
51
|
+
query_builder = Class.new(QueryBuilder) { extend context.query_scope }
|
52
|
+
query_builder.instance = instance
|
53
|
+
query_builder
|
54
|
+
end
|
55
|
+
|
56
|
+
def scope(type)
|
57
|
+
Scope.new(type, instance)
|
58
|
+
end
|
59
|
+
|
60
|
+
def block(&blk)
|
61
|
+
SexpPath::Matcher::Block.new(&blk)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sexp_builder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.1"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Magnus Holm
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-25 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: sexp_path
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description:
|
26
|
+
email: judofyr@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- COPYING
|
35
|
+
- README.rdoc
|
36
|
+
- examples/andand.rb
|
37
|
+
- lib/sexp_builder.rb
|
38
|
+
- lib/sexp_builder/context.rb
|
39
|
+
- lib/sexp_builder/query_builder.rb
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: http://dojo.rubyforge.org/
|
42
|
+
licenses: []
|
43
|
+
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.3.5
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Easily to match and rewrite S-expressions
|
68
|
+
test_files: []
|
69
|
+
|