ruby-next-core 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c192e6090c23fb8d462dae6403a3e952f661bd0972f49eaaaba064cb2b07084
4
- data.tar.gz: c72d83029a820636ce4809e727ad385a5d85bffafee94db2a4764f40eccb36f4
3
+ metadata.gz: '0456426952d07bcec1db2c70d256177bd8d1503c8edc7e667e2af62bf5f03ae3'
4
+ data.tar.gz: 4528e483af6cd8cdbf7b613454a0051abd5aaad4ec6cd02c727690d64fbfd848
5
5
  SHA512:
6
- metadata.gz: 26eb5a69b3993cde39121bd19d710d5850f9dc722a807776bdf969b3fc0b52625de05f4f583e10b97fd137bc5c7a108c1c7b4582b8f541d109282c85ed244dc4
7
- data.tar.gz: eed1785c806afd65b681f4b22c98658f0e2b43bc20d534ffc1382c476e600b2715d048e1e5409f65c43f92f9913531c1858b6a842348dd172526e1a2391f7865
6
+ metadata.gz: a25d4e8c619b0644f4bcb0f411d0f0c86d1aa7c343ab0e55b42c4d8adad10b2c5c7072ee13f019c9da29223774f6c34c353f58b64f32caf91e60040ec1b029a2
7
+ data.tar.gz: 02e6e9bdc4379b2542fe6802160f2946ac9d151878bea3f51bcd27da749f7e0bb2cf2891aa0ddf88f5a8221185d8ab9e1bdab4e639de6d69c2a44409e734626c
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.4.0 (2020-03-09)
6
+
7
+ - Optimize pattern matching transpiled code. ([@palkan][])
8
+
9
+ For array patterns, transpiled code is ~1.5-2x faster than native.
10
+ For hash patterns it's about the same.
11
+
12
+ - Pattern matching is 100% compatible with RubySpec. ([@palkan][])
13
+
14
+ - Add `Symbol#start_with?/end_with?`. ([@palkan][])
15
+
5
16
  ## 0.3.0 (2020-02-14) 💕
6
17
 
7
18
  - Add `Time#floor` and `Time#ceil`. ([@palkan][])
@@ -48,7 +59,7 @@ You can still use this feature by enabling it explicitly (see Readme).
48
59
  - Support in modifier. ([@palkan][])
49
60
 
50
61
  ```ruby
51
- {a:1, b: 2} in {a:, **}
62
+ {a: 1, b: 2} in {a:, **}
52
63
  p a #=> 1
53
64
  ```
54
65
 
data/README.md CHANGED
@@ -3,9 +3,10 @@
3
3
 
4
4
  # Ruby Next
5
5
 
6
- > Make all Rubies quack like edge Ruby!
6
+ <img align="right" height="184"
7
+ title="Ruby Next logo" src="./assets/images/logo.svg">
7
8
 
8
- Ruby Next is a collection of **polyfills** and a **transpiler** for supporting the latest and upcoming Ruby features (APIs and syntax) in older versions and alternative implementations. For example, you can use pattern matching and `Kernel#then` in Ruby 2.5 or [mruby][].
9
+ Ruby Next is a **transpiler** and a collection of **polyfills** for supporting the latest and upcoming Ruby features (APIs and syntax) in older versions and alternative implementations. For example, you can use pattern matching and `Kernel#then` in Ruby 2.5 or [mruby][].
9
10
 
10
11
  Who might be interested in Ruby Next?
11
12
 
@@ -14,9 +15,10 @@ Who might be interested in Ruby Next?
14
15
  - **Users of non-MRI implementations** such as [mruby][], [JRuby][], [TruffleRuby][], [Opal][], [RubyMotion][], [Artichoke][], [Prism][].
15
16
 
16
17
  Ruby Next also aims to help the community to assess new, _experimental_, MRI features by making it easier to play with them.
17
- That's why Ruby Next implements the `trunk` features as fast as possible.
18
+ That's why Ruby Next implements the `master` features as fast as possible.
18
19
 
