ruby-next 0.0.1.26 → 0.1.0

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +201 -11
  5. data/bin/parse +19 -0
  6. data/bin/ruby-next +16 -0
  7. data/bin/transform +18 -0
  8. data/lib/ruby-next/cli.rb +55 -0
  9. data/lib/ruby-next/commands/base.rb +42 -0
  10. data/lib/ruby-next/commands/nextify.rb +113 -0
  11. data/lib/ruby-next/core/array/difference_union_intersection.rb +31 -0
  12. data/lib/ruby-next/core/enumerable/filter.rb +23 -0
  13. data/lib/ruby-next/core/enumerable/filter_map.rb +50 -0
  14. data/lib/ruby-next/core/enumerable/tally.rb +28 -0
  15. data/lib/ruby-next/core/hash/merge.rb +16 -0
  16. data/lib/ruby-next/core/kernel/then.rb +12 -0
  17. data/lib/ruby-next/core/pattern_matching.rb +37 -0
  18. data/lib/ruby-next/core/proc/compose.rb +21 -0
  19. data/lib/ruby-next/core/runtime.rb +10 -0
  20. data/lib/ruby-next/core.rb +28 -0
  21. data/lib/ruby-next/language/parser.rb +23 -0
  22. data/lib/ruby-next/language/rewriters/args_forward.rb +57 -0
  23. data/lib/ruby-next/language/rewriters/base.rb +87 -0
  24. data/lib/ruby-next/language/rewriters/endless_range.rb +60 -0
  25. data/lib/ruby-next/language/rewriters/method_reference.rb +27 -0
  26. data/lib/ruby-next/language/rewriters/numbered_params.rb +41 -0
  27. data/lib/ruby-next/language/rewriters/pattern_matching.rb +464 -0
  28. data/lib/ruby-next/language/runtime.rb +149 -0
  29. data/lib/ruby-next/language/setup.rb +43 -0
  30. data/lib/ruby-next/language/unparser.rb +23 -0
  31. data/lib/ruby-next/language.rb +100 -0
  32. data/lib/ruby-next/utils.rb +39 -0
  33. data/lib/ruby-next/version.rb +5 -0
  34. data/lib/ruby-next.rb +37 -0
  35. data/lib/uby-next.rb +66 -0
  36. metadata +69 -7
