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 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
+