ruby-next-core 0.15.3 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +127 -54
  4. data/bin/mspec +11 -0
  5. data/lib/.rbnext/2.1/ruby-next/commands/nextify.rb +295 -0
  6. data/lib/.rbnext/2.1/ruby-next/core.rb +10 -2
  7. data/lib/.rbnext/2.1/ruby-next/language.rb +59 -12
  8. data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +83 -3
  9. data/lib/.rbnext/2.3/ruby-next/config.rb +79 -0
  10. data/lib/.rbnext/2.3/ruby-next/core/data.rb +163 -0
  11. data/lib/.rbnext/2.3/ruby-next/language/eval.rb +4 -4
  12. data/lib/.rbnext/2.3/ruby-next/language/rewriters/2.7/pattern_matching.rb +2 -2
  13. data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +6 -32
  14. data/lib/.rbnext/2.3/ruby-next/utils.rb +3 -22
  15. data/lib/.rbnext/2.6/ruby-next/core/data.rb +163 -0
  16. data/lib/.rbnext/2.7/ruby-next/core/data.rb +163 -0
  17. data/lib/.rbnext/2.7/ruby-next/core.rb +10 -2
  18. data/lib/.rbnext/2.7/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  19. data/lib/.rbnext/2.7/ruby-next/language/rewriters/2.7/pattern_matching.rb +2 -2
  20. data/lib/.rbnext/2.7/ruby-next/language/rewriters/text.rb +132 -0
  21. data/lib/.rbnext/3.2/ruby-next/commands/base.rb +55 -0
  22. data/lib/.rbnext/3.2/ruby-next/language/rewriters/2.7/pattern_matching.rb +1095 -0
  23. data/lib/.rbnext/3.2/ruby-next/rubocop.rb +210 -0
  24. data/lib/ruby-next/commands/nextify.rb +85 -3
  25. data/lib/ruby-next/config.rb +29 -2
  26. data/lib/ruby-next/core/data.rb +163 -0
  27. data/lib/ruby-next/core/matchdata/deconstruct.rb +9 -0
  28. data/lib/ruby-next/core/matchdata/deconstruct_keys.rb +20 -0
  29. data/lib/ruby-next/core/matchdata/named_captures.rb +11 -0
  30. data/lib/ruby-next/core/refinement/import.rb +44 -36
  31. data/lib/ruby-next/core/time/deconstruct_keys.rb +30 -0
  32. data/lib/ruby-next/core.rb +10 -2
  33. data/lib/ruby-next/irb.rb +2 -2
  34. data/lib/ruby-next/language/bootsnap.rb +2 -25
  35. data/lib/ruby-next/language/eval.rb +4 -4
  36. data/lib/ruby-next/language/paco_parser.rb +7 -0
  37. data/lib/ruby-next/language/paco_parsers/base.rb +47 -0
  38. data/lib/ruby-next/language/paco_parsers/comments.rb +26 -0
  39. data/lib/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  40. data/lib/ruby-next/language/parser.rb +31 -6
  41. data/lib/ruby-next/language/rewriters/3.0/args_forward_leading.rb +2 -2
  42. data/lib/ruby-next/language/rewriters/3.1/oneline_pattern_parensless.rb +1 -1
  43. data/lib/ruby-next/language/rewriters/3.1/shorthand_hash.rb +2 -1
  44. data/lib/ruby-next/language/rewriters/3.2/anonymous_restargs.rb +104 -0
  45. data/lib/ruby-next/language/rewriters/abstract.rb +57 -0
  46. data/lib/ruby-next/language/rewriters/base.rb +6 -32
  47. data/lib/ruby-next/language/rewriters/edge/it_param.rb +58 -0
  48. data/lib/ruby-next/language/rewriters/edge.rb +12 -0
  49. data/lib/ruby-next/language/rewriters/proposed/bind_vars_pattern.rb +3 -0
  50. data/lib/ruby-next/language/rewriters/proposed/method_reference.rb +9 -20
  51. data/lib/ruby-next/language/rewriters/text.rb +132 -0
  52. data/lib/ruby-next/language/runtime.rb +9 -86
  53. data/lib/ruby-next/language/setup.rb +5 -2
  54. data/lib/ruby-next/language/unparser.rb +5 -0
  55. data/lib/ruby-next/language.rb +59 -12
  56. data/lib/ruby-next/pry.rb +1 -1
  57. data/lib/ruby-next/rubocop.rb +2 -0
  58. data/lib/ruby-next/utils.rb +3 -22
  59. data/lib/ruby-next/version.rb +1 -1
  60. data/lib/uby-next.rb +2 -2
  61. metadata +63 -10