19
- _⚡️ The project is in a **beta** phase. That means that the main functionality has been implemented (see [the list][features]) and APIs shouldn't change a lot in the nearest future. On the other hand, the number of users/projects is not enough to say we're "production-ready". So, can't wait to hear your feedback 🙂_
20
+ <a href="https://evilmartians.com/?utm_source=ruby-next">
21
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
20
22
 
21
23
  ## Links
22
24
 
@@ -92,7 +94,9 @@ gem "ruby-next"
92
94
 
93
95
  # gemspec
94
96
  spec.add_dependency "ruby-next"
97
+ ```
95
98
 
99
+ ```sh
96
100
  # or install globally
97
101
  gem install ruby-next
98
102
  ```
@@ -164,7 +168,7 @@ The behaviour depends on whether you transpile a single file or a directory:
164
168
 
165
169
  ```sh
166
170
  $ ruby-next nextify my_ruby.rb -o my_ruby_next.rb -V
167
- RubyNext core strategy: refine
171
+ Ruby Next core strategy: refine
168
172
  Generated: my_ruby_next.rb
169
173
  ```
170
174
 
@@ -298,7 +302,7 @@ Then, add to your Gemfile:
298
302
 
299
303
  ```ruby
300
304
  source "https://rubygems.pkg.github.com/ruby-next" do
301
- gem "parser", "2.7.0.100"
305
+ gem "parser", "~> 2.7.0.100", "< 2.7.1"
302
306
  end
303
307
 
304
308
  gem "ruby-next"
@@ -311,7 +315,7 @@ gem "ruby-next"
311
315
  You can install `ruby-next` globally by running the following commands:
312
316
 
313
317
  ```sh
314
- gem install parser -v "2.7.0.100" --source "https://USERNAME:ACCESS_TOKEN@rubygems.pkg.github.com/ruby-next"
318
+ gem install parser -v "~> 2.7.0.100" -v "< 2.7.1" --source "https://USERNAME:ACCESS_TOKEN@rubygems.pkg.github.com/ruby-next"
315
319
  gem install ruby-next
316
320
  ```
317
321
 
@@ -144,7 +144,7 @@ module RubyNext
144
144
 
145
145
  def generation_meta
146
146
  <<~MSG
147
- # Generated by RubyNext v#{RubyNext::VERSION} using the following command:
147
+ # Generated by Ruby Next v#{RubyNext::VERSION} using the following command:
148
148
  #
149
149
  # #{original_command}
150
150
  #
@@ -152,6 +152,9 @@ require_relative "core/hash/merge"
152
152
 
153
153
  require_relative "core/string/split"
154
154
 
155
+ require_relative "core/symbol/start_with"
156
+ require_relative "core/symbol/end_with"
157
+
155
158
  require_relative "core/unboundmethod/bind_call"
156
159
 
157
160
  require_relative "core/time/floor"
@@ -8,8 +8,6 @@ RubyNext::Core.patch Struct, method: :deconstruct_keys, version: "2.7" do
8
8
 
9
9
  return to_h unless keys
10
10
 
11
- return {} if size < keys.size
12
-
13
11
  keys.each_with_object({}) do |k, acc|
14
12
  # if k is Symbol and not a member of a Struct return {}
15
13
  next if (Symbol === k || String === k) && !members.include?(k.to_sym)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch Symbol, method: :end_with?, version: "2.7" do
4
+ <<~RUBY
5
+ def end_with?(*prefixes)
6
+ to_s.end_with?(*prefixes)
7
+ end
8
+ RUBY
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch Symbol, method: :start_with?, version: "2.7" do
4
+ <<~RUBY
5
+ def start_with?(*prefixes)
6
+ to_s.start_with?(*prefixes)
7
+ end
8
+ RUBY
9
+ end
@@ -31,6 +31,190 @@ module RubyNext
31
31
  end
32
32
  end)
33
33
 
