ruby-next-core 0.9.2 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +14 -4
  4. data/lib/.rbnext/2.3/ruby-next/commands/core_ext.rb +167 -0
  5. data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +198 -0
  6. data/lib/.rbnext/2.3/ruby-next/language/eval.rb +66 -0
  7. data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +121 -0
  8. data/lib/.rbnext/2.3/ruby-next/language/rewriters/endless_range.rb +63 -0
  9. data/lib/.rbnext/2.3/ruby-next/language/rewriters/pattern_matching.rb +944 -0
  10. data/lib/.rbnext/2.3/ruby-next/utils.rb +65 -0
  11. data/lib/ruby-next.rb +8 -6
  12. data/lib/ruby-next/cli.rb +2 -2
  13. data/lib/ruby-next/commands/core_ext.rb +1 -1
  14. data/lib/ruby-next/core.rb +27 -21
  15. data/lib/ruby-next/core/array/deconstruct.rb +9 -9
  16. data/lib/ruby-next/core/array/difference_union_intersection.rb +12 -12
  17. data/lib/ruby-next/core/constants/no_matching_pattern_error.rb +3 -3
  18. data/lib/ruby-next/core/enumerable/filter.rb +8 -8
  19. data/lib/ruby-next/core/enumerable/filter_map.rb +25 -25
  20. data/lib/ruby-next/core/enumerable/tally.rb +7 -7
  21. data/lib/ruby-next/core/enumerator/produce.rb +12 -12
  22. data/lib/ruby-next/core/hash/deconstruct_keys.rb +9 -9
  23. data/lib/ruby-next/core/hash/except.rb +11 -0
  24. data/lib/ruby-next/core/hash/merge.rb +8 -8
  25. data/lib/ruby-next/core/kernel/then.rb +2 -2
  26. data/lib/ruby-next/core/proc/compose.rb +11 -11
  27. data/lib/ruby-next/core/string/split.rb +6 -6
  28. data/lib/ruby-next/core/struct/deconstruct.rb +2 -2
  29. data/lib/ruby-next/core/struct/deconstruct_keys.rb +17 -17
  30. data/lib/ruby-next/core/symbol/end_with.rb +4 -4
  31. data/lib/ruby-next/core/symbol/start_with.rb +4 -4
  32. data/lib/ruby-next/core/time/ceil.rb +6 -6
  33. data/lib/ruby-next/core/time/floor.rb +4 -4
  34. data/lib/ruby-next/core/unboundmethod/bind_call.rb +4 -4
  35. data/lib/ruby-next/core_ext.rb +1 -1
  36. data/lib/ruby-next/language.rb +12 -1
  37. data/lib/ruby-next/language/parser.rb +0 -3
  38. data/lib/ruby-next/language/proposed.rb +3 -0
  39. data/lib/ruby-next/language/rewriters/args_forward.rb +23 -20
  40. data/lib/ruby-next/language/rewriters/base.rb +1 -1
  41. data/lib/ruby-next/language/rewriters/endless_method.rb +25 -3
  42. data/lib/ruby-next/language/rewriters/find_pattern.rb +44 -0
  43. data/lib/ruby-next/language/rewriters/method_reference.rb +1 -1
  44. data/lib/ruby-next/language/rewriters/pattern_matching.rb +102 -12
  45. data/lib/ruby-next/language/rewriters/right_hand_assignment.rb +1 -1
  46. data/lib/ruby-next/language/rewriters/safe_navigation.rb +87 -0
  47. data/lib/ruby-next/language/rewriters/shorthand_hash.rb +47 -0
  48. data/lib/ruby-next/language/rewriters/squiggly_heredoc.rb +36 -0
  49. data/lib/ruby-next/language/unparser.rb +0 -14
  50. data/lib/ruby-next/logging.rb +1 -1
  51. data/lib/ruby-next/rubocop.rb +15 -9
  52. data/lib/ruby-next/setup_self.rb +22 -0
  53. data/lib/ruby-next/version.rb +1 -1
  54. data/lib/uby-next.rb +8 -4
  55. metadata +20 -7
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module KernelEval
6
+ if Utils.refine_modules?
7
+ refine Kernel do
8
+ def eval(source, bind = nil, *args)
9
+ new_source = ::RubyNext::Language::Runtime.transform(
10
+ source,
11
+ using: ((!bind.nil?) || nil) && bind.receiver == TOPLEVEL_BINDING.receiver || ((!(((!bind.nil?) || nil) && bind.receiver).nil?) || nil) && (((!bind.nil?) || nil) && bind.receiver).is_a?(Module)
12
+ )
13
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
14
+ super new_source, bind, *args
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ module InstanceEval # :nodoc:
21
+ refine Object do
22
+ def instance_eval(*args, &block)
23
+ return super(*args, &block) if block_given?
24
+
25
+ source = args.shift
26
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
27
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
28
+ super new_source, *args
29
+ end
30
+ end
31
+ end
32
+
33
+ module ClassEval
34
+ refine Module do
35
+ def module_eval(*args, &block)
36
+ return super(*args, &block) if block_given?
37
+
38
+ source = args.shift
39
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
40
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
41
+ super new_source, *args
42
+ end
43
+
44
+ def class_eval(*args, &block)
45
+ return super(*args, &block) if block_given?
46
+
47
+ source = args.shift
48
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
49
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
50
+ super new_source, *args
51
+ end
52
+ end
53
+ end
54
+
55
+ # Refinements for `eval`-like methods.
56
+ # Transpiling eval is only possible if we do not use local from the binding,
57
+ # because we cannot access the binding of caller (without non-production ready hacks).
58
+ #
59
+ # This module is meant mainly for testing purposes.
60
+ module Eval
61
+ include InstanceEval
62
+ include ClassEval
63
+ include KernelEval
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module Rewriters
6
+ using RubyNext
7
+
8
+ CUSTOM_PARSER_REQUIRED = <<-MSG
9
+ The %s feature is not a part of the latest stable Ruby release
10
+ and is not supported by your Parser gem version.
11
+ Use RubyNext's parser to use it: https://github.com/ruby-next/parser
12
+
13
+ MSG
14
+
15
+ class Base < ::Parser::TreeRewriter
16
+ class LocalsTracker
17
+ attr_reader :stacks
18
+
19
+ def initialize
20
+ @stacks = []
21
+ end
22
+
23
+ def with(**locals)
24
+ stacks << locals
25
+ yield.tap { stacks.pop }
26
+ end
27
+
28
+ def [](name, suffix = nil)
29
+ fetch(name).then do |name|
30
+ next name unless suffix
31
+ :"#{name}#{suffix}__"
32
+ end
33
+ end
34
+
35
+ def key?(name)
36
+ !!fetch(name) { false } # rubocop:disable Style/RedundantFetchBlock
37
+ end
38
+
39
+ def fetch(name)
40
+ ind = -1
41
+
42
+ loop do
43
+ break stacks[ind][name] if stacks[ind].key?(name)
44
+ ind -= 1
45
+ break if stacks[ind].nil?
46
+ end.then do |name|
47
+ next name unless name.nil?
48
+
49
+ return yield if block_given?
50
+ raise ArgumentError, "Local var not found in scope: #{name}"
51
+ end
52
+ end
53
+ end
54
+
55
+ class << self
56
+ # Returns true if the syntax is supported
57
+ # by the current Ruby (performs syntax check, not version check)
58
+ def unsupported_syntax?
59
+ save_verbose, $VERBOSE = $VERBOSE, nil
60
+ eval_mid = Kernel.respond_to?(:eval_without_ruby_next) ? :eval_without_ruby_next : :eval
61
+ Kernel.send eval_mid, self::SYNTAX_PROBE, nil, __FILE__, __LINE__
62
+ false
63
+ rescue SyntaxError, NameError
64
+ true
65
+ ensure
66
+ $VERBOSE = save_verbose
67
+ end
68
+
69
+ # Returns true if the syntax is supported
70
+ # by the specified version
71
+ def unsupported_version?(version)
72
+ self::MIN_SUPPORTED_VERSION > version
73
+ end
74
+
75
+ private
76
+
77
+ def transform(source)
78
+ Language.transform(source, rewriters: [self], using: false)
79
+ end
80
+ end
81
+
82
+ attr_reader :locals
83
+
84
+ def initialize(context)
85
+ @context = context
86
+ @locals = LocalsTracker.new
87
+ super()
88
+ end
89
+
90
+ def s(type, *children)
91
+ ::Parser::AST::Node.new(type, children)
92
+ end
93
+
94
+ private
95
+
96
+ def replace(range, ast)
97
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.replace(range, unparse(ast))
98
+ end
99
+
100
+ def remove(range)
101
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.remove(range)
102
+ end
103
+
104
+ def insert_after(range, ast)
105
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.insert_after(range, unparse(ast))
106
+ end
107
+
108
+ def insert_before(range, ast)
109
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.insert_before(range, unparse(ast))
110
+ end
111
+
112
+ def unparse(ast)
113
+ return ast if ast.is_a?(String)
114
+ Unparser.unparse(ast)
115
+ end
116
+
117
+ attr_reader :context
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module Rewriters
6
+ class EndlessRange < Base
7
+ NAME = "endless-range"
8
+ SYNTAX_PROBE = "[0, 1][1..]"
9
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.6.0")
10
+
11
+ def on_index(node)
12
+ @current_index = node
13
+ new_index = process(node.children.last)
14
+ return unless new_index != node.children.last
15
+
16
+ node.updated(
17
+ nil,
18
+ [
19
+ node.children.first,
20
+ new_index
21
+ ]
22
+ )
23
+ end
24
+
25
+ def on_erange(node)
26
+ return unless node.children.last.nil?
27
+
28
+ context.track! self
29
+
30
+ new_end =
31
+ if index_arg?(node)
32
+ s(:int, -1)
33
+ else
34
+ s(:const,
35
+ s(:const,
36
+ s(:cbase), :Float),
37
+ :INFINITY)
38
+ end
39
+
40
+ replace(node.loc.expression, "#{node.children.first.loc.expression.source}..#{unparse(new_end)}")
41
+
42
+ node.updated(
43
+ :irange,
44
+ [
45
+ node.children.first,
46
+ new_end
47
+ ]
48
+ )
49
+ end
50
+
51
+ alias_method :on_irange, :on_erange
52
+
53
+ private
54
+
55
+ attr_reader :current_index
56
+
57
+ def index_arg?(node)
58
+ ((!(((!current_index.nil?) || nil) && current_index.children).nil?) || nil) && (((!current_index.nil?) || nil) && current_index.children).include?(node)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,944 @@
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 -(val)
17
+ ::Parser::AST::Node.new(:send, [self, :-, val.to_ast_node])
18
+ end
19
+
20
+ def +(val)
21
+ ::Parser::AST::Node.new(:send, [self, :+, val.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/MethodMissingSuper
186
+ # rubocop:disable Style/MissingRespondToMissing
187
+ class Noop < Base
188
+ # Return node itself, no memoization
189
+ def method_missing(mid, node, *)
190
+ node
191
+ end
192
+ end
193
+ # rubocop:enable Style/MethodMissingSuper
194
+ # rubocop:enable Style/MissingRespondToMissing
195
+
196
+ class CaseIn < Base
197
+ def const(node, const)
198
+ node
199
+ end
200
+
201
+ def respond_to_deconstruct(node)
202
+ predicate_clause(:respond_to_deconstruct, node)
203
+ end
204
+
205
+ def array_size(node, size)
206
+ predicate_clause(:"array_size_#{size}", node)
207
+ end
208
+
209
+ def array_deconstructed(node)
210
+ predicate_clause(:array_deconstructed, node)
211
+ end
212
+
213
+ def hash_deconstructed(node, keys)
214
+ predicate_clause(:"hash_deconstructed_#{keys.join("_p_")}", node)
215
+ end
216
+
217
+ def respond_to_deconstruct_keys(node)
218
+ predicate_clause(:respond_to_deconstruct_keys, node)
219
+ end
220
+
221
+ def hash_key(node, key)
222
+ key = key.children.first if key.is_a?(::Parser::AST::Node)
223
+ predicate_clause(:"hash_key_#{key}", 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
+
246
+ matchee_ast =
247
+ s(:lvasgn, MATCHEE, node.children[0])
248
+
249
+ patterns = locals.with(
250
+ matchee: MATCHEE,
251
+ arr: MATCHEE_ARR,
252
+ hash: MATCHEE_HASH
253
+ ) do
254
+ build_case_when(node.children[1..-1])
255
+ end
256
+
257
+ case_clause = predicates.process(s(:case, *patterns))
258
+
259
+ rewrite_case_in! node, matchee_ast, case_clause
260
+
261
+ node.updated(
262
+ :kwbegin,
263
+ [
264
+ matchee_ast, case_clause
265
+ ]
266
+ )
267
+ end
268
+
269
+ def on_in_match(node)
270
+ context.track! self
271
+
272
+ @deconstructed_keys = {}
273
+ @predicates = Predicates::Noop.new
274
+
275
+ matchee =
276
+ s(:lvasgn, MATCHEE, node.children[0])
277
+
278
+ pattern =
279
+ locals.with(
280
+ matchee: MATCHEE,
281
+ arr: MATCHEE_ARR,
282
+ hash: MATCHEE_HASH
283
+ ) do
284
+ send(
285
+ :"#{node.children[1].type}_clause",
286
+ node.children[1]
287
+ ).then do |node|
288
+ s(:or,
289
+ node,
290
+ no_matching_pattern)
291
+ end
292
+ end
293
+
294
+ node.updated(
295
+ :and,
296
+ [
297
+ matchee,
298
+ pattern
299
+ ]
300
+ ).tap do |new_node|
301
+ replace(node.loc.expression, inline_blocks(unparse(new_node)))
302
+ end
303
+ end
304
+
305
+ private
306
+
307
+ def rewrite_case_in!(node, matchee, new_node)
308
+ replace(node.loc.keyword, "case; when (#{unparse(matchee)}) && false")
309
+ remove(node.children[0].loc.expression)
310
+
311
+ node.children[1..-1].each.with_index do |clause, i|
312
+ if ((!clause.nil?) || nil) && clause.type == :in_pattern
313
+ # handle multiline clauses differently
314
+ if clause.loc.last_line > clause.children[0].loc.last_line + 1
315
+ height = clause.loc.last_line - clause.children[0].loc.last_line
316
+ padding = "\n" * height
317
+ body_indent = " " * clause.children[2].loc.column
318
+ replace(
319
+ clause.loc.expression,
320
+ "when #{inline_blocks(unparse(new_node.children[i].children[0]))}" \
321
+ "#{padding}" \
322
+ "#{body_indent}#{clause.children[2].loc.expression.source}"
323
+ )
324
+ else
325
+ replace(
326
+ clause.loc.keyword.end.join(clause.children[0].loc.expression.end),
327
+ inline_blocks(unparse(new_node.children[i].children[0]))
328
+ )
329
+ remove(clause.children[1].loc.expression) if clause.children[1]
330
+ replace(clause.loc.keyword, "when ")
331
+ end
332
+ elsif clause.nil?
333
+ insert_after(node.children[-2].loc.expression, "; else; #{unparse(new_node.children.last)}")
334
+ end
335
+ end
336
+ end
337
+
338
+ def build_case_when(nodes)
339
+ else_clause = nil
340
+ clauses = []
341
+
342
+ nodes.each do |clause|
343
+ if ((!clause.nil?) || nil) && clause.type == :in_pattern
344
+ clauses << build_when_clause(clause)
345
+ else
346
+ else_clause = process(clause)
347
+ end
348
+ end
349
+
350
+ else_clause = (else_clause || no_matching_pattern).then do |node|
351
+ next node unless node.type == :empty_else
352
+ s(:empty)
353
+ end
354
+
355
+ clauses << else_clause
356
+ clauses
357
+ end
358
+
359
+ def build_when_clause(clause)
360
+ predicates.reset!
361
+ [
362
+ with_guard(
363
+ send(
364
+ :"#{clause.children[0].type}_clause",
365
+ clause.children[0]
366
+ ),
367
+ clause.children[1] # guard
368
+ ),
369
+ process(clause.children[2] || s(:nil)) # expression
370
+ ].then do |children|
371
+ s(:when, *children)
372
+ end
373
+ end
374
+
375
+ def const_pattern_clause(node, right = s(:lvar, locals[:matchee]))
376
+ const, pattern = *node.children
377
+
378
+ predicates.const(case_eq_clause(const, right), const).then do |node|
379
+ next node if pattern.nil?
380
+
381
+ s(:and,
382
+ node,
383
+ send(:"#{pattern.type}_clause", pattern))
384
+ end
385
+ end
386
+
387
+ def match_alt_clause(node)
388
+ children = locals.with(ALTERNATION_MARKER => true) do
389
+ node.children.map.with_index do |child, i|
390
+ predicates.terminate! if i == 1
391
+ send :"#{child.type}_clause", child
392
+ end
393
+ end
394
+ s(:or, *children)
395
+ end
396
+
397
+ def match_as_clause(node, right = s(:lvar, locals[:matchee]))
398
+ s(:and,
399
+ send(:"#{node.children[0].type}_clause", node.children[0], right),
400
+ match_var_clause(node.children[1], right))
401
+ end
402
+
403
+ def match_var_clause(node, left = s(:lvar, locals[:matchee]))
404
+ return s(:true) if node.children[0] == :_
405
+
406
+ check_match_var_alternation! node.children[0]
407
+
408
+ s(:or,
409
+ s(:lvasgn, node.children[0], left),
410
+ s(:true))
411
+ end
412
+
413
+ def pin_clause(node, right = s(:lvar, locals[:matchee]))
414
+ predicates.terminate!
415
+ case_eq_clause node.children[0], right
416
+ end
417
+
418
+ def case_eq_clause(node, right = s(:lvar, locals[:matchee]))
419
+ predicates.terminate!
420
+ s(:send,
421
+ process(node), :===, right)
422
+ end
423
+
424
+ #=========== ARRAY PATTERN (START) ===============
425
+
426
+ def array_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
427
+ deconstruct_node(matchee).then do |dnode|
428
+ size_check = nil
429
+ # if there is no rest or tail, match the size first
430
+ unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
431
+ size_check = predicates.array_size(
432
+ s(:send,
433
+ node.children.size.to_ast_node,
434
+ :==,
435
+ s(:send, s(:lvar, locals[:arr]), :size)),
436
+ node.children.size
437
+ )
438
+ end
439
+
440
+ right =
441
+ if node.children.empty?
442
+ case_eq_clause(s(:array), s(:lvar, locals[:arr]))
443
+ elsif node.children.size > 1 && node.children.first.type == :match_rest && node.children.last.type == :match_rest
444
+ array_find(*node.children)
445
+ else
446
+ array_element(0, *node.children)
447
+ end
448
+
449
+ right = s(:and, size_check, right) if size_check
450
+
451
+ s(:and,
452
+ dnode,
453
+ right)
454
+ end
455
+ end
456
+
457
+ alias array_pattern_with_tail_clause array_pattern_clause
458
+ alias find_pattern_clause array_pattern_clause
459
+
460
+ def deconstruct_node(matchee)
461
+ context.use_ruby_next!
462
+
463
+ # we do not memoize respond_to_check for arrays, 'cause
464
+ # we can memoize is together with #deconstruct result
465
+ respond_check = respond_to_check(matchee, :deconstruct)
466
+ right = s(:send, matchee, :deconstruct)
467
+
468
+ predicates.array_deconstructed(
469
+ s(:and,
470
+ respond_check,
471
+ s(:and,
472
+ s(:or,
473
+ s(:lvasgn, locals[:arr], right),
474
+ s(:true)),
475
+ s(:or,
476
+ s(:send,
477
+ s(:const, nil, :Array), :===, s(:lvar, locals[:arr])),
478
+ raise_error(:TypeError, "#deconstruct must return Array"))))
479
+ )
480
+ end
481
+
482
+ def array_element(index, head, *tail)
483
+ return array_match_rest(index, head, *tail) if head.type == :match_rest
484
+
485
+ send("#{head.type}_array_element", head, index).then do |node|
486
+ next node if tail.empty?
487
+
488
+ s(:and,
489
+ node,
490
+ array_element(index + 1, *tail))
491
+ end
492
+ end
493
+
494
+ # [*a, 1, 2, *] -> arr.find.with_index { |_, i| (a = arr.take(i)) && arr[i] == 1 && arr[i + 1] == 2 }
495
+ def array_find(head, *nodes, tail)
496
+ index = s(:lvar, :__i__)
497
+
498
+ match_vars = []
499
+
500
+ head_match =
501
+ unless head.children.empty?
502
+ match_vars << s(:lvasgn, head.children[0].children[0])
503
+
504
+ arr_take = s(:send,
505
+ s(:lvar, locals[:arr]),
506
+ :take,
507
+ index)
508
+
509
+ match_var_clause(head.children[0], arr_take)
510
+ end
511
+
512
+ tail_match =
513
+ unless tail.children.empty?
514
+ match_vars << s(:lvasgn, tail.children[0].children[0])
515
+
516
+ match_var_clause(tail.children[0], arr_slice(index + nodes.size, -1))
517
+ end
518
+
519
+ nodes.each do |node|
520
+ if node.type == :match_var
521
+ match_vars << s(:lvasgn, node.children[0])
522
+ elsif node.type == :match_as
523
+ match_vars << s(:lvasgn, node.children[1].children[0])
524
+ end
525
+ end
526
+
527
+ pattern = array_rest_element(*nodes, index).then do |needle|
528
+ next needle unless head_match
529
+ s(:and,
530
+ needle,
531
+ head_match)
532
+ end.then do |headed_needle|
533
+ next headed_needle unless tail_match
534
+
535
+ s(:and,
536
+ headed_needle,
537
+ tail_match)
538
+ end
539
+
540
+ s(:block,
541
+ s(:send,
542
+ s(:send,
543
+ s(:lvar, locals[:arr]),
544
+ :find),
545
+ :with_index),
546
+ s(:args,
547
+ s(:arg, :_),
548
+ s(:arg, :__i__)),
549
+ pattern).then do |block|
550
+ next block if match_vars.empty?
551
+
552
+ # We need to declare match vars outside of `find` block
553
+ locals_declare = s(:masgn,
554
+ s(:mlhs, *match_vars),
555
+ s(:nil))
556
+
557
+ s(:or,
558
+ locals_declare,
559
+ block)
560
+ end
561
+ end
562
+
563
+ def array_match_rest(index, node, *tail)
564
+ size = tail.size + 1
565
+ child = node.children[0]
566
+
567
+ rest = arr_slice(index, -size).then do |r|
568
+ next r unless child
569
+
570
+ match_var_clause(
571
+ child,
572
+ r
573
+ )
574
+ end
575
+
576
+ return rest if tail.empty?
577
+
578
+ s(:and,
579
+ rest,
580
+ array_rest_element(*tail, -(size - 1)))
581
+ end
582
+
583
+ def array_rest_element(head, *tail, index)
584
+ send("#{head.type}_array_element", head, index).then do |node|
585
+ next node if tail.empty?
586
+
587
+ s(:and,
588
+ node,
589
+ array_rest_element(*tail, index + 1))
590
+ end
591
+ end
592
+
593
+ def array_pattern_array_element(node, index)
594
+ element = arr_item_at(index)
595
+ locals.with(arr: locals[:arr, index]) do
596
+ predicates.push :"i#{index}"
597
+ array_pattern_clause(node, element).tap { predicates.pop }
598
+ end
599
+ end
600
+
601
+ def hash_pattern_array_element(node, index)
602
+ element = arr_item_at(index)
603
+ locals.with(hash: locals[:arr, index]) do
604
+ predicates.push :"i#{index}"
605
+ hash_pattern_clause(node, element).tap { predicates.pop }
606
+ end
607
+ end
608
+
609
+ def match_alt_array_element(node, index)
610
+ children = node.children.map do |child, i|
611
+ send :"#{child.type}_array_element", child, index
612
+ end
613
+ s(:or, *children)
614
+ end
615
+
616
+ def match_var_array_element(node, index)
617
+ match_var_clause(node, arr_item_at(index))
618
+ end
619
+
620
+ def match_as_array_element(node, index)
621
+ match_as_clause(node, arr_item_at(index))
622
+ end
623
+
624
+ def pin_array_element(node, index)
625
+ case_eq_array_element node.children[0], index
626
+ end
627
+
628
+ def case_eq_array_element(node, index)
629
+ case_eq_clause(node, arr_item_at(index))
630
+ end
631
+
632
+ def arr_item_at(index, arr = s(:lvar, locals[:arr]))
633
+ s(:index, arr, index.to_ast_node)
634
+ end
635
+
636
+ def arr_slice(lindex, rindex, arr = s(:lvar, locals[:arr]))
637
+ s(:index,
638
+ arr,
639
+ s(:irange,
640
+ lindex.to_ast_node,
641
+ rindex.to_ast_node))
642
+ end
643
+
644
+ #=========== ARRAY PATTERN (END) ===============
645
+
646
+ #=========== HASH PATTERN (START) ===============
647
+
648
+ def hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
649
+ # Optimization: avoid hash modifications when not needed
650
+ # (we use #dup and #delete when "reading" values when **rest is present
651
+ # to assign the rest of the hash copy to it)
652
+ @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern }
653
+ keys = hash_pattern_destruction_keys(node.children)
654
+
655
+ specified_key_names = hash_pattern_keys(node.children)
656
+
657
+ deconstruct_keys_node(keys, matchee).then do |dnode|
658
+ right =
659
+ if node.children.empty?
660
+ case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
661
+ elsif specified_key_names.empty?
662
+ hash_element(*node.children)
663
+ else
664
+ s(:and,
665
+ having_hash_keys(specified_key_names),
666
+ hash_element(*node.children))
667
+ end
668
+
669
+ predicates.pop
670
+
671
+ next dnode if right.nil?
672
+
673
+ s(:and,
674
+ dnode,
675
+ right)
676
+ end
677
+ end
678
+
679
+ def hash_pattern_keys(children)
680
+ children.filter_map do |child|
681
+ # Skip ** without var
682
+ next if child.type == :match_rest || child.type == :match_nil_pattern
683
+
684
+ send("#{child.type}_hash_key", child)
685
+ end
686
+ end
687
+
688
+ def hash_pattern_destruction_keys(children)
689
+ return s(:nil) if children.empty?
690
+
691
+ children.filter_map do |child|
692
+ # Skip ** without var
693
+ next if child.type == :match_rest && child.children.empty?
694
+ return s(:nil) if child.type == :match_rest || child.type == :match_nil_pattern
695
+
696
+ send("#{child.type}_hash_key", child)
697
+ end.then { |keys| s(:array, *keys) }
698
+ end
699
+
700
+ def pair_hash_key(node)
701
+ node.children[0]
702
+ end
703
+
704
+ def match_var_hash_key(node)
705
+ check_match_var_alternation! node.children[0]
706
+
707
+ s(:sym, node.children[0])
708
+ end
709
+
710
+ def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
711
+ # Use original hash returned by #deconstruct_keys if not **rest matching,
712
+ # 'cause it remains immutable
713
+ deconstruct_name = @hash_match_rest ? locals[:hash, :src] : locals[:hash]
714
+
715
+ # Duplicate the source hash when matching **rest, 'cause we mutate it
716
+ hash_dup =
717
+ if @hash_match_rest
718
+ s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup))
719
+ else
720
+ s(:true)
721
+ end
722
+
723
+ context.use_ruby_next!
724
+
725
+ respond_to_checked = predicates.pred?(:respond_to_deconstruct_keys)
726
+ respond_check = predicates.respond_to_deconstruct_keys(respond_to_check(matchee, :deconstruct_keys))
727
+
728
+ key_names = keys.children.map { |node| node.children.last }
729
+ predicates.push locals[:hash]
730
+
731
+ s(:lvasgn, deconstruct_name,
732
+ s(:send,
733
+ matchee, :deconstruct_keys, keys)).then do |dnode|
734
+ next dnode if respond_to_checked
735
+
736
+ s(:and,
737
+ respond_check,
738
+ s(:and,
739
+ s(:or,
740
+ dnode,
741
+ s(:true)),
742
+ s(:or,
743
+ s(:send,
744
+ s(:const, nil, :Hash), :===, s(:lvar, deconstruct_name)),
745
+ raise_error(:TypeError, "#deconstruct_keys must return Hash"))))
746
+ end.then do |dnode|
747
+ predicates.hash_deconstructed(dnode, key_names)
748
+ end.then do |dnode|
749
+ next dnode unless @hash_match_rest
750
+
751
+ s(:and,
752
+ dnode,
753
+ hash_dup)
754
+ end
755
+ end
756
+
757
+ def hash_pattern_hash_element(node, key)
758
+ element = hash_value_at(key)
759
+ key_index = deconstructed_key(key)
760
+ locals.with(hash: locals[:hash, key_index]) do
761
+ predicates.push :"k#{key_index}"
762
+ hash_pattern_clause(node, element).tap { predicates.pop }
763
+ end
764
+ end
765
+
766
+ def array_pattern_hash_element(node, key)
767
+ element = hash_value_at(key)
768
+ key_index = deconstructed_key(key)
769
+ locals.with(arr: locals[:hash, key_index]) do
770
+ predicates.push :"k#{key_index}"
771
+ array_pattern_clause(node, element).tap { predicates.pop }
772
+ end
773
+ end
774
+
775
+ def hash_element(head, *tail)
776
+ send("#{head.type}_hash_element", head).then do |node|
777
+ next node if tail.empty?
778
+
779
+ right = hash_element(*tail)
780
+
781
+ next node if right.nil?
782
+
783
+ s(:and,
784
+ node,
785
+ right)
786
+ end
787
+ end
788
+
789
+ def pair_hash_element(node, _key = nil)
790
+ key, val = *node.children
791
+ send("#{val.type}_hash_element", val, key)
792
+ end
793
+
794
+ def match_alt_hash_element(node, key)
795
+ element_node = s(:lvasgn, locals[:hash, :el], hash_value_at(key))
796
+
797
+ children = locals.with(hash_element: locals[:hash, :el]) do
798
+ node.children.map do |child, i|
799
+ send :"#{child.type}_hash_element", child, key
800
+ end
801
+ end
802
+
803
+ s(:and,
804
+ s(:or,
805
+ element_node,
806
+ s(:true)),
807
+ s(:or, *children))
808
+ end
809
+
810
+ def match_as_hash_element(node, key)
811
+ match_as_clause(node, hash_value_at(key))
812
+ end
813
+
814
+ def match_var_hash_element(node, key = nil)
815
+ key ||= node.children[0]
816
+ match_var_clause(node, hash_value_at(key))
817
+ end
818
+
819
+ def match_nil_pattern_hash_element(node, _key = nil)
820
+ s(:send,
821
+ s(:lvar, locals[:hash]),
822
+ :empty?)
823
+ end
824
+
825
+ def match_rest_hash_element(node, _key = nil)
826
+ # case {}; in **; end
827
+ return if node.children.empty?
828
+
829
+ child = node.children[0]
830
+
831
+ raise ArgumentError, "Unknown hash match_rest child: #{child.type}" unless child.type == :match_var
832
+
833
+ match_var_clause(child, s(:lvar, locals[:hash]))
834
+ end
835
+
836
+ def case_eq_hash_element(node, key)
837
+ case_eq_clause node, hash_value_at(key)
838
+ end
839
+
840
+ def hash_value_at(key, hash = s(:lvar, locals[:hash]))
841
+ return s(:lvar, locals.fetch(:hash_element)) if locals.key?(:hash_element)
842
+
843
+ if @hash_match_rest
844
+ s(:send,
845
+ hash, :delete,
846
+ key.to_ast_node)
847
+ else
848
+ s(:index,
849
+ hash,
850
+ key.to_ast_node)
851
+ end
852
+ end
853
+
854
+ def hash_has_key(key, hash = s(:lvar, locals[:hash]))
855
+ s(:send,
856
+ hash, :key?,
857
+ key.to_ast_node)
858
+ end
859
+
860
+ def having_hash_keys(keys, hash = s(:lvar, locals[:hash]))
861
+ key = keys.shift
862
+ node = predicates.hash_key(hash_has_key(key, hash), key)
863
+
864
+ keys.reduce(node) do |res, key|
865
+ s(:and,
866
+ res,
867
+ predicates.hash_key(hash_has_key(key, hash), key))
868
+ end
869
+ end
870
+
871
+ #=========== HASH PATTERN (END) ===============
872
+
873
+ def with_guard(node, guard)
874
+ return node unless guard
875
+
876
+ s(:and,
877
+ node,
878
+ guard.children[0]).then do |expr|
879
+ next expr unless guard.type == :unless_guard
880
+ s(:send, expr, :!)
881
+ end
882
+ end
883
+
884
+ def no_matching_pattern
885
+ raise_error(
886
+ :NoMatchingPatternError,
887
+ s(:send,
888
+ s(:lvar, locals[:matchee]), :inspect)
889
+ )
890
+ end
891
+
892
+ def raise_error(type, msg = "")
893
+ s(:send, s(:const, nil, :Kernel), :raise,
894
+ s(:const, nil, type),
895
+ msg.to_ast_node)
896
+ end
897
+
898
+ # Add respond_to? check
899
+ def respond_to_check(node, mid)
900
+ s(:send, node, :respond_to?, mid.to_ast_node)
901
+ end
902
+
903
+ def respond_to_missing?(mid, *)
904
+ return true if mid.to_s.match?(/_(clause|array_element)/)
905
+ super
906
+ end
907
+
908
+ def method_missing(mid, *args, &block)
909
+ mid = mid.to_s
910
+ return case_eq_clause(*args) if mid.match?(/_clause$/)
911
+ return case_eq_array_element(*args) if mid.match?(/_array_element$/)
912
+ return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
913
+ super
914
+ end
915
+
916
+ private
917
+
918
+ attr_reader :deconstructed_keys, :predicates
919
+
920
+ # Raise SyntaxError if match-var is used within alternation
921
+ # https://github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900
922
+ def check_match_var_alternation!(name)
923
+ return unless locals.key?(ALTERNATION_MARKER)
924
+
925
+ return if name.start_with?("_")
926
+
927
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name})"
928
+ end
929
+
930
+ def deconstructed_key(key)
931
+ return deconstructed_keys[key] if deconstructed_keys.key?(key)
932
+
933
+ deconstructed_keys[key] = :"k#{deconstructed_keys.size}"
934
+ end
935
+
936
+ # Unparser generates `do .. end` blocks, we want to
937
+ # have single-line blocks with `{ ... }`.
938
+ def inline_blocks(source)
939
+ source.gsub(/do \|_, __i__\|\n\s*([^\n]+)\n\s*end/, '{ |_, __i__| \1 }')
940
+ end
941
+ end
942
+ end
943
+ end
944
+ end