ruby-next-core 0.14.0 → 0.15.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.
@@ -0,0 +1,1060 @@
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, *)
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)))
429
+ end
430
+ end
431
+
432
+ def match_alt_clause(node)
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
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 match_alt_array_element(node, index)
667
+ children = node.children.map do |child, i|
668
+ send :"#{child.type}_array_element", child, index
669
+ end
670
+ s(:begin, s(:or, *children))
671
+ end
672
+
673
+ def match_var_array_element(node, index)
674
+ match_var_clause(node, arr_item_at(index))
675
+ end
676
+
677
+ def match_as_array_element(node, index)
678
+ match_as_clause(node, arr_item_at(index))
679
+ end
680
+
681
+ def pin_array_element(node, index)
682
+ case_eq_array_element node.children[0], index
683
+ end
684
+
685
+ def case_eq_array_element(node, index)
686
+ case_eq_clause(node, arr_item_at(index))
687
+ end
688
+
689
+ def arr_item_at(index, arr = s(:lvar, locals[:arr]))
690
+ s(:index, arr, index.to_ast_node)
691
+ end
692
+
693
+ def arr_slice(lindex, rindex, arr = s(:lvar, locals[:arr]))
694
+ s(:index,
695
+ arr,
696
+ s(:irange,
697
+ lindex.to_ast_node,
698
+ rindex.to_ast_node))
699
+ end
700
+
701
+ #=========== ARRAY PATTERN (END) ===============
702
+
703
+ #=========== HASH PATTERN (START) ===============
704
+
705
+ def hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
706
+ # Optimization: avoid hash modifications when not needed
707
+ # (we use #dup and #delete when "reading" values when **rest is present
708
+ # to assign the rest of the hash copy to it)
709
+ @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern }
710
+ keys = hash_pattern_destruction_keys(node.children)
711
+
712
+ specified_key_names = hash_pattern_keys(node.children)
713
+
714
+ deconstruct_keys_node(keys, matchee).then do |dnode|
715
+ right =
716
+ if node.children.empty?
717
+ case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
718
+ elsif specified_key_names.empty?
719
+ hash_element(*node.children)
720
+ else
721
+ s(:begin,
722
+ s(:and,
723
+ having_hash_keys(specified_key_names),
724
+ hash_element(*node.children)))
725
+ end
726
+
727
+ predicates.pop
728
+
729
+ next dnode if right.nil?
730
+
731
+ s(:begin,
732
+ s(:and,
733
+ dnode,
734
+ right))
735
+ end
736
+ end
737
+
738
+ def hash_pattern_keys(children)
739
+ children.filter_map do |child|
740
+ # Skip ** without var
741
+ next if child.type == :match_rest || child.type == :match_nil_pattern
742
+
743
+ send("#{child.type}_hash_key", child)
744
+ end
745
+ end
746
+
747
+ def hash_pattern_destruction_keys(children)
748
+ return s(:nil) if children.empty?
749
+
750
+ children.filter_map do |child|
751
+ # Skip ** without var
752
+ next if child.type == :match_rest && child.children.empty?
753
+ return s(:nil) if child.type == :match_rest || child.type == :match_nil_pattern
754
+
755
+ send("#{child.type}_hash_key", child)
756
+ end.then { |keys| s(:array, *keys) }
757
+ end
758
+
759
+ def pair_hash_key(node)
760
+ node.children[0]
761
+ end
762
+
763
+ def match_var_hash_key(node)
764
+ check_match_var_alternation! node.children[0]
765
+
766
+ s(:sym, node.children[0])
767
+ end
768
+
769
+ def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
770
+ # Use original hash returned by #deconstruct_keys if not **rest matching,
771
+ # 'cause it remains immutable
772
+ deconstruct_name = @hash_match_rest ? locals[:hash, :src] : locals[:hash]
773
+
774
+ # Duplicate the source hash when matching **rest, 'cause we mutate it
775
+ hash_dup =
776
+ if @hash_match_rest
777
+ s(:begin, s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup)))
778
+ else
779
+ s(:true)
780
+ end
781
+
782
+ context.use_ruby_next!
783
+
784
+ respond_to_checked = predicates.pred?(:respond_to_deconstruct_keys)
785
+ respond_check = predicates.respond_to_deconstruct_keys(respond_to_check(matchee, :deconstruct_keys))
786
+
787
+ key_names = keys.children.map { |node| node.children.last }
788
+ predicates.push locals[:hash]
789
+
790
+ s(:begin, s(:lvasgn, deconstruct_name,
791
+ s(:send,
792
+ matchee, :deconstruct_keys, keys))).then do |dnode|
793
+ next dnode if respond_to_checked
794
+
795
+ s(:and,
796
+ respond_check,
797
+ s(:begin,
798
+ s(:and,
799
+ s(:begin,
800
+ s(:or,
801
+ dnode,
802
+ s(:true))),
803
+ s(:begin,
804
+ s(:or,
805
+ s(:send,
806
+ s(:const, nil, :Hash), :===, s(:lvar, deconstruct_name)),
807
+ raise_error(:TypeError, "#deconstruct_keys must return Hash"))))))
808
+ end.then do |dnode|
809
+ predicates.hash_deconstructed(dnode, key_names)
810
+ end.then do |dnode|
811
+ next dnode unless @hash_match_rest
812
+
813
+ s(:begin,
814
+ s(:and,
815
+ dnode,
816
+ hash_dup))
817
+ end
818
+ end
819
+
820
+ def hash_pattern_hash_element(node, key)
821
+ element = hash_value_at(key)
822
+ key_index = deconstructed_key(key)
823
+ locals.with(hash: locals[:hash, key_index]) do
824
+ predicates.push :"k#{key_index}"
825
+ hash_pattern_clause(node, element).tap { predicates.pop }
826
+ end
827
+ end
828
+
829
+ def array_pattern_hash_element(node, key)
830
+ element = hash_value_at(key)
831
+ key_index = deconstructed_key(key)
832
+ locals.with(arr: locals[:hash, key_index]) do
833
+ predicates.push :"k#{key_index}"
834
+ array_pattern_clause(node, element).tap { predicates.pop }
835
+ end
836
+ end
837
+
838
+ def find_pattern_hash_element(node, key)
839
+ element = hash_value_at(key)
840
+ key_index = deconstructed_key(key)
841
+ locals.with(arr: locals[:hash, key_index]) do
842
+ predicates.push :"k#{key_index}"
843
+ find_pattern_clause(node, element).tap { predicates.pop }
844
+ end
845
+ end
846
+
847
+ def hash_element(head, *tail)
848
+ send("#{head.type}_hash_element", head).then do |node|
849
+ next node if tail.empty?
850
+
851
+ right = hash_element(*tail)
852
+
853
+ next node if right.nil?
854
+
855
+ s(:begin,
856
+ s(:and,
857
+ node,
858
+ right))
859
+ end
860
+ end
861
+
862
+ def pair_hash_element(node, _key = nil)
863
+ key, val = *node.children
864
+ send("#{val.type}_hash_element", val, key)
865
+ end
866
+
867
+ def match_alt_hash_element(node, key)
868
+ element_node = s(:begin, s(:lvasgn, locals[:hash, :el], hash_value_at(key)))
869
+
870
+ children = locals.with(hash_element: locals[:hash, :el]) do
871
+ node.children.map do |child, i|
872
+ send :"#{child.type}_hash_element", child, key
873
+ end
874
+ end
875
+
876
+ s(:begin,
877
+ s(:and,
878
+ s(:begin,
879
+ s(:or,
880
+ element_node,
881
+ s(:true))),
882
+ s(:begin,
883
+ s(:or, *children))))
884
+ end
885
+
886
+ def match_as_hash_element(node, key)
887
+ match_as_clause(node, hash_value_at(key))
888
+ end
889
+
890
+ def match_var_hash_element(node, key = nil)
891
+ key ||= node.children[0]
892
+ match_var_clause(node, hash_value_at(key))
893
+ end
894
+
895
+ def match_nil_pattern_hash_element(node, _key = nil)
896
+ s(:send,
897
+ s(:lvar, locals[:hash]),
898
+ :empty?)
899
+ end
900
+
901
+ def match_rest_hash_element(node, _key = nil)
902
+ # case {}; in **; end
903
+ return if node.children.empty?
904
+
905
+ child = node.children[0]
906
+
907
+ raise ArgumentError, "Unknown hash match_rest child: #{child.type}" unless child.type == :match_var
908
+
909
+ match_var_clause(child, s(:lvar, locals[:hash]))
910
+ end
911
+
912
+ def pin_hash_element(node, index)
913
+ case_eq_hash_element node.children[0], index
914
+ end
915
+
916
+ def case_eq_hash_element(node, key)
917
+ case_eq_clause node, hash_value_at(key)
918
+ end
919
+
920
+ def hash_value_at(key, hash = s(:lvar, locals[:hash]))
921
+ return s(:lvar, locals.fetch(:hash_element)) if locals.key?(:hash_element)
922
+
923
+ if @hash_match_rest
924
+ s(:send,
925
+ hash, :delete,
926
+ key.to_ast_node)
927
+ else
928
+ s(:index,
929
+ hash,
930
+ key.to_ast_node)
931
+ end
932
+ end
933
+
934
+ def hash_has_key(key, hash = s(:lvar, locals[:hash]))
935
+ s(:send,
936
+ hash, :key?,
937
+ key.to_ast_node)
938
+ end
939
+
940
+ def having_hash_keys(keys, hash = s(:lvar, locals[:hash]))
941
+ keys.reduce(nil) do |acc, key|
942
+ pnode = hash_has_key(key, hash)
943
+ next pnode unless acc
944
+
945
+ s(:begin,
946
+ s(:and, acc, pnode))
947
+ end.then do |node|
948
+ predicates.hash_keys(node, keys)
949
+ end
950
+ end
951
+
952
+ #=========== HASH PATTERN (END) ===============
953
+
954
+ def with_guard(node, guard)
955
+ return node unless guard
956
+
957
+ s(:begin,
958
+ s(:and,
959
+ node,
960
+ guard.children[0])).then do |expr|
961
+ next expr unless guard.type == :unless_guard
962
+ s(:send, expr, :!)
963
+ end
964
+ end
965
+
966
+ def with_declared_locals
967
+ lvars.clear
968
+ node = yield
969
+
970
+ return node if lvars.empty?
971
+
972
+ # We need to declare match lvars outside of the outer `find` block,
973
+ # so we do that for that whole pattern
974
+ locals_declare = s(:begin, s(:masgn,
975
+ s(:mlhs, *lvars.uniq.map { |_1| s(:lvasgn, _1) }),
976
+ s(:nil)))
977
+
978
+ s(:begin,
979
+ s(:or,
980
+ locals_declare,
981
+ node))
982
+ end
983
+
984
+ def no_matching_pattern
985
+ raise_error(
986
+ :NoMatchingPatternError,
987
+ s(:send,
988
+ s(:lvar, locals[:matchee]), :inspect)
989
+ )
990
+ end
991
+
992
+ def raise_error(type, msg = "")
993
+ s(:send, s(:const, nil, :Kernel), :raise,
994
+ s(:const, nil, type),
995
+ msg.to_ast_node)
996
+ end
997
+
998
+ # Add respond_to? check
999
+ def respond_to_check(node, mid)
1000
+ s(:send, node, :respond_to?, mid.to_ast_node)
1001
+ end
1002
+
1003
+ def respond_to_missing?(mid, *)
1004
+ return true if mid.to_s.match?(/_(clause|array_element)/)
1005
+ super
1006
+ end
1007
+
1008
+ def method_missing(mid, *args, &block)
1009
+ mid = mid.to_s
1010
+ return case_eq_clause(*args) if mid.match?(/_clause$/)
1011
+ return case_eq_array_element(*args) if mid.match?(/_array_element$/)
1012
+ return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
1013
+ super
1014
+ end
1015
+
1016
+ private
1017
+
1018
+ attr_reader :deconstructed_keys, :predicates, :lvars
1019
+
1020
+ # Raise SyntaxError if match-var is used within alternation
1021
+ # https://github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900
1022
+ def check_match_var_alternation!(name)
1023
+ return unless locals.key?(ALTERNATION_MARKER)
1024
+
1025
+ if name.is_a?(::Parser::AST::Node)
1026
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name.children.first})"
1027
+ end
1028
+
1029
+ return if name.start_with?("_")
1030
+
1031
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name})"
1032
+ end
1033
+
1034
+ def deconstructed_key(key)
1035
+ return deconstructed_keys[key] if deconstructed_keys.key?(key)
1036
+
1037
+ deconstructed_keys[key] = :"k#{deconstructed_keys.size}"
1038
+ end
1039
+
1040
+ # Unparser generates `do .. end` or `{ ... }` multiline blocks, we want to
1041
+ # have single-line blocks with `{ ... }`.
1042
+ def inline_blocks(source)
1043
+ source.gsub(/(?:do|{) \|_, __i__\|\n\s*([^\n]+)\n\s*(?:end|})/, '{ |_, __i__| \1 }')
1044
+ end
1045
+
1046
+ # Value could be omitted for mass assignment
1047
+ def build_var_assignment(var, value = nil)
1048
+ unless var.is_a?(::Parser::AST::Node)
1049
+ lvars << var
1050
+ return s(:lvasgn, *[var, value].compact)
1051
+ end
1052
+
1053
+ asign_type = :"#{var.type.to_s[0]}vasgn"
1054
+
1055
+ s(asign_type, *[var.children[0], value].compact)
1056
+ end
1057
+ end
1058
+ end
1059
+ end
1060
+ end