34
+ # We can memoize structural predicates to avoid double calculation.
35
+ #
36
+ # For example, consider the following case and the corresponding predicate chains:
37
+ #
38
+ # case val
39
+ # in [:ok, 200] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
40
+ # in [:created, 201] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
41
+ # in [401 | 403] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_1]
42
+ # end
43
+ #
44
+ # We can minimize the number of predicate calls by storing the intermediate values (prefixed with `p_`) and using them
45
+ # in the subsequent calls:
46
+ #
47
+ # case val
48
+ # in [:ok, 200] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
49
+ # in [:created, 201] #=> [:p_deconstructed, :p_arr_size_2]
50
+ # in [401 | 403] #=> [:p_deconstructed, :arr_size_is_1]
51
+ # end
52
+ #
53
+ # This way we mimic a naive decision tree algorithim.
54
+ module Predicates
55
+ class Processor < ::Parser::TreeRewriter
56
+ attr_reader :predicates
57
+
58
+ def initialize(predicates)
59
+ @predicates = predicates
60
+ super()
61
+ end
62
+
63
+ def on_lvasgn(node)
64
+ lvar, val = *node.children
65
+ if predicates.store[lvar] == false
66
+ process(val)
67
+ else
68
+ node
69
+ end
70
+ end
71
+
72
+ def on_and(node)
73
+ left, right = *node.children
74
+
75
+ if truthy(left)
76
+ process(right)
77
+ elsif truthy(right)
78
+ process(left)
79
+ else
80
+ node.updated(
81
+ :and,
82
+ [
83
+ process(left),
84
+ process(right)
85
+ ]
86
+ )
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def truthy(node)
93
+ return false unless node.is_a?(::Parser::AST::Node)
94
+ return true if node.type == :true
95
+ return false if node.children.empty?
96
+
97
+ node.children.all? { |child| truthy(child) }
98
+ end
99
+ end
100
+
101
+ class Base
102
+ attr_reader :store, :predicates_by_path, :count, :terminated, :current_path
103
+ alias terminated? terminated
104
+
105
+ def initialize
106
+ # total number of predicates
107
+ @count = 0
108
+ # cache of all predicates by path
109
+ @predicates_by_path = {}
110
+ # all predicates and their dirty state
111
+ @store = {}
112
+
113
+ @current_path = []
114
+ end
115
+
116
+ def reset!
117
+ @current_path = []
118
+ @terminated = false
119
+ end
120
+
121
+ def push(path)
122
+ current_path << path
123
+ end
124
+
125
+ def pop
126
+ current_path.pop
127
+ end
128
+
129
+ def terminate!
130
+ @terminated = true
131
+ end
132
+
133
+ def predicate_clause(name, node)
134
+ if pred?(name)
135
+ read_pred(name)
136
+ else
137
+ write_pred(name, node)
138
+ end
139
+ end
140
+
141
+ def pred?(name)
142
+ predicates_by_path.key?(current_path + [name])
143
+ end
144
+
145
+ def read_pred(name)
146
+ lvar = predicates_by_path.fetch(current_path + [name])
147
+ # mark as used
148
+ store[lvar] = true
149
+ s(:lvar, lvar)
150
+ end
151
+
152
+ def write_pred(name, node)
153
+ return node if terminated?
154
+ @count += 1
155
+ lvar = :"__p_#{count}__"
156
+ predicates_by_path[current_path + [name]] = lvar
157
+ store[lvar] = false
158
+
159
+ s(:lvasgn,
160
+ lvar,
161
+ node)
162
+ end
163
+
164
+ def process(ast)
165
+ Processor.new(self).process(ast)
166
+ end
167
+
168
+ private
169
+
170
+ def s(type, *children)
171
+ ::Parser::AST::Node.new(type, children)
172
+ end
173
+ end
174
+
175
+ # rubocop:disable Style/MethodMissingSuper
176
+ # rubocop:disable Style/MissingRespondToMissing
177
+ class Noop < Base
178
+ # Return node itself, no memoization
179
+ def method_missing(mid, node, *)
180
+ node
181
+ end
182
+ end
183
+ # rubocop:enable Style/MethodMissingSuper
184
+ # rubocop:enable Style/MissingRespondToMissing
185
+
186
+ class CaseIn < Base
187
+ def const(node, const)
188
+ node
189
+ end
190
+
191
+ def respond_to_deconstruct(node)
192
+ predicate_clause(:respond_to_deconstruct, node)
193
+ end
194
+
195
+ def array_size(node, size)
196
+ predicate_clause(:"array_size_#{size}", node)
197
+ end
198
+
199
+ def array_deconstructed(node)
200
+ predicate_clause(:array_deconstructed, node)
201
+ end
202
+
203
+ def hash_deconstructed(node, keys)
204
+ predicate_clause(:"hash_deconstructed_#{keys.join("_p_")}", node)
205
+ end
206
+
207
+ def respond_to_deconstruct_keys(node)
208
+ predicate_clause(:respond_to_deconstruct_keys, node)
209
+ end
210
+
211
+ def hash_key(node, key)
212
+ key = key.children.first if key.is_a?(::Parser::AST::Node)
213
+ predicate_clause(:"hash_key_#{key}", node)
214
+ end
215
+ end
216
+ end
217
+
34
218
  class PatternMatching < Base
