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 +4 -4
- data/CHANGELOG.md +12 -1
- data/README.md +11 -7
- data/lib/ruby-next/commands/core_ext.rb +1 -1
- data/lib/ruby-next/core.rb +3 -0
- data/lib/ruby-next/core/struct/deconstruct_keys.rb +0 -2
- data/lib/ruby-next/core/symbol/end_with.rb +9 -0
- data/lib/ruby-next/core/symbol/start_with.rb +9 -0
- data/lib/ruby-next/language/rewriters/pattern_matching.rb +352 -79
- data/lib/ruby-next/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0456426952d07bcec1db2c70d256177bd8d1503c8edc7e667e2af62bf5f03ae3'
|
4
|
+
data.tar.gz: 4528e483af6cd8cdbf7b613454a0051abd5aaad4ec6cd02c727690d64fbfd848
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a25d4e8c619b0644f4bcb0f411d0f0c86d1aa7c343ab0e55b42c4d8adad10b2c5c7072ee13f019c9da29223774f6c34c353f58b64f32caf91e60040ec1b029a2
|
7
|
+
data.tar.gz: 02e6e9bdc4379b2542fe6802160f2946ac9d151878bea3f51bcd27da749f7e0bb2cf2891aa0ddf88f5a8221185d8ab9e1bdab4e639de6d69c2a44409e734626c
|
data/CHANGELOG.md
CHANGED
@@ -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
|
-
|
6
|
+
<img align="right" height="184"
|
7
|
+
title="Ruby Next logo" src="./assets/images/logo.svg">
|
7
8
|
|
8
|
-
Ruby Next is a
|
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 `
|
18
|
+
That's why Ruby Next implements the `master` features as fast as possible.
|
18
19
|
|
19
|
-
|
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
|
-
|
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
|
|
data/lib/ruby-next/core.rb
CHANGED
@@ -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)
|
@@ -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
|
-
@
|
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
|
-
|
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,
|
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 =
|
142
|
-
|
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
|
-
|
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))
|
361
|
+
s(:true))
|
157
362
|
end
|
158
363
|
|
159
|
-
def pin_clause(node)
|
160
|
-
|
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
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
#
|
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(:
|
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
|
-
|
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
|
-
|
381
|
-
|
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
|
-
|
389
|
-
|
390
|
-
|
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
|
-
|
397
|
-
|
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
|
-
|
404
|
-
|
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)),
|
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
|
-
|
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
|
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 :
|
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
|
data/lib/ruby-next/version.rb
CHANGED
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.
|
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-
|
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
|