sexp_builder 0.1

Sign up to get free protection for your applications and to get access to all the features.
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.
@@ -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
+
@@ -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
@@ -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
+