35
219
  SYNTAX_PROBE = "case 0; in 0; true; else; 1; end"
36
220
  MIN_SUPPORTED_VERSION = Gem::Version.new("2.7.0")
@@ -39,15 +223,19 @@ module RubyNext
39
223
  MATCHEE_ARR = :__m_arr__
40
224
  MATCHEE_HASH = :__m_hash__
41
225
 
226
+ ALTERNATION_MARKER = :__alt__
227
+ CURRENT_HASH_KEY = :__chk__
228
+
42
229
  def on_case_match(node)
43
230
  context.track! self
44
231
 
45
- @deconstructed = []
232
+ @deconstructed_keys = {}
233
+ @predicates = Predicates::CaseIn.new
46
234
 
47
235
  matchee_ast =
48
236
  s(:lvasgn, MATCHEE, node.children[0])
49
237
 
50
- ifs_ast = locals.with(
238
+ patterns = locals.with(
51
239
  matchee: MATCHEE,
52
240
  arr: MATCHEE_ARR,
53
241
  hash: MATCHEE_HASH
@@ -55,10 +243,13 @@ module RubyNext
55
243
  build_if_clause(node.children[1], node.children[2..-1])
56
244
  end
57
245
 
246
+ # remove unused predicate assignments and truthy expressions
247
+ patterns = predicates.process(patterns)
248
+
58
249
  node.updated(
59
250
  :begin,
60
251
  [
61
- matchee_ast, ifs_ast
252
+ matchee_ast, patterns
62
253
  ]
63
254
  )
64
255
  end
@@ -66,6 +257,9 @@ module RubyNext
66
257
  def on_in_match(node)
67
258
  context.track! self
68
259
 
260
+ @deconstructed_keys = {}
261
+ @predicates = Predicates::Noop.new
262
+
69
263
  matchee =
70
264
  s(:lvasgn, MATCHEE, node.children[0])
71
265
 
@@ -98,11 +292,15 @@ module RubyNext
98
292
 
99
293
  def build_if_clause(node, rest)
100
294
  if node&.type == :in_pattern
295
+ predicates.reset!
101
296
  build_in_pattern(node, rest)
102
297
  else
103
298
  raise "Unexpected else in the middle of case ... in" if rest && rest.size > 0
104
299
  # else clause must be present
105
- node || no_matching_pattern
300
+ (process(node) || no_matching_pattern).then do |else_node|
301
+ next else_node unless else_node.type == :empty_else
302
+ s(:empty)
303
+ end
106
304
  end
107
305
  end
108
306
 
@@ -115,7 +313,7 @@ module RubyNext
115
313
  ),
116
314
  clause.children[1] # guard
117
315
  ),
118
- clause.children[2] || s(:nil) # expression
316
+ process(clause.children[2] || s(:nil)) # expression
119
317
  ].then do |children|