@@ -0,0 +1,464 @@
1
+ # frozen_string_literal: true
2
+
3
+ using RubyNext
4
+
5
+ module RubyNext
6
+ module Language
7
+ module Rewriters
8
+ using(Module.new do
9
+ refine ::Parser::AST::Node do
10
+ def to_ast_node
11
+ self
12
+ end
13
+ end
14
+
15
+ refine String do
16
+ def to_ast_node
17
+ ::Parser::AST::Node.new(:str, [self])
18
+ end
19
+ end
20
+
21
+ refine Symbol do
22
+ def to_ast_node
23
+ ::Parser::AST::Node.new(:sym, [self])
24
+ end
25
+ end
26
+
27
+ refine Integer do
28
+ def to_ast_node
29
+ ::Parser::AST::Node.new(:int, [self])
30
+ end
31
+ end
32
+ end)
33
+
34
+ class PatternMatching < Base
35
+ SYNTAX_PROBE = "case 0; in 0; true; else; 1; end"
36
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.7.0")
37
+
38
+ MATCHEE = :__matchee__
39
+ MATCHEE_ARR = :__matchee_arr__
40
+ MATCHEE_HASH = :__matchee_hash__
41
+
42
+ def on_case_match(node)
43
+ context.track! self
44
+ context.use_ruby_next!
45
+
46
+ @deconstructed = []
47
+
48
+ matchee_ast =
49
+ s(:lvasgn, MATCHEE, node.children[0])
50
+
51
+ ifs_ast = locals.with(
52
+ matchee: MATCHEE,
53
+ arr: MATCHEE_ARR,
54
+ hash: MATCHEE_HASH
55
+ ) do
56
+ build_if_clause(node.children[1], node.children[2..-1])
57
+ end
58
+
59
+ node.updated(
60
+ :begin,
61
+ [
62
+ matchee_ast, ifs_ast
63
+ ]
64
+ )
65
+ end
66
+
67
+ private
68
+
69
+ def build_if_clause(node, rest)
70
+ if node&.type == :in_pattern
71
+ build_in_pattern(node, rest)
72
+ else
73
+ raise "Unexpected else in the middle of case ... in" if rest && rest.size > 0
74
+ # else clause must be present
75
+ node || no_matching_pattern
76
+ end
77
+ end
78
+
79
+ def build_in_pattern(clause, rest)
80
+ [
81
+ with_guard(
82
+ send(
83
+ :"#{clause.children[0].type}_clause",
84
+ clause.children[0]
85
+ ),
86
+ clause.children[1] # guard
87
+ ),
88
+ clause.children[2] || s(:nil) # expression
89
+ ].then do |children|
90
+ if rest && rest.size > 0
91
+ children << build_if_clause(rest.first, rest[1..-1])
92
+ end
93
+
94
+ s(:if, *children)
95
+ end
96
+ end
97
+
98
+ def const_pattern_clause(node)
99
+ const, pattern = *node.children
100
+
101
+ case_eq_clause(const).then do |node|
102
+ next node if pattern.nil?
103
+
104
+ s(:and,
105
+ node,
106
+ send(:"#{pattern.type}_clause", pattern))
107
+ end
108
+ end
109
+
110
+ def match_alt_clause(node)
111
+ children = node.children.map do |child|
112
+ send :"#{child.type}_clause", child
113
+ end
114
+ s(:or, *children)
115
+ end
116
+
117
+ def match_as_clause(node)
118
+ s(:and,
119
+ case_eq_clause(node.children[0]),
120
+ match_var_clause(node.children[1], s(:lvar, locals[:matchee])))
121
+ end
122
+
123
+ def match_var_clause(node, left = s(:lvar, locals[:matchee]))
124
+ s(:or,
125
+ s(:lvasgn, node.children[0], left),
126
+ s(:true)) # rubocop:disable Lint/BooleanSymbol
127
+ end
128
+
129
+ def pin_clause(node)
130
+ case_eq_clause node.children[0]
131
+ end
132
+
133
+ def case_eq_clause(node, right = s(:lvar, locals[:matchee]))
134
+ s(:send,
135
+ node, :===, right)
136
+ end
137
+
138
+ #=========== ARRAY PATTERN (START) ===============
139
+
140
+ def array_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
141
+ deconstruct_node(matchee).then do |dnode|
142
+ right =
143
+ if node.children.empty?
144
+ case_eq_clause(s(:array), s(:lvar, locals[:arr]))
145
+ else
146
+ array_element(0, *node.children)
147
+ end
148
+
149
+ # already deconsrtructed
150
+ next right if dnode.nil?
151
+
152
+ # if there is no rest or tail, match the size first
153
+ unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
154
+ right =
155
+ s(:and,
156
+ s(:send,
157
+ node.children.size.to_ast_node,
158
+ :==,
159
+ s(:send, s(:lvar, locals[:arr]), :size)),
160
+ right)
161
+ end
162
+
163
+ s(:and,
164
+ dnode,
165
+ right)
166
+ end
167
+ end
168
+
169
+ alias array_pattern_with_tail_clause array_pattern_clause
170
+
171
+ def deconstruct_node(matchee)
172
+ # only deconstruct once per case
173
+ return if deconstructed.include?(locals[:arr])
174
+
175
+ right = s(:send, matchee, :deconstruct)
176
+
177
+ deconstructed << locals[:arr]
178
+ s(:and,
179
+ s(:or,
180
+ s(:lvasgn, locals[:arr], right),
181
+ s(:true)), # rubocop:disable Lint/BooleanSymbol
182
+ s(:or,
183
+ case_eq_clause(s(:const, nil, :Array), s(:lvar, locals[:arr])),
184
+ raise_error(:TypeError)))
185
+ end
186
+
187
+ def array_element(index, head, *tail)
188
+ return array_match_rest(index, head, *tail) if head.type == :match_rest
189
+
190
+ send("#{head.type}_array_element", head, index).then do |node|
191
+ next node if tail.empty?
192
+
193
+ s(:and,
194
+ node,
195
+ array_element(index + 1, *tail))
196
+ end
197
+ end
198
+
199
+ def array_match_rest(index, node, *tail)
200
+ child = node.children[0]
201
+ rest = arr_rest_items(index, tail.size).then do |r|
202
+ next r unless child
203
+ match_var_clause(
204
+ child,
205
+ r
206
+ )
207
+ end
208
+
209
+ return rest if tail.empty?
210
+
211
+ s(:and,
212
+ rest,
213
+ array_rest_element(*tail))
214
+ end
215
+
216
+ def array_rest_element(head, *tail)
217
+ send("#{head.type}_array_element", head, -(tail.size + 1)).then do |node|
218
+ next node if tail.empty?
219
+
220
+ s(:and,
221
+ node,
222
+ array_rest_element(*tail))
223
+ end
224
+ end
225
+
226
+ def array_pattern_array_element(node, index)
227
+ element = arr_item_at(index)
228
+ locals.with(arr: locals[:arr, index]) do
229
+ array_pattern_clause(node, element)
230
+ end
231
+ end
232
+
233
+ def match_alt_array_element(node, index)
234
+ children = node.children.map do |child, i|
235
+ send :"#{child.type}_array_element", child, index
236
+ end
237
+ s(:or, *children)
238
+ end
239
+
240
+ def match_var_array_element(node, index)
241
+ match_var_clause(node, arr_item_at(index))
242
+ end
243
+
244
+ def pin_array_element(node, index)
245
+ case_eq_array_element node.children[0], index
246
+ end
247
+
248
+ def case_eq_array_element(node, index)
249
+ case_eq_clause(node, arr_item_at(index))
250
+ end
251
+
252
+ def arr_item_at(index, arr = s(:lvar, locals[:arr]))
253
+ s(:index, arr, index.to_ast_node)
254
+ end
255
+
256
+ def arr_rest_items(index, size, arr = s(:lvar, locals[:arr]))
257
+ s(:index,
258
+ arr,
259
+ s(:irange,
260
+ s(:int, index),
261
+ s(:int, -(size + 1))))
262
+ end
263
+
264
+ #=========== ARRAY PATTERN (END) ===============
265
+
266
+ #=========== HASH PATTERN (START) ===============
267
+
268
+ def hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
269
+ # Optimization: avoid hash modifications when not needed
270
+ # (we use #dup and #delete when "reading" values when **rest is present
271
+ # to assign the rest of the hash copy to it)
272
+ @hash_match_rest = node.children.any? { |child| child.type == :match_rest }
273
+ keys = hash_pattern_keys(node.children)
274
+
275
+ deconstruct_keys_node(keys, matchee).then do |dnode|
276
+ right =
277
+ if node.children.empty?
278
+ case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
279
+ else
280
+ hash_element(*node.children)
281
+ end
282
+
283
+ return dnode if right.nil?
284
+
285
+ s(:and,
286
+ dnode,
287
+ right)
288
+ end
289
+ end
290
+
291
+ def hash_pattern_keys(children)
292
+ return s(:nil) if children.empty?
293
+
294
+ children.filter_map do |child|
295
+ return s(:nil) if child.type == :match_rest
296
+
297
+ send("#{child.type}_hash_key", child)
298
+ end.then { |keys| s(:array, *keys) }
299
+ end
300
+
301
+ def pair_hash_key(node)
302
+ node.children[0]
303
+ end
304
+
305
+ def match_var_hash_key(node)
306
+ s(:sym, node.children[0])
307
+ end
308
+
309
+ def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
310
+ # Deconstruct once and use a copy of the hash for each pattern if we need **rest.
311
+ hash_dup =
312
+ if @hash_match_rest
313
+ s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup))
314
+ else
315
+ s(:lvasgn, locals[:hash], s(:lvar, locals[:hash, :src]))
316
+ end
317
+
318
+ # Create a copy of the original hash if already deconstructed
319
+ return hash_dup if deconstructed.include?(locals[:hash])
320
+
321
+ deconstructed << locals[:hash]
322
+
323
+ right = s(:send,
324
+ matchee, :deconstruct_keys, keys)
325
+
326
+ s(:and,
327
+ s(:or,
328
+ s(:lvasgn, locals[:hash, :src], right),
329
+ s(:true)), # rubocop:disable Lint/BooleanSymbol
330
+ s(:and,
331
+ s(:or,
332
+ case_eq_clause(s(:const, nil, :Hash), s(:lvar, locals[:hash, :src])),
333
+ raise_error(:TypeError)),
334
+ hash_dup))
335
+ end
336
+
337
+ def hash_pattern_hash_element(node, key)
338
+ element = hash_value_at(key)
339
+ locals.with(hash: locals[:hash, deconstructed.size]) do
340
+ hash_pattern_clause(node, element)
341
+ end
342
+ end
343
+
344
+ def hash_element(head, *tail)
345
+ send("#{head.type}_hash_element", head).then do |node|
346
+ next node if tail.empty?
347
+
348
+ right = hash_element(*tail)
349
+
350
+ next node if right.nil?
351
+
352
+ s(:and,
353
+ node,
354
+ right)
355
+ end
356
+ end
357
+
358
+ def pair_hash_element(node, _key = nil)
359
+ key, val = *node.children
360
+ send("#{val.type}_hash_element", val, key)
361
+ end
362
+
363
+ def match_alt_hash_element(node, key)
364
+ element_node = s(:lvasgn, locals[:hash, :el], hash_value_at(key))
365
+
366
+ children = locals.with(hash_element: locals[:hash, :el]) do
367
+ node.children.map do |child, i|
368
+ send :"#{child.type}_hash_element", child, key
369
+ end
370
+ end
371
+
372
+ s(:and,
373
+ s(:or,
374
+ element_node,
375
+ s(:true)), # rubocop:disable Lint/BooleanSymbol
376
+ s(:or, *children))
377
+ end
378
+
379
+ def match_var_hash_element(node, key = nil)
380
+ key ||= node.children[0]
381
+ # We need to check whether key is present first
382
+ s(:and,
383
+ hash_has_key(key),
384
+ match_var_clause(node, hash_value_at(key)))
385
+ end
386
+
387
+ def match_rest_hash_element(node, _key = nil)
388
+ # case {}; in **; end
389
+ return if node.children.empty?
390
+
391
+ child = node.children[0]
392
+
393
+ raise ArgumentError, "Unknown hash match_rest child: #{child.type}" unless child.type == :match_var
394
+
395
+ match_var_clause(child, s(:lvar, locals[:hash]))
396
+ end
397
+
398
+ def case_eq_hash_element(node, key)
399
+ case_eq_clause node, hash_value_at(key)
400
+ end
401
+
402
+ def hash_value_at(key, hash = s(:lvar, locals[:hash]))
403
+ return s(:lvar, locals.fetch(:hash_element)) if locals.key?(:hash_element)
404
+
405
+ if @hash_match_rest
406
+ s(:send,
407
+ hash, :delete,
408
+ key.to_ast_node)
409
+ else
410
+ s(:index,
411
+ hash,
412
+ key.to_ast_node)
413
+ end
414
+ end
415
+
416
+ def hash_has_key(key, hash = s(:lvar, locals[:hash]))
417
+ s(:send,
418
+ hash, :key?,
419
+ key.to_ast_node)
420
+ end
421
+
422
+ #=========== HASH PATTERN (END) ===============
423
+
424
+ def with_guard(node, guard)
425
+ return node unless guard
426
+
427
+ s(:and,
428
+ node,
429
+ guard.children[0]).then do |expr|
430
+ next expr unless guard.type == :unless_guard
431
+ s(:send, expr, :!)
432
+ end
433
+ end
434
+
435
+ def no_matching_pattern
436
+ raise_error :NoMatchingPatternError
437
+ end
438
+
439
+ def raise_error(type)
440
+ s(:send, s(:const, nil, :Kernel), :raise,
441
+ s(:const, nil, type),
442
+ s(:send,
443
+ s(:lvar, locals[:matchee]), :inspect))
444
+ end
445
+
446
+ def respond_to_missing?(mid, *)
447
+ return true if mid.match?(/_(clause|array_element)/)
448
+ super
449
+ end
450
+
451
+ def method_missing(mid, *args, &block)
452
+ return case_eq_clause(args.first) if mid.match?(/_clause$/)
453
+ return case_eq_array_element(*args) if mid.match?(/_array_element$/)
454
+ return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
455
+ super
456
+ end
457
+
458
+ private
459
+
460
+ attr_reader :deconstructed
461
+ end
462
+ end
463
+ end
464
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ require "ruby-next"
6
+ require "ruby-next/utils"
7
+ require "ruby-next/language"
8
+
9
+ using RubyNext
10
+
11
+ module RubyNext
12
+ module Language
13
+ # Module responsible for runtime transformations
14
+ module Runtime
15
+ # Apply only rewriters required for the current version
16
+ REWRITERS = RubyNext::Language.rewriters.select(&:unsupported_syntax?)
17
+
18
+ class << self
19
+ include Utils
20
+
21
+ attr_reader :watch_dirs
22
+
23
+ def load(path, wrap: false)
24
+ raise "RubyNext cannot handle `load(smth, wrap: true)`" if wrap
25
+
26
+ contents = File.read(path)
27
+ new_contents = transform contents
28
+
29
+ puts source_with_lines(new_contents) if ENV["RUBY_NEXT_DEBUG"] == "1"
30
+
31
+ TOPLEVEL_BINDING.eval(new_contents, path)
32
+ true
33
+ end
34
+
35
+ def transform(contents, **options)
36
+ Language.transform(contents, rewriters: REWRITERS, **options)
37
+ end
38
+
39
+ def transformable?(path)
40
+ watch_dirs.any? { |dir| path.start_with?(dir) }
41
+ end
42
+
43
+ def feature_path(path)
44
+ path = resolve_feature_path(path)
45
+ return if path.nil?
46
+ return if File.extname(path) != ".rb"
47
+ return unless transformable?(path)
48
+ path
49
+ end
50
+
51
+ private
52
+
53
+ attr_writer :watch_dirs
54
+ end
55
+
56
+ self.watch_dirs = %w[app lib spec test].map { |path| File.join(Dir.pwd, path) }
57
+ end
58
+ end
59
+ end
60
+
61
+ # Patch Kernel to hijack require/require_relative/load/eval
62
+ module Kernel
63
+ module_function # rubocop:disable Style/ModuleFunction
64
+
65
+ alias_method :require_without_ruby_next, :require
66
+ def require(path)
67
+ realpath = RubyNext::Language::Runtime.feature_path(path)
68
+ return require_without_ruby_next(path) unless realpath
69
+
70
+ return false if $LOADED_FEATURES.include?(realpath)
71
+
72
+ RubyNext::Language::Runtime.load(realpath)
73
+
74
+ $LOADED_FEATURES << realpath
75
+ true
76
+ rescue => e
77
+ warn "RubyNext failed to require '#{path}': #{e.message}"
78
+ require_without_ruby_next(path)
79
+ end
80
+
81
+ alias_method :require_relative_without_ruby_next, :require_relative
82
+ def require_relative(path)
83
+ from = caller_locations(1..1).first.absolute_path || File.join(Dir.pwd, "main")
84
+ realpath = File.absolute_path(
85
+ File.join(
86
+ File.dirname(File.absolute_path(from)),
87
+ path
88
+ )
89
+ )
90
+ require(realpath)
91
+ rescue => e
92
+ warn "RubyNext failed to require relative '#{path}' from #{from}: #{e.message}"
93
+ require_relative_without_ruby_next(path)
94
+ end
95
+
96
+ alias_method :load_without_ruby_next, :load
97
+ def load(path, wrap = false)
98
+ realpath = RubyNext::Language::Runtime.feature_path(path)
99
+
100
+ return load_without_ruby_next(path, wrap) unless realpath
101
+
102
+ RubyNext::Language::Runtime.load(realpath, wrap: wrap)
103
+ rescue => e
104
+ warn "RubyNext failed to load '#{path}': #{e.message}"
105
+ load_without_ruby_next(path)
106
+ end
107
+
108
+ alias_method :eval_without_ruby_next, :eval
109
+ def eval(source, *args)
110
+ new_source = RubyNext::Language::Runtime.transform(source, eval: true)
111
+ eval_without_ruby_next new_source, *args
112
+ end
113
+ end
114
+
115
+ # Patch BasicObject to hijack instance_eval
116
+ class BasicObject
117
+ alias_method :instance_eval_without_ruby_next, :instance_eval
118
+
119
+ def instance_eval(*args, &block)
120
+ return instance_eval_without_ruby_next(*args, &block) if block_given?
121
+
122
+ source = args.shift
123
+ new_source = ::RubyNext::Language::Runtime.transform(source, eval: true)
124
+ instance_eval_without_ruby_next new_source, *args
125
+ end
126
+ end
127
+
128
+ # Patch Module to hijack class_eval/module_eval
129
+ class Module
130
+ alias_method :module_eval_without_ruby_next, :module_eval
131
+
132
+ def module_eval(*args, &block)
133
+ return module_eval_without_ruby_next(*args, &block) if block_given?
134
+
135
+ source = args.shift
136
+ new_source = ::RubyNext::Language::Runtime.transform(source, eval: true)
137
+ module_eval_without_ruby_next new_source, *args
138
+ end
139
+
140
+ alias_method :class_eval_without_ruby_next, :class_eval
141
+
142
+ def class_eval(*args, &block)
143
+ return class_eval_without_ruby_next(*args, &block) if block_given?
144
+
145
+ source = args.shift
146
+ new_source = ::RubyNext::Language::Runtime.transform(source, eval: true)
147
+ class_eval_without_ruby_next new_source, *args
148
+ end
149
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Make sure Core is loaded
4
+ require "ruby-next"
5
+
6
+ module RubyNext
7
+ module Language
8
+ class << self
9
+ def setup_gem_load_path(lib_dir = "lib", rbnext_dir: RUBY_NEXT_DIR)
10
+ called_from = caller_locations(1, 1).first.path
11
+ dirname = File.dirname(called_from)
12
+
13
+ loop do
14
+ basename = File.basename(dirname)
15
+ raise "Couldn't find gem's load dir: #{lib_dir}" if basename == dirname
16
+
17
+ break if basename == lib_dir
18
+
19
+ dirname = File.dirname(basename)
20
+ end
21
+
22
+ current_index = $LOAD_PATH.index(dirname)
23
+
24
+ raise "Gem's lib is not in the $LOAD_PATH: #{dirname}" if current_index.nil?
25
+
26
+ version = RubyNext.next_version
27
+
28
+ loop do
29
+ break unless version
30
+
31
+ version_dir = File.join(dirname, rbnext_dir, version.segments[0..1].join("."))
32
+
33
+ if File.exist?(version_dir)
34
+ $LOAD_PATH.insert current_index, version_dir
35
+ current_index += 1
36
+ end
37
+
38
+ version = RubyNext.next_version(version)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Require current parser without warnings
4
+ save_verbose, $VERBOSE = $VERBOSE, nil
5
+ require "parser/current"
6
+ $VERBOSE = save_verbose
7
+
8
+ require "unparser"
9
+
10
+ # Unparser patches
11
+
12
+ # Unparser doesn't support endless ranges
13
+ # Source: https://github.com/mbj/unparser/blob/a4f959d58b660ef0630659efa5882fc20936eb18/lib/unparser/emitter/literal/range.rb
14
+ # TODO: propose a PR
15
+ class Unparser::Emitter::Literal::Range
16
+ private
17
+
18
+ def dispatch
19
+ visit(begin_node)
20
+ write(TOKENS.fetch(node.type))
21
+ visit(end_node) unless end_node.nil?
22
+ end
23
+ end