@@ -0,0 +1,1095 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module Rewriters
6
+ using RubyNext
7
+
8
+ using(Module.new do
9
+ refine ::Parser::AST::Node do
10
+ def to_ast_node
11
+ self
12
+ end
13
+
14
+ # Useful to generate simple operation nodes
15
+ # (e.g., 'a + b')
16
+ def -(other)
17
+ ::Parser::AST::Node.new(:send, [self, :-, other.to_ast_node])
18
+ end
19
+
20
+ def +(other)
21
+ ::Parser::AST::Node.new(:send, [self, :+, other.to_ast_node])
22
+ end
23
+ end
24
+
25
+ refine String do
26
+ def to_ast_node
27
+ ::Parser::AST::Node.new(:str, [self])
28
+ end
29
+ end
30
+
31
+ refine Symbol do
32
+ def to_ast_node
33
+ ::Parser::AST::Node.new(:sym, [self])
34
+ end
35
+ end
36
+
37
+ refine Integer do
38
+ def to_ast_node
39
+ ::Parser::AST::Node.new(:int, [self])
40
+ end
41
+ end
42
+ end)
43
+
44
+ # We can memoize structural predicates to avoid double calculation.
45
+ #
46
+ # For example, consider the following case and the corresponding predicate chains:
47
+ #
48
+ # case val
49
+ # in [:ok, 200] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
50
+ # in [:created, 201] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
51
+ # in [401 | 403] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_1]
52
+ # end
53
+ #
54
+ # We can minimize the number of predicate calls by storing the intermediate values (prefixed with `p_`) and using them
55
+ # in the subsequent calls:
56
+ #
57
+ # case val
58
+ # in [:ok, 200] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
59
+ # in [:created, 201] #=> [:p_deconstructed, :p_arr_size_2]
60
+ # in [401 | 403] #=> [:p_deconstructed, :arr_size_is_1]
61
+ # end
62
+ #
63
+ # This way we mimic a naive decision tree algorithim.
64
+ module Predicates
65
+ class Processor < ::Parser::TreeRewriter
66
+ attr_reader :predicates
67
+
68
+ def initialize(predicates)
69
+ @predicates = predicates
70
+ super()
71
+ end
72
+
73
+ def on_lvasgn(node)
74
+ lvar, val = *node.children
75
+ if predicates.store[lvar] == false
76
+ process(val)
77
+ else
78
+ node
79
+ end
80
+ end
81
+
82
+ def on_and(node)
83
+ left, right = *node.children
84
+
85
+ if truthy(left)
86
+ process(right)
87
+ elsif truthy(right)
88
+ process(left)
89
+ else
90
+ node.updated(
91
+ :and,
92
+ [
93
+ process(left),
94
+ process(right)
95
+ ]
96
+ )
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def truthy(node)
103
+ return false unless node.is_a?(::Parser::AST::Node)
104
+ return true if node.type == :true
105
+ return false if node.children.empty?
106
+
107
+ node.children.all? { |child| truthy(child) }
108
+ end
109
+ end
110
+
111
+ class Base
112
+ attr_reader :store, :predicates_by_path, :count, :terminated, :current_path
113
+ alias terminated? terminated
114
+
115
+ def initialize
116
+ # total number of predicates
117
+ @count = 0
118
+ # cache of all predicates by path
119
+ @predicates_by_path = {}
120
+ # all predicates and their dirty state
121
+ @store = {}
122
+
123
+ @current_path = []
124
+ end
125
+
126
+ def reset!
127
+ @current_path = []
128
+ @terminated = false
129
+ end
130
+
131
+ def push(path)
132
+ current_path << path
133
+ end
134
+
135
+ def pop
136
+ current_path.pop
137
+ end
138
+
139
+ def terminate!
140
+ @terminated = true
141
+ end
142
+
143
+ def predicate_clause(name, node)
144
+ if pred?(name)
145
+ read_pred(name)
146
+ else
147
+ write_pred(name, node)
148
+ end
149
+ end
150
+
151
+ def pred?(name)
152
+ predicates_by_path.key?(current_path + [name])
153
+ end
154
+
155
+ def read_pred(name)
156
+ lvar = predicates_by_path.fetch(current_path + [name])
157
+ # mark as used
158
+ store[lvar] = true
159
+ s(:lvar, lvar)
160
+ end
161
+
162
+ def write_pred(name, node)
163
+ return node if terminated?
164
+ @count += 1
165
+ lvar = :"__p_#{count}__"
166
+ predicates_by_path[current_path + [name]] = lvar
167
+ store[lvar] = false
168
+
169
+ s(:lvasgn,
170
+ lvar,
171
+ node)
172
+ end
173
+
174
+ def process(ast)
175
+ Processor.new(self).process(ast)
176
+ end
177
+
178
+ private
179
+
180
+ def s(type, *children)
181
+ ::Parser::AST::Node.new(type, children)
182
+ end
183
+ end
184
+
185
+ # rubocop:disable Style/MissingRespondToMissing
186
+ class Noop < Base
187
+ # Return node itself, no memoization
188
+ def method_missing(mid, node, *__rest__)
189
+ node
190
+ end
191
+ end
192
+ # rubocop:enable Style/MethodMissingSuper
193
+ # rubocop:enable Style/MissingRespondToMissing
194
+
195
+ class CaseIn < Base
196
+ def const(node, const)
197
+ node
198
+ end
199
+
200
+ def respond_to_deconstruct(node)
201
+ predicate_clause(:respond_to_deconstruct, node)
202
+ end
203
+
204
+ def array_size(node, size)
205
+ predicate_clause(:"array_size_#{size}", node)
206
+ end
207
+
208
+ def array_deconstructed(node)
209
+ predicate_clause(:array_deconstructed, node)
210
+ end
211
+
212
+ def hash_deconstructed(node, keys)
213
+ predicate_clause(:"hash_deconstructed_#{keys.join("_p_")}", node)
214
+ end
215
+
216
+ def respond_to_deconstruct_keys(node)
217
+ predicate_clause(:respond_to_deconstruct_keys, node)
218
+ end
219
+
220
+ def hash_keys(node, keys)
221
+ keys = keys.map { |key| key.is_a?(::Parser::AST::Node) ? key.children.first : key }
222
+
223
+ predicate_clause(:"hash_keys_#{keys.join("_p_")}", node)
224
+ end
225
+ end
226
+ end
227
+
228
+ class PatternMatching < Base
229
+ NAME = "pattern-matching"
230
+ SYNTAX_PROBE = "case 0; in 0; true; else; 1; end"
231
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.7.0")
232
+
233
+ MATCHEE = :__m__
234
+ MATCHEE_ARR = :__m_arr__
235
+ MATCHEE_HASH = :__m_hash__
236
+
237
+ ALTERNATION_MARKER = :__alt__
238
+ CURRENT_HASH_KEY = :__chk__
239
+
240
+ def on_case_match(node)
241
+ context.track! self
242
+
243
+ @deconstructed_keys = {}
244
+ @predicates = Predicates::CaseIn.new
245
+ @lvars = []
246
+
247
+ matchee_ast =
248
+ s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
249
+
250
+ patterns = locals.with(
251
+ matchee: MATCHEE,
252
+ arr: MATCHEE_ARR,
253
+ hash: MATCHEE_HASH
254
+ ) do
255
+ build_case_when(node.children[1..-1])
256
+ end
257
+
258
+ case_clause = predicates.process(s(:case, *patterns))
259
+
260
+ rewrite_case_in! node, matchee_ast, case_clause
261
+
262
+ node.updated(
263
+ :kwbegin,
264
+ [
265
+ matchee_ast, case_clause
266
+ ]
267
+ )
268
+ end
269
+
270
+ def on_match_pattern(node)
271
+ context.track! self
272
+
273
+ @deconstructed_keys = {}
274
+ @predicates = Predicates::Noop.new
275
+ @lvars = []
276
+
277
+ matchee =
278
+ s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
279
+
280
+ pattern =
281
+ locals.with(
282
+ matchee: MATCHEE,
283
+ arr: MATCHEE_ARR,
284
+ hash: MATCHEE_HASH
285
+ ) do
286
+ with_declared_locals do
287
+ send(
288
+ :"#{node.children[1].type}_clause",
289
+ node.children[1]
290
+ )
291
+ end.then do |node|
292
+ s(:begin,
293
+ s(:or,
294
+ node,
295
+ no_matching_pattern))
296
+ end
297
+ end
298
+
299
+ node.updated(
300
+ :and,
301
+ [
302
+ matchee,
303
+ pattern
304
+ ]
305
+ ).tap do |new_node|
306
+ replace(node.loc.expression, inline_blocks(unparse(new_node)))
307
+ end
308
+ end
309
+
310
+ alias on_in_match on_match_pattern
311
+
312
+ def on_match_pattern_p(node)
313
+ context.track! self
314
+
315
+ @deconstructed_keys = {}
316
+ @predicates = Predicates::Noop.new
317
+ @lvars = []
318
+
319
+ matchee =
320
+ s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
321
+
322
+ pattern =
323
+ locals.with(
324
+ matchee: MATCHEE,
325
+ arr: MATCHEE_ARR,
326
+ hash: MATCHEE_HASH
327
+ ) do
328
+ with_declared_locals do
329
+ send(
330
+ :"#{node.children[1].type}_clause",
331
+ node.children[1]
332
+ )
333
+ end
334
+ end
335
+
336
+ node.updated(
337
+ :and,
338
+ [
339
+ matchee,
340
+ pattern
341
+ ]
342
+ ).tap do |new_node|
343
+ replace(node.loc.expression, inline_blocks(unparse(new_node)))
344
+ end
345
+ end
346
+
347
+ private
348
+
349
+ def rewrite_case_in!(node, matchee, new_node)
350
+ replace(node.loc.keyword, "case; when (#{unparse(matchee)}) && false")
351
+ remove(node.children[0].loc.expression)
352
+
353
+ node.children[1..-1].each.with_index do |clause, i|
354
+ if clause&.type == :in_pattern
355
+ # handle multiline clauses differently
356
+ if clause.loc.last_line > clause.children[0].loc.last_line + 1
357
+ height = clause.loc.last_line - clause.children[0].loc.last_line
358
+ padding = "\n" * height
359
+ body_indent = " " * clause.children[2].loc.column
360
+ replace(
361
+ clause.loc.expression,
362
+ "when #{inline_blocks(unparse(new_node.children[i].children[0]))}" \
363
+ "#{padding}" \
364
+ "#{body_indent}#{clause.children[2].loc.expression.source}"
365
+ )
366
+ else
367
+ replace(
368
+ clause.loc.keyword.end.join(clause.children[0].loc.expression.end),
369
+ inline_blocks(unparse(new_node.children[i].children[0]))
370
+ )
371
+ remove(clause.children[1].loc.expression) if clause.children[1]
372
+ replace(clause.loc.keyword, "when ")
373
+ end
374
+ elsif clause.nil?
375
+ insert_after(node.children[-2].loc.expression, "; else; #{unparse(new_node.children.last)}")
376
+ end
377
+ end
378
+ end
379
+
380
+ def build_case_when(nodes)
381
+ else_clause = nil
382
+ clauses = []
383
+
384
+ nodes.each do |clause|
385
+ if clause&.type == :in_pattern
386
+ clauses << build_when_clause(clause)
387
+ else
388
+ else_clause = process(clause)
389
+ end
390
+ end
391
+
392
+ else_clause = (else_clause || no_matching_pattern).then do |node|
393
+ next node unless node.type == :empty_else
394
+ nil
395
+ end
396
+
397
+ clauses << else_clause
398
+ clauses
399
+ end
400
+
401
+ def build_when_clause(clause)
402
+ predicates.reset!
403
+ [
404
+ with_declared_locals do
405
+ with_guard(
406
+ send(
407
+ :"#{clause.children[0].type}_clause",
408
+ clause.children[0]
409
+ ),
410
+ clause.children[1] # guard
411
+ )
412
+ end,
413
+ process(clause.children[2] || s(:nil)) # expression
414
+ ].then do |children|
415
+ s(:when, *children)
416
+ end
417
+ end
418
+
419
+ def const_pattern_clause(node, right = s(:lvar, locals[:matchee]))
420
+ const, pattern = *node.children
421
+
422
+ predicates.const(case_eq_clause(const, right), const).then do |node|
423
+ next node if pattern.nil?
424
+
425
+ s(:begin,
426
+ s(:and,
427
+ node,
428
+ send(:"#{pattern.type}_clause", pattern, right)))
429
+ end
430
+ end
431
+
432
+ def match_alt_clause(node, matchee = s(:lvar, locals[:matchee]))
433
+ children = locals.with(ALTERNATION_MARKER => true) do
434
+ node.children.map.with_index do |child, i|
435
+ predicates.terminate! if i == 1
436
+ send :"#{child.type}_clause", child, matchee
437
+ end
438
+ end
439
+ s(:begin, s(:or, *children))
440
+ end
441
+
442
+ def match_as_clause(node, right = s(:lvar, locals[:matchee]))
443
+ s(:begin,
444
+ s(:and,
445
+ send(:"#{node.children[0].type}_clause", node.children[0], right),
446
+ match_var_clause(node.children[1], right)))
447
+ end
448
+
449
+ def match_var_clause(node, left = s(:lvar, locals[:matchee]))
450
+ var = node.children[0]
451
+ return s(:true) if var == :_
452
+
453
+ check_match_var_alternation!(var)
454
+
455
+ s(:begin,
456
+ s(:or,
457
+ s(:begin, build_var_assignment(var, left)),
458
+ s(:true)))
459
+ end
460
+
461
+ def pin_clause(node, right = s(:lvar, locals[:matchee]))
462
+ predicates.terminate!
463
+ case_eq_clause node.children[0], right
464
+ end
465
+
466
+ def case_eq_clause(node, right = s(:lvar, locals[:matchee]))
467
+ predicates.terminate!
468
+ s(:begin, s(:send,
469
+ process(node), :===, right))
470
+ end
471
+
472
+ #=========== ARRAY PATTERN (START) ===============
473
+
474
+ def array_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
475
+ deconstruct_node(matchee).then do |dnode|
476
+ size_check = nil
477
+ # if there is no rest or tail, match the size first
478
+ unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
479
+ size_check = predicates.array_size(
480
+ s(:begin,
481
+ s(:send,
482
+ node.children.size.to_ast_node,
483
+ :==,
484
+ s(:send, s(:lvar, locals[:arr]), :size))),
485
+ node.children.size
486
+ )
487
+ end
488
+
489
+ right =
490
+ if node.children.empty?
491
+ case_eq_clause(s(:array), s(:lvar, locals[:arr]))
492
+ elsif node.children.size > 1 && node.children.first.type == :match_rest && node.children.last.type == :match_rest
493
+ array_find(*node.children)
494
+ else
495
+ array_element(0, *node.children)
496
+ end
497
+
498
+ right = s(:and, size_check, right) if size_check
499
+
500
+ s(:begin,
501
+ s(:and,
502
+ dnode,
503
+ right))
504
+ end
505
+ end
506
+
507
+ alias array_pattern_with_tail_clause array_pattern_clause
508
+ alias find_pattern_clause array_pattern_clause
509
+
510
+ def deconstruct_node(matchee)
511
+ context.use_ruby_next!
512
+
513
+ # we do not memoize respond_to_check for arrays, 'cause
514
+ # we can memoize is together with #deconstruct result
515
+ respond_check = respond_to_check(matchee, :deconstruct)
516
+ right = s(:send, matchee, :deconstruct)
517
+
518
+ predicates.array_deconstructed(
519
+ s(:and,
520
+ respond_check,
521
+ s(:begin,
522
+ s(:and,
523
+ s(:begin,
524
+ s(:or,
525
+ s(:begin, s(:lvasgn, locals[:arr], right)),
526
+ s(:true))),
527
+ s(:begin,
528
+ s(:or,
529
+ s(:send,
530
+ s(:const, nil, :Array), :===, s(:lvar, locals[:arr])),
531
+ raise_error(:TypeError, "#deconstruct must return Array"))))))
532
+ )
533
+ end
534
+
535
+ def array_element(index, head, *tail)
536
+ return array_match_rest(index, head, *tail) if head.type == :match_rest
537
+
538
+ send("#{head.type}_array_element", head, index).then do |node|
539
+ next node if tail.empty?
540
+
541
+ s(:begin,
542
+ s(:and,
543
+ node,
544
+ array_element(index + 1, *tail)))
545
+ end
546
+ end
547
+
548
+ # [*a, 1, 2, *] -> arr.find.with_index { |_, i| (a = arr.take(i)) && arr[i] == 1 && arr[i + 1] == 2 }
549
+ def array_find(head, *nodes, tail)
550
+ index = s(:lvar, :__i__)
551
+
552
+ head_match =
553
+ unless head.children.empty?
554
+ # we only need to call this to track the lvar usage
555
+ build_var_assignment(head.children[0].children[0])
556
+
557
+ arr_take = s(:send,
558
+ s(:lvar, locals[:arr]),
559
+ :take,
560
+ index)
561
+
562
+ match_var_clause(head.children[0], arr_take)
563
+ end
564
+
565
+ tail_match =
566
+ unless tail.children.empty?
567
+ # we only need to call this to track the lvar usage
568
+ build_var_assignment(tail.children[0].children[0])
569
+
570
+ match_var_clause(tail.children[0], arr_slice(index + nodes.size, -1))
571
+ end
572
+
573
+ nodes.each do |node|
574
+ if node.type == :match_var
575
+ # we only need to call this to track the lvar usage
576
+ build_var_assignment(node.children[0])
577
+ elsif node.type == :match_as
578
+ # we only need to call this to track the lvar usage
579
+ build_var_assignment(node.children[1].children[0])
580
+ end
581
+ end
582
+
583
+ pattern = array_rest_element(*nodes, index).then do |needle|
584
+ next needle unless head_match
585
+ s(:begin,
586
+ s(:and,
587
+ needle,
588
+ head_match))
589
+ end.then do |headed_needle|
590
+ next headed_needle unless tail_match
591
+
592
+ s(:begin,
593
+ s(:and,
594
+ headed_needle,
595
+ tail_match))
596
+ end
597
+
598
+ s(:block,
599
+ s(:send,
600
+ s(:send,
601
+ s(:lvar, locals[:arr]),
602
+ :find),
603
+ :with_index),
604
+ s(:args,
605
+ s(:arg, :_),
606
+ s(:arg, :__i__)),
607
+ pattern)
608
+ end
609
+
610
+ def array_match_rest(index, node, *tail)
611
+ size = tail.size + 1
612
+ child = node.children[0]
613
+
614
+ rest = arr_slice(index, -size).then do |r|
615
+ next r unless child
616
+
617
+ match_var_clause(
618
+ child,
619
+ r
620
+ )
621
+ end
622
+
623
+ return rest if tail.empty?
624
+
625
+ s(:begin,
626
+ s(:and,
627
+ rest,
628
+ array_rest_element(*tail, -(size - 1))))
629
+ end
630
+
631
+ def array_rest_element(head, *tail, index)
632
+ send("#{head.type}_array_element", head, index).then do |node|
633
+ next node if tail.empty?
634
+
635
+ s(:begin,
636
+ s(:and,
637
+ node,
638
+ array_rest_element(*tail, index + 1)))
639
+ end
640
+ end
641
+
642
+ def array_pattern_array_element(node, index)
643
+ element = arr_item_at(index)
644
+ locals.with(arr: locals[:arr, index]) do
645
+ predicates.push :"i#{index}"
646
+ array_pattern_clause(node, element).tap { predicates.pop }
647
+ end
648
+ end
649
+
650
+ def find_pattern_array_element(node, index)
651
+ element = arr_item_at(index)
652
+ locals.with(arr: locals[:arr, index]) do
653
+ predicates.push :"i#{index}"
654
+ find_pattern_clause(node, element).tap { predicates.pop }
655
+ end
656
+ end
657
+
658
+ def hash_pattern_array_element(node, index)
659
+ element = arr_item_at(index)
660
+ locals.with(hash: locals[:arr, index]) do
661
+ predicates.push :"i#{index}"
662
+ hash_pattern_clause(node, element).tap { predicates.pop }
663
+ end
664
+ end
665
+
666
+ def const_pattern_array_element(node, index)
667
+ element = arr_item_at(index)
668
+ locals.with(arr: locals[:arr, index]) do
669
+ predicates.push :"i#{index}"
670
+ const_pattern_clause(node, element).tap { predicates.pop }
671
+ end
672
+ end
673
+
674
+ def match_alt_array_element(node, index)
675
+ children = node.children.map do |child, i|
676
+ send :"#{child.type}_array_element", child, index
677
+ end
678
+ s(:begin, s(:or, *children))
679
+ end
680
+
681
+ def match_var_array_element(node, index)
682
+ element = arr_item_at(index)
683
+ locals.with(arr: locals[:arr, index]) do
684
+ predicates.push :"i#{index}"
685
+ match_var_clause(node, element).tap { predicates.pop }
686
+ end
687
+ end
688
+
689
+ def match_as_array_element(node, index)
690
+ element = arr_item_at(index)
691
+ locals.with(arr: locals[:arr, index]) do
692
+ predicates.push :"i#{index}"
693
+ match_as_clause(node, element).tap { predicates.pop }
694
+ end
695
+ end
696
+
697
+ def pin_array_element(node, index)
698
+ case_eq_array_element node.children[0], index
699
+ end
700
+
701
+ def case_eq_array_element(node, index)
702
+ case_eq_clause(node, arr_item_at(index))
703
+ end
704
+
705
+ def arr_item_at(index, arr = s(:lvar, locals[:arr]))
706
+ s(:index, arr, index.to_ast_node)
707
+ end
708
+
709
+ def arr_slice(lindex, rindex, arr = s(:lvar, locals[:arr]))
710
+ s(:index,
711
+ arr,
712
+ s(:irange,
713
+ lindex.to_ast_node,
714
+ rindex.to_ast_node))
715
+ end
716
+
717
+ #=========== ARRAY PATTERN (END) ===============
718
+
719
+ #=========== HASH PATTERN (START) ===============
720
+
721
+ def hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
722
+ # Optimization: avoid hash modifications when not needed
723
+ # (we use #dup and #delete when "reading" values when **rest is present
724
+ # to assign the rest of the hash copy to it)
725
+ @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern }
726
+ keys = hash_pattern_destruction_keys(node.children)
727
+
728
+ specified_key_names = hash_pattern_keys(node.children)
729
+
730
+ deconstruct_keys_node(keys, matchee).then do |dnode|
731
+ right =
732
+ if node.children.empty?
733
+ case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
734
+ elsif specified_key_names.empty?
735
+ hash_element(*node.children)
736
+ else
737
+ s(:begin,
738
+ s(:and,
739
+ having_hash_keys(specified_key_names),
740
+ hash_element(*node.children)))
741
+ end
742
+
743
+ predicates.pop
744
+
745
+ next dnode if right.nil?
746
+
747
+ s(:begin,
748
+ s(:and,
749
+ dnode,
750
+ right))
751
+ end
752
+ end
753
+
754
+ def hash_pattern_keys(children)
755
+ children.filter_map do |child|
756
+ # Skip ** without var
757
+ next if child.type == :match_rest || child.type == :match_nil_pattern
758
+
759
+ send("#{child.type}_hash_key", child)
760
+ end
761
+ end
762
+
763
+ def hash_pattern_destruction_keys(children)
764
+ return s(:nil) if children.empty?
765
+
766
+ children.filter_map do |child|
767
+ # Skip ** without var
768
+ next if child.type == :match_rest && child.children.empty?
769
+ return s(:nil) if child.type == :match_rest || child.type == :match_nil_pattern
770
+
771
+ send("#{child.type}_hash_key", child)
772
+ end.then { |keys| s(:array, *keys) }
773
+ end
774
+
775
+ def pair_hash_key(node)
776
+ node.children[0]
777
+ end
778
+
779
+ def match_var_hash_key(node)
780
+ check_match_var_alternation! node.children[0]
781
+
782
+ s(:sym, node.children[0])
783
+ end
784
+
785
+ def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
786
+ # Use original hash returned by #deconstruct_keys if not **rest matching,
787
+ # 'cause it remains immutable
788
+ deconstruct_name = @hash_match_rest ? locals[:hash, :src] : locals[:hash]
789
+
790
+ # Duplicate the source hash when matching **rest, 'cause we mutate it
791
+ hash_dup =
792
+ if @hash_match_rest
793
+ s(:begin, s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup)))
794
+ else
795
+ s(:true)
796
+ end
797
+
798
+ context.use_ruby_next!
799
+
800
+ respond_to_checked = predicates.pred?(:respond_to_deconstruct_keys)
801
+ respond_check = predicates.respond_to_deconstruct_keys(respond_to_check(matchee, :deconstruct_keys))
802
+
803
+ key_names = keys.children.map { |node| node.children.last }
804
+ predicates.push locals[:hash]
805
+
806
+ s(:begin, s(:lvasgn, deconstruct_name,
807
+ s(:send,
808
+ matchee, :deconstruct_keys, keys))).then do |dnode|
809
+ next dnode if respond_to_checked
810
+
811
+ s(:and,
812
+ respond_check,
813
+ s(:begin,
814
+ s(:and,
815
+ s(:begin,
816
+ s(:or,
817
+ dnode,
818
+ s(:true))),
819
+ s(:begin,
820
+ s(:or,
821
+ s(:send,
822
+ s(:const, nil, :Hash), :===, s(:lvar, deconstruct_name)),
823
+ raise_error(:TypeError, "#deconstruct_keys must return Hash"))))))
824
+ end.then do |dnode|
825
+ predicates.hash_deconstructed(dnode, key_names)
826
+ end.then do |dnode|
827
+ next dnode unless @hash_match_rest
828
+
829
+ s(:begin,
830
+ s(:and,
831
+ dnode,
832
+ hash_dup))
833
+ end
834
+ end
835
+
836
+ def hash_pattern_hash_element(node, key)
837
+ element = hash_value_at(key)
838
+ key_index = deconstructed_key(key)
839
+ locals.with(hash: locals[:hash, key_index]) do
840
+ predicates.push :"k#{key_index}"
841
+ hash_pattern_clause(node, element).tap { predicates.pop }
842
+ end
843
+ end
844
+
845
+ def array_pattern_hash_element(node, key)
846
+ element = hash_value_at(key)
847
+ key_index = deconstructed_key(key)
848
+ locals.with(arr: locals[:hash, key_index]) do
849
+ predicates.push :"k#{key_index}"
850
+ array_pattern_clause(node, element).tap { predicates.pop }
851
+ end
852
+ end
853
+
854
+ def find_pattern_hash_element(node, key)
855
+ element = hash_value_at(key)
856
+ key_index = deconstructed_key(key)
857
+ locals.with(arr: locals[:hash, key_index]) do
858
+ predicates.push :"k#{key_index}"
859
+ find_pattern_clause(node, element).tap { predicates.pop }
860
+ end
861
+ end
862
+
863
+ def const_pattern_hash_element(node, key)
864
+ element = hash_value_at(key)
865
+ key_index = deconstructed_key(key)
866
+ locals.with(hash: locals[:hash, key_index]) do
867
+ predicates.push :"k#{key_index}"
868
+ const_pattern_clause(node, element).tap { predicates.pop }
869
+ end
870
+ end
871
+
872
+ def hash_element(head, *tail)
873
+ send("#{head.type}_hash_element", head).then do |node|
874
+ next node if tail.empty?
875
+
876
+ right = hash_element(*tail)
877
+
878
+ next node if right.nil?
879
+
880
+ s(:begin,
881
+ s(:and,
882
+ node,
883
+ right))
884
+ end
885
+ end
886
+
887
+ def pair_hash_element(node, _key = nil)
888
+ key, val = *node.children
889
+ send("#{val.type}_hash_element", val, key)
890
+ end
891
+
892
+ def match_alt_hash_element(node, key)
893
+ element_node = s(:begin, s(:lvasgn, locals[:hash, :el], hash_value_at(key)))
894
+
895
+ children = locals.with(hash_element: locals[:hash, :el]) do
896
+ node.children.map do |child, i|
897
+ send :"#{child.type}_hash_element", child, key
898
+ end
899
+ end
900
+
901
+ s(:begin,
902
+ s(:and,
903
+ s(:begin,
904
+ s(:or,
905
+ element_node,
906
+ s(:true))),
907
+ s(:begin,
908
+ s(:or, *children))))
909
+ end
910
+
911
+ def match_as_hash_element(node, key)
912
+ element = hash_value_at(key)
913
+ key_index = deconstructed_key(key)
914
+ locals.with(hash: locals[:hash, key_index]) do
915
+ predicates.push :"k#{key_index}"
916
+ match_as_clause(node, element).tap { predicates.pop }
917
+ end
918
+ end
919
+
920
+ def match_var_hash_element(node, key = nil)
921
+ key ||= node.children[0]
922
+ element = hash_value_at(key)
923
+ key_index = deconstructed_key(key)
924
+ locals.with(hash: locals[:hash, key_index]) do
925
+ predicates.push :"k#{key_index}"
926
+ match_var_clause(node, element).tap { predicates.pop }
927
+ end
928
+ end
929
+
930
+ def match_nil_pattern_hash_element(node, _key = nil)
931
+ s(:send,
932
+ s(:lvar, locals[:hash]),
933
+ :empty?)
934
+ end
935
+
936
+ def match_rest_hash_element(node, _key = nil)
937
+ # case {}; in **; end
938
+ return if node.children.empty?
939
+
940
+ child = node.children[0]
941
+
942
+ raise ArgumentError, "Unknown hash match_rest child: #{child.type}" unless child.type == :match_var
943
+
944
+ match_var_clause(child, s(:lvar, locals[:hash]))
945
+ end
946
+
947
+ def pin_hash_element(node, index)
948
+ case_eq_hash_element node.children[0], index
949
+ end
950
+
951
+ def case_eq_hash_element(node, key)
952
+ case_eq_clause node, hash_value_at(key)
953
+ end
954
+
955
+ def hash_value_at(key, hash = s(:lvar, locals[:hash]))
956
+ return s(:lvar, locals.fetch(:hash_element)) if locals.key?(:hash_element)
957
+
958
+ if @hash_match_rest
959
+ s(:send,
960
+ hash, :delete,
961
+ key.to_ast_node)
962
+ else
963
+ s(:index,
964
+ hash,
965
+ key.to_ast_node)
966
+ end
967
+ end
968
+
969
+ def hash_has_key(key, hash = s(:lvar, locals[:hash]))
970
+ s(:send,
971
+ hash, :key?,
972
+ key.to_ast_node)
973
+ end
974
+
975
+ def having_hash_keys(keys, hash = s(:lvar, locals[:hash]))
976
+ keys.reduce(nil) do |acc, key|
977
+ pnode = hash_has_key(key, hash)
978
+ next pnode unless acc
979
+
980
+ s(:begin,
981
+ s(:and, acc, pnode))
982
+ end.then do |node|
983
+ predicates.hash_keys(node, keys)
984
+ end
985
+ end
986
+
987
+ #=========== HASH PATTERN (END) ===============
988
+
989
+ def with_guard(node, guard)
990
+ return node unless guard
991
+
992
+ s(:begin,
993
+ s(:and,
994
+ node,
995
+ guard.children[0])).then do |expr|
996
+ next expr unless guard.type == :unless_guard
997
+ s(:send, expr, :!)
998
+ end
999
+ end
1000
+
1001
+ def with_declared_locals
1002
+ lvars.clear
1003
+ node = yield
1004
+
1005
+ return node if lvars.empty?
1006
+
1007
+ # We need to declare match lvars outside of the outer `find` block,
1008
+ # so we do that for that whole pattern
1009
+ locals_declare = s(:begin, s(:masgn,
1010
+ s(:mlhs, *lvars.uniq.map { s(:lvasgn, _1) }),
1011
+ s(:nil)))
1012
+
1013
+ s(:begin,
1014
+ s(:or,
1015
+ locals_declare,
1016
+ node))
1017
+ end
1018
+
1019
+ def no_matching_pattern
1020
+ raise_error(
1021
+ :NoMatchingPatternError,
1022
+ s(:send,
1023
+ s(:lvar, locals[:matchee]), :inspect)
1024
+ )
1025
+ end
1026
+
1027
+ def raise_error(type, msg = "")
1028
+ s(:send, s(:const, nil, :Kernel), :raise,
1029
+ s(:const, nil, type),
1030
+ msg.to_ast_node)
1031
+ end
1032
+
1033
+ # Add respond_to? check
1034
+ def respond_to_check(node, mid)
1035
+ s(:send, node, :respond_to?, mid.to_ast_node)
1036
+ end
1037
+
1038
+ def respond_to_missing?(mid, *__rest__)
1039
+ return true if mid.to_s.match?(/_(clause|array_element)/)
1040
+ super
1041
+ end
1042
+
1043
+ def method_missing(mid, *args, &block)
1044
+ mid = mid.to_s
1045
+ return case_eq_clause(*args) if mid.match?(/_clause$/)
1046
+ return case_eq_array_element(*args) if mid.match?(/_array_element$/)
1047
+ return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
1048
+ super
1049
+ end
1050
+
1051
+ private
1052
+
1053
+ attr_reader :deconstructed_keys, :predicates, :lvars
1054
+
1055
+ # Raise SyntaxError if match-var is used within alternation
1056
+ # https://github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900
1057
+ def check_match_var_alternation!(name)
1058
+ return unless locals.key?(ALTERNATION_MARKER)
1059
+
1060
+ if name.is_a?(::Parser::AST::Node)
1061
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name.children.first})"
1062
+ end
1063
+
1064
+ return if name.start_with?("_")
1065
+
1066
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name})"
1067
+ end
1068
+
1069
+ def deconstructed_key(key)
1070
+ return deconstructed_keys[key] if deconstructed_keys.key?(key)
1071
+
1072
+ deconstructed_keys[key] = :"k#{deconstructed_keys.size}"
1073
+ end
1074
+
1075
+ # Unparser generates `do .. end` or `{ ... }` multiline blocks, we want to
1076
+ # have single-line blocks with `{ ... }`.
1077
+ def inline_blocks(source)
1078
+ source.gsub(/(?:do|{) \|_, __i__\|\n\s*([^\n]+)\n\s*(?:end|})/, '{ |_, __i__| \1 }')
1079
+ end
1080
+
1081
+ # Value could be omitted for mass assignment
1082
+ def build_var_assignment(var, value = nil)
1083
+ unless var.is_a?(::Parser::AST::Node)
1084
+ lvars << var
1085
+ return s(:lvasgn, *[var, value].compact)
1086
+ end
1087
+
1088
+ asign_type = :"#{var.type.to_s[0]}vasgn"
1089
+
1090
+ s(asign_type, *[var.children[0], value].compact)
1091
+ end
1092
+ end
1093
+ end
1094
+ end
1095
+ end