120
318
  if rest && rest.size > 0
121
319
  children << build_if_clause(rest.first, rest[1..-1])
@@ -125,10 +323,10 @@ module RubyNext
125
323
  end
126
324
  end
127
325
 
128
- def const_pattern_clause(node)
326
+ def const_pattern_clause(node, right = s(:lvar, locals[:matchee]))
129
327
  const, pattern = *node.children
130
328
 
131
- case_eq_clause(const).then do |node|
329
+ predicates.const(case_eq_clause(const, right), const).then do |node|
132
330
  next node if pattern.nil?
133
331
 
134
332
  s(:and,
@@ -138,37 +336,58 @@ module RubyNext
138
336
  end
139
337
 
140
338
  def match_alt_clause(node)
141
- children = node.children.map do |child|
142
- send :"#{child.type}_clause", child
339
+ children = locals.with(ALTERNATION_MARKER => true) do
340
+ node.children.map.with_index do |child, i|
341
+ predicates.terminate! if i == 1
342
+ send :"#{child.type}_clause", child
343
+ end
143
344
  end
144
345
  s(:or, *children)
145
346
  end
146
347
 
147
348
  def match_as_clause(node, right = s(:lvar, locals[:matchee]))
148
349
  s(:and,
149
- case_eq_clause(node.children[0]),
350
+ send(:"#{node.children[0].type}_clause", node.children[0], right),
150
351
  match_var_clause(node.children[1], right))
151
352
  end
152
353
 
153
354
  def match_var_clause(node, left = s(:lvar, locals[:matchee]))
355
+ return s(:true) if node.children[0] == :_
356
+
357
+ check_match_var_alternation! node.children[0]
358
+
154
359
  s(:or,
155
360
  s(:lvasgn, node.children[0], left),
156
- s(:true)) # rubocop:disable Lint/BooleanSymbol
361
+ s(:true))
157
362
  end
158
363
 
159
- def pin_clause(node)
160
- case_eq_clause node.children[0]
364
+ def pin_clause(node, right = s(:lvar, locals[:matchee]))
365
+ predicates.terminate!
366
+ case_eq_clause node.children[0], right
161
367
  end
162
368
 
163
369
  def case_eq_clause(node, right = s(:lvar, locals[:matchee]))
370
+ predicates.terminate!
164
371
  s(:send,
165
- node, :===, right)
372
+ process(node), :===, right)
166
373
  end
167
374
 
168
375
  #=========== ARRAY PATTERN (START) ===============
169
376
 
170
377
  def array_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
171
378
  deconstruct_node(matchee).then do |dnode|
379
+ size_check = nil
380
+ # if there is no rest or tail, match the size first
381
+ unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
382
+ size_check = predicates.array_size(
383
+ s(:send,
384
+ node.children.size.to_ast_node,
385
+ :==,
386
+ s(:send, s(:lvar, locals[:arr]), :size)),
387
+ node.children.size
388
+ )
389
+ end
390
+
172
391
  right =
173
392
  if node.children.empty?
174
393
  case_eq_clause(s(:array), s(:lvar, locals[:arr]))
@@ -176,49 +395,36 @@ module RubyNext
176
395
  array_element(0, *node.children)
177
396
  end
178
397
 
179
- # already deconsrtructed
180
- next right if dnode.nil?
181
-
182
- # if there is no rest or tail, match the size first
183
- unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
184
- right =
185
- s(:and,
186
- s(:send,
187
- node.children.size.to_ast_node,
188
- :==,
189
- s(:send, s(:lvar, locals[:arr]), :size)),
190
- right)
191
- end
398
+ right = s(:and, size_check, right) if size_check
192
399
 
193
400
  s(:and,
194
401
  dnode,
195
402
  right)
196
- end.then do |right|
197
- s(:and,
198
- respond_to_check(matchee, :deconstruct),
199
- right)
200
403
  end
201
404
  end
202
405
 
203
406
  alias array_pattern_with_tail_clause array_pattern_clause
204
407
 
205
408
  def deconstruct_node(matchee)
206
- # only deconstruct once per case
207
- return if deconstructed&.include?(locals[:arr])
208
-
209
409
  context.use_ruby_next!
210
410
 
411
+ # we do not memoize respond_to_check for arrays, 'cause
412
+ # we can memoize is together with #deconstruct result
413
+ respond_check = respond_to_check(matchee, :deconstruct)
211
414
  right = s(:send, matchee, :deconstruct)
212
415
 
213
- deconstructed << locals[:arr] if deconstructed
214
-
215
- s(:and,
216
- s(:or,
217
- s(:lvasgn, locals[:arr], right),
218
- s(:true)), # rubocop:disable Lint/BooleanSymbol
219
- s(:or,
220
- case_eq_clause(s(:const, nil, :Array), s(:lvar, locals[:arr])),
221
- raise_error(:TypeError, "#deconstruct must return Array")))
416
+ predicates.array_deconstructed(
417
+ s(:and,
418
+ respond_check,
419
+ s(:and,
420
+ s(:or,
421
+ s(:lvasgn, locals[:arr], right),
422
+ s(:true)),
423
+ s(:or,
424
+ s(:send,
425
+ s(:const, nil, :Array), :===, s(:lvar, locals[:arr])),
426
+ raise_error(:TypeError, "#deconstruct must return Array"))))
427
+ )
222
428
  end
223
429
 
224
430
  def array_element(index, head, *tail)
@@ -237,6 +443,7 @@ module RubyNext
237
443
  child = node.children[0]
238
444
  rest = arr_rest_items(index, tail.size).then do |r|
239
445
  next r unless child
446
+
240
447
  match_var_clause(
241
448
  child,
242
449
  r
@@ -263,14 +470,16 @@ module RubyNext
263
470
  def array_pattern_array_element(node, index)
264
471
  element = arr_item_at(index)
265
472
  locals.with(arr: locals[:arr, index]) do
266
- array_pattern_clause(node, element)
473
+ predicates.push :"i#{index}"
474
+ array_pattern_clause(node, element).tap { predicates.pop }
267
475
  end
268
476
  end
269
477
 
270
478
  def hash_pattern_array_element(node, index)
271
479
  element = arr_item_at(index)
272
480
  locals.with(hash: locals[:arr, index]) do
273
- hash_pattern_clause(node, element)
481
+ predicates.push :"i#{index}"
482
+ hash_pattern_clause(node, element).tap { predicates.pop }
274
483
  end
275
484
  end
276
485
 
@@ -318,29 +527,42 @@ module RubyNext
318
527
  # (we use #dup and #delete when "reading" values when **rest is present
319
528
  # to assign the rest of the hash copy to it)
320
529
  @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern }
321
- keys = hash_pattern_keys(node.children)
530
+ keys = hash_pattern_destruction_keys(node.children)
531
+
532
+ specified_key_names = hash_pattern_keys(node.children)
322
533
 
323
534
  deconstruct_keys_node(keys, matchee).then do |dnode|
324
535
  right =
325
536
  if node.children.empty?
326
537
  case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
327
- else
538
+ elsif specified_key_names.empty?
328
539
  hash_element(*node.children)
540
+ else
541
+ s(:and,
542
+ having_hash_keys(specified_key_names),
543
+ hash_element(*node.children))
329
544
  end
330
545
 
546
+ predicates.pop
547
+
331
548
  next dnode if right.nil?
332
549
 
333
550
  s(:and,
334
551
  dnode,
335
552
  right)
336
- end.then do |right|
337
- s(:and,
338
- respond_to_check(matchee, :deconstruct_keys),
339
- right)
340
553
  end
341
554
  end
342
555
 
343
556
  def hash_pattern_keys(children)
557
+ children.filter_map do |child|
558
+ # Skip ** without var
559
+ next if child.type == :match_rest || child.type == :match_nil_pattern
560
+
561
+ send("#{child.type}_hash_key", child)
562
+ end
563
+ end
564
+
565
+ def hash_pattern_destruction_keys(children)
344
566
  return s(:nil) if children.empty?
345
567
 
346
568
  children.filter_map do |child|
@@ -357,51 +579,73 @@ module RubyNext
357
579
  end
358
580
 
359
581
  def match_var_hash_key(node)
582
+ check_match_var_alternation! node.children[0]
583
+
360
584
  s(:sym, node.children[0])
361
585
  end
362
586
 
363
587
  def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
364
- # Deconstruct once and use a copy of the hash for each pattern if we need **rest.
588
+ # Use original hash returned by #deconstruct_keys if not **rest matching,
589
+ # 'cause it remains immutable
590
+ deconstruct_name = @hash_match_rest ? locals[:hash, :src] : locals[:hash]
591
+
592
+ # Duplicate the source hash when matching **rest, 'cause we mutate it
365
593
  hash_dup =
366
594
  if @hash_match_rest
367
595
  s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup))
368
596
  else
369
- s(:lvasgn, locals[:hash], s(:lvar, locals[:hash, :src]))
597
+ s(:true)
370
598
  end
371
599
 
372
- # Create a copy of the original hash if already deconstructed
373
- # FIXME: need better algorithm to handle different key sets
374
- # return hash_dup if deconstructed&.include?(locals[:hash])
375
-
376
600
  context.use_ruby_next!
377
601
 
378
- deconstructed << locals[:hash] if deconstructed
602
+ respond_to_checked = predicates.pred?(:respond_to_deconstruct_keys)
603
+ respond_check = predicates.respond_to_deconstruct_keys(respond_to_check(matchee, :deconstruct_keys))
379
604
 
380
- right = s(:send,
381
- matchee, :deconstruct_keys, keys)
605
+ key_names = keys.children.map { |node| node.children.last }
606
+ predicates.push locals[:hash]
607
+
608
+ s(:lvasgn, deconstruct_name,
609
+ s(:send,
610
+ matchee, :deconstruct_keys, keys)).then do |dnode|
611
+ next dnode if respond_to_checked
612
+
613
+ s(:and,
614
+ respond_check,
615
+ s(:and,
616
+ s(:or,
617
+ dnode,
618
+ s(:true)),
619
+ s(:or,
620
+ s(:send,
621
+ s(:const, nil, :Hash), :===, s(:lvar, deconstruct_name)),
622
+ raise_error(:TypeError, "#deconstruct_keys must return Hash"))))
623
+ end.then do |dnode|
624
+ predicates.hash_deconstructed(dnode, key_names)
625
+ end.then do |dnode|
626
+ next dnode unless @hash_match_rest
382
627
 
383
- s(:and,
384
- s(:or,
385
- s(:lvasgn, locals[:hash, :src], right),
386
- s(:true)), # rubocop:disable Lint/BooleanSymbol
387
628
  s(:and,
388
- s(:or,
389
- case_eq_clause(s(:const, nil, :Hash), s(:lvar, locals[:hash, :src])),
390
- raise_error(:TypeError, "#deconstruct_keys must return Hash")),
391
- hash_dup))
629
+ dnode,
630
+ hash_dup)
631
+ end
392
632
  end
393
633
 
394
634
  def hash_pattern_hash_element(node, key)
395
635
  element = hash_value_at(key)
396
- locals.with(hash: locals[:hash, deconstructed.size]) do
397
- hash_pattern_clause(node, element)
636
+ key_index = deconstructed_key(key)
637
+ locals.with(hash: locals[:hash, key_index]) do
638
+ predicates.push :"k#{key_index}"
639
+ hash_pattern_clause(node, element).tap { predicates.pop }
398
640
  end
399
641
  end
400
642
 
401
643
  def array_pattern_hash_element(node, key)
402
644
  element = hash_value_at(key)
403
- locals.with(arr: locals[:hash, deconstructed.size]) do
404
- array_pattern_clause(node, element)
645
+ key_index = deconstructed_key(key)
646
+ locals.with(arr: locals[:hash, key_index]) do
647
+ predicates.push :"k#{key_index}"
648
+ array_pattern_clause(node, element).tap { predicates.pop }
405
649
  end
406
650
  end
407
651
 
@@ -436,16 +680,17 @@ module RubyNext
436
680
  s(:and,
437
681
  s(:or,
438
682
  element_node,
439
- s(:true)), # rubocop:disable Lint/BooleanSymbol
683
+ s(:true)),
440
684
  s(:or, *children))
441
685
  end
442
686
 
687
+ def match_as_hash_element(node, key)
688
+ match_as_clause(node, hash_value_at(key))
689
+ end
690
+
443
691
  def match_var_hash_element(node, key = nil)
444
692
  key ||= node.children[0]
445
- # We need to check whether key is present first
446
- s(:and,
447
- hash_has_key(key),
448
- match_var_clause(node, hash_value_at(key)))
693
+ match_var_clause(node, hash_value_at(key))
449
694
  end
450
695
 
451
696
  def match_nil_pattern_hash_element(node, _key = nil)
@@ -489,6 +734,17 @@ module RubyNext
489
734
  key.to_ast_node)
490
735
  end
491
736
 
737
+ def having_hash_keys(keys, hash = s(:lvar, locals[:hash]))
738
+ key = keys.shift
739
+ node = predicates.hash_key(hash_has_key(key, hash), key)
740
+
741
+ keys.reduce(node) do |res, key|
742
+ s(:and,
743
+ res,
744
+ predicates.hash_key(hash_has_key(key, hash), key))
745
+ end
746
+ end
747
+
492
748
  #=========== HASH PATTERN (END) ===============
493
749
 
494
750
  def with_guard(node, guard)
@@ -516,6 +772,7 @@ module RubyNext
516
772
  msg.to_ast_node)
517
773
  end
518
774
 
775
+ # Add respond_to? check
519
776
  def respond_to_check(node, mid)
520
777
  s(:send, node, :respond_to?, mid.to_ast_node)
521
778
  end
@@ -526,7 +783,7 @@ module RubyNext
526
783
  end
527
784
 
528
785
  def method_missing(mid, *args, &block)
529
- return case_eq_clause(args.first) if mid.match?(/_clause$/)
786
+ return case_eq_clause(*args) if mid.match?(/_clause$/)
530
787
  return case_eq_array_element(*args) if mid.match?(/_array_element$/)
531
788
  return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
532
789
  super
@@ -534,7 +791,23 @@ module RubyNext
534
791
 
535
792
  private
536
793
 
537
- attr_reader :deconstructed
794
+ attr_reader :deconstructed_keys, :predicates
795
+
796
+ # Raise SyntaxError if match-var is used within alternation
797
+ # https://github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900
798
+ def check_match_var_alternation!(name)
799
+ return unless locals.key?(ALTERNATION_MARKER)
800
+
801
+ return if name.start_with?("_")
802
+
803
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name})"
804
+ end
805
+
806
+ def deconstructed_key(key)
807
+ return deconstructed_keys[key] if deconstructed_keys.key?(key)
808
+
809
+ deconstructed_keys[key] = :"k#{deconstructed_keys.size}"
810
+ end
538
811
  end
539
812
  end
540
813
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyNext
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-next-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-15 00:00:00.000000000 Z
11
+ date: 2020-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser
@@ -76,6 +76,8 @@ files:
76
76
  - lib/ruby-next/core/string/split.rb
77
77
  - lib/ruby-next/core/struct/deconstruct.rb
78
78
  - lib/ruby-next/core/struct/deconstruct_keys.rb
79
+ - lib/ruby-next/core/symbol/end_with.rb
80
+ - lib/ruby-next/core/symbol/start_with.rb
79
81
  - lib/ruby-next/core/time/ceil.rb
80
82
  - lib/ruby-next/core/time/floor.rb
81
83
  - lib/ruby-next/core/unboundmethod/bind_call.rb