class RubyNext::Language::Rewriters::PatternMatching
Constants
- ALTERNATION_MARKER
- CURRENT_HASH_KEY
- MATCHEE
- MATCHEE_ARR
- MATCHEE_HASH
- MIN_SUPPORTED_VERSION
- NAME
- SYNTAX_PROBE
Attributes
deconstructed_keys[R]
predicates[R]
Public Instance Methods
on_case_match(node)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 241 def on_case_match(node) context.track! self @deconstructed_keys = {} @predicates = Predicates::CaseIn.new matchee_ast = s(:begin, s(:lvasgn, MATCHEE, node.children[0])) patterns = locals.with( matchee: MATCHEE, arr: MATCHEE_ARR, hash: MATCHEE_HASH ) do build_case_when(node.children[1..-1]) end case_clause = predicates.process(s(:case, *patterns)) rewrite_case_in! node, matchee_ast, case_clause node.updated( :kwbegin, [ matchee_ast, case_clause ] ) end
on_match_pattern(node)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 270 def on_match_pattern(node) context.track! self @deconstructed_keys = {} @predicates = Predicates::Noop.new matchee = s(:begin, s(:lvasgn, MATCHEE, node.children[0])) pattern = locals.with( matchee: MATCHEE, arr: MATCHEE_ARR, hash: MATCHEE_HASH ) do send( :"#{node.children[1].type}_clause", node.children[1] ).then do |node| s(:begin, s(:or, node, no_matching_pattern)) end end node.updated( :and, [ matchee, pattern ] ).tap do |new_node| replace(node.loc.expression, inline_blocks(unparse(new_node))) end end
Also aliased as: on_in_match
Private Instance Methods
arr_item_at(index, arr = s(:lvar, locals[:arr]))
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 650 def arr_item_at(index, arr = s(:lvar, locals[:arr])) s(:index, arr, index.to_ast_node) end
arr_slice(lindex, rindex, arr = s(:lvar, locals[:arr]))
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 654 def arr_slice(lindex, rindex, arr = s(:lvar, locals[:arr])) s(:index, arr, s(:irange, lindex.to_ast_node, rindex.to_ast_node)) end
array_element(index, head, *tail)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 494 def array_element(index, head, *tail) return array_match_rest(index, head, *tail) if head.type == :match_rest send("#{head.type}_array_element", head, index).then do |node| next node if tail.empty? s(:begin, s(:and, node, array_element(index + 1, *tail))) end end
array_find(head, *nodes, tail)
click to toggle source
- *a, 1, 2, *
-
-> arr.find.with_index { |_, i| (a = arr.take(i)) && arr == 1 && arr[i + 1] == 2 }
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 508 def array_find(head, *nodes, tail) index = s(:lvar, :__i__) match_vars = [] head_match = unless head.children.empty? match_vars << s(:lvasgn, head.children[0].children[0]) arr_take = s(:send, s(:lvar, locals[:arr]), :take, index) match_var_clause(head.children[0], arr_take) end tail_match = unless tail.children.empty? match_vars << s(:lvasgn, tail.children[0].children[0]) match_var_clause(tail.children[0], arr_slice(index + nodes.size, -1)) end nodes.each do |node| if node.type == :match_var match_vars << s(:lvasgn, node.children[0]) elsif node.type == :match_as match_vars << s(:lvasgn, node.children[1].children[0]) end end pattern = array_rest_element(*nodes, index).then do |needle| next needle unless head_match s(:begin, s(:and, needle, head_match)) end.then do |headed_needle| next headed_needle unless tail_match s(:begin, s(:and, headed_needle, tail_match)) end s(:block, s(:send, s(:send, s(:lvar, locals[:arr]), :find), :with_index), s(:args, s(:arg, :_), s(:arg, :__i__)), pattern).then do |block| next block if match_vars.empty? # We need to declare match vars outside of `find` block locals_declare = s(:begin, s(:masgn, s(:mlhs, *match_vars), s(:nil))) s(:begin, s(:or, locals_declare, block)) end end
array_match_rest(index, node, *tail)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 579 def array_match_rest(index, node, *tail) size = tail.size + 1 child = node.children[0] rest = arr_slice(index, -size).then do |r| next r unless child match_var_clause( child, r ) end return rest if tail.empty? s(:begin, s(:and, rest, array_rest_element(*tail, -(size - 1)))) end
array_pattern_array_element(node, index)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 611 def array_pattern_array_element(node, index) element = arr_item_at(index) locals.with(arr: locals[:arr, index]) do predicates.push :"i#{index}" array_pattern_clause(node, element).tap { predicates.pop } end end
array_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
click to toggle source
ARRAY PATTERN (START) ===============¶ ↑
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 433 def array_pattern_clause(node, matchee = s(:lvar, locals[:matchee])) deconstruct_node(matchee).then do |dnode| size_check = nil # if there is no rest or tail, match the size first unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest } size_check = predicates.array_size( s(:begin, s(:send, node.children.size.to_ast_node, :==, s(:send, s(:lvar, locals[:arr]), :size))), node.children.size ) end right = if node.children.empty? case_eq_clause(s(:array), s(:lvar, locals[:arr])) elsif node.children.size > 1 && node.children.first.type == :match_rest && node.children.last.type == :match_rest array_find(*node.children) else array_element(0, *node.children) end right = s(:and, size_check, right) if size_check s(:begin, s(:and, dnode, right)) end end
Also aliased as: array_pattern_with_tail_clause, find_pattern_clause
array_pattern_hash_element(node, key)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 790 def array_pattern_hash_element(node, key) element = hash_value_at(key) key_index = deconstructed_key(key) locals.with(arr: locals[:hash, key_index]) do predicates.push :"k#{key_index}" array_pattern_clause(node, element).tap { predicates.pop } end end
array_pattern_with_tail_clause(node, matchee = s(:lvar, locals[:matchee]))
Alias for: array_pattern_clause
array_rest_element(head, *tail, index)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 600 def array_rest_element(head, *tail, index) send("#{head.type}_array_element", head, index).then do |node| next node if tail.empty? s(:begin, s(:and, node, array_rest_element(*tail, index + 1))) end end
build_case_when(nodes)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 342 def build_case_when(nodes) else_clause = nil clauses = [] nodes.each do |clause| if clause&.type == :in_pattern clauses << build_when_clause(clause) else else_clause = process(clause) end end else_clause = (else_clause || no_matching_pattern).then do |node| next node unless node.type == :empty_else s(:empty) end clauses << else_clause clauses end
build_when_clause(clause)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 363 def build_when_clause(clause) predicates.reset! [ with_guard( send( :"#{clause.children[0].type}_clause", clause.children[0] ), clause.children[1] # guard ), process(clause.children[2] || s(:nil)) # expression ].then do |children| s(:when, *children) end end
case_eq_array_element(node, index)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 646 def case_eq_array_element(node, index) case_eq_clause(node, arr_item_at(index)) end
case_eq_clause(node, right = s(:lvar, locals[:matchee]))
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 425 def case_eq_clause(node, right = s(:lvar, locals[:matchee])) predicates.terminate! s(:begin, s(:send, process(node), :===, right)) end
case_eq_hash_element(node, key)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 864 def case_eq_hash_element(node, key) case_eq_clause node, hash_value_at(key) end
check_match_var_alternation!(name)
click to toggle source
Raise SyntaxError if match-var is used within alternation github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 952 def check_match_var_alternation!(name) return unless locals.key?(ALTERNATION_MARKER) return if name.start_with?("_") raise ::SyntaxError, "illegal variable in alternative pattern (#{name})" end
const_pattern_clause(node, right = s(:lvar, locals[:matchee]))
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 379 def const_pattern_clause(node, right = s(:lvar, locals[:matchee])) const, pattern = *node.children predicates.const(case_eq_clause(const, right), const).then do |node| next node if pattern.nil? s(:begin, s(:and, node, send(:"#{pattern.type}_clause", pattern))) end end
deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 730 def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee])) # Use original hash returned by #deconstruct_keys if not **rest matching, # 'cause it remains immutable deconstruct_name = @hash_match_rest ? locals[:hash, :src] : locals[:hash] # Duplicate the source hash when matching **rest, 'cause we mutate it hash_dup = if @hash_match_rest s(:begin, s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup))) else s(:true) end context.use_ruby_next! respond_to_checked = predicates.pred?(:respond_to_deconstruct_keys) respond_check = predicates.respond_to_deconstruct_keys(respond_to_check(matchee, :deconstruct_keys)) key_names = keys.children.map { |node| node.children.last } predicates.push locals[:hash] s(:begin, s(:lvasgn, deconstruct_name, s(:send, matchee, :deconstruct_keys, keys))).then do |dnode| next dnode if respond_to_checked s(:and, respond_check, s(:begin, s(:and, s(:begin, s(:or, dnode, s(:true))), s(:begin, s(:or, s(:send, s(:const, nil, :Hash), :===, s(:lvar, deconstruct_name)), raise_error(:TypeError, "#deconstruct_keys must return Hash")))))) end.then do |dnode| predicates.hash_deconstructed(dnode, key_names) end.then do |dnode| next dnode unless @hash_match_rest s(:begin, s(:and, dnode, hash_dup)) end end
deconstruct_node(matchee)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 469 def deconstruct_node(matchee) context.use_ruby_next! # we do not memoize respond_to_check for arrays, 'cause # we can memoize is together with #deconstruct result respond_check = respond_to_check(matchee, :deconstruct) right = s(:send, matchee, :deconstruct) predicates.array_deconstructed( s(:and, respond_check, s(:begin, s(:and, s(:begin, s(:or, s(:begin, s(:lvasgn, locals[:arr], right)), s(:true))), s(:begin, s(:or, s(:send, s(:const, nil, :Array), :===, s(:lvar, locals[:arr])), raise_error(:TypeError, "#deconstruct must return Array")))))) ) end
deconstructed_key(key)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 960 def deconstructed_key(key) return deconstructed_keys[key] if deconstructed_keys.key?(key) deconstructed_keys[key] = :"k#{deconstructed_keys.size}" end
hash_element(head, *tail)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 799 def hash_element(head, *tail) send("#{head.type}_hash_element", head).then do |node| next node if tail.empty? right = hash_element(*tail) next node if right.nil? s(:begin, s(:and, node, right)) end end
hash_has_key(key, hash = s(:lvar, locals[:hash]))
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 882 def hash_has_key(key, hash = s(:lvar, locals[:hash])) s(:send, hash, :key?, key.to_ast_node) end
hash_pattern_array_element(node, index)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 619 def hash_pattern_array_element(node, index) element = arr_item_at(index) locals.with(hash: locals[:arr, index]) do predicates.push :"i#{index}" hash_pattern_clause(node, element).tap { predicates.pop } end end
hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
click to toggle source
HASH PATTERN (START) ===============¶ ↑
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 666 def hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee])) # Optimization: avoid hash modifications when not needed # (we use #dup and #delete when "reading" values when **rest is present # to assign the rest of the hash copy to it) @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern } keys = hash_pattern_destruction_keys(node.children) specified_key_names = hash_pattern_keys(node.children) deconstruct_keys_node(keys, matchee).then do |dnode| right = if node.children.empty? case_eq_clause(s(:hash), s(:lvar, locals[:hash])) elsif specified_key_names.empty? hash_element(*node.children) else s(:begin, s(:and, having_hash_keys(specified_key_names), hash_element(*node.children))) end predicates.pop next dnode if right.nil? s(:begin, s(:and, dnode, right)) end end
hash_pattern_destruction_keys(children)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 708 def hash_pattern_destruction_keys(children) return s(:nil) if children.empty? children.filter_map do |child| # Skip ** without var next if child.type == :match_rest && child.children.empty? return s(:nil) if child.type == :match_rest || child.type == :match_nil_pattern send("#{child.type}_hash_key", child) end.then { |keys| s(:array, *keys) } end
hash_pattern_hash_element(node, key)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 781 def hash_pattern_hash_element(node, key) element = hash_value_at(key) key_index = deconstructed_key(key) locals.with(hash: locals[:hash, key_index]) do predicates.push :"k#{key_index}" hash_pattern_clause(node, element).tap { predicates.pop } end end
hash_pattern_keys(children)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 699 def hash_pattern_keys(children) children.filter_map do |child| # Skip ** without var next if child.type == :match_rest || child.type == :match_nil_pattern send("#{child.type}_hash_key", child) end end
hash_value_at(key, hash = s(:lvar, locals[:hash]))
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 868 def hash_value_at(key, hash = s(:lvar, locals[:hash])) return s(:lvar, locals.fetch(:hash_element)) if locals.key?(:hash_element) if @hash_match_rest s(:send, hash, :delete, key.to_ast_node) else s(:index, hash, key.to_ast_node) end end
having_hash_keys(keys, hash = s(:lvar, locals[:hash]))
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 888 def having_hash_keys(keys, hash = s(:lvar, locals[:hash])) keys.reduce(nil) do |acc, key| pnode = hash_has_key(key, hash) next pnode unless acc s(:begin, s(:and, acc, pnode)) end.then do |node| predicates.hash_keys(node, keys) end end
inline_blocks(source)
click to toggle source
Unparser generates `do .. end` or `{ … }` multiline blocks, we want to have single-line blocks with `{ … }`.
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 968 def inline_blocks(source) source.gsub(/(?:do|{) \|_, __i__\|\n\s*([^\n]+)\n\s*(?:end|})/, '{ |_, __i__| \1 }') end
match_alt_array_element(node, index)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 627 def match_alt_array_element(node, index) children = node.children.map do |child, i| send :"#{child.type}_array_element", child, index end s(:begin, s(:or, *children)) end
match_alt_clause(node)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 392 def match_alt_clause(node) children = locals.with(ALTERNATION_MARKER => true) do node.children.map.with_index do |child, i| predicates.terminate! if i == 1 send :"#{child.type}_clause", child end end s(:begin, s(:or, *children)) end
match_alt_hash_element(node, key)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 819 def match_alt_hash_element(node, key) element_node = s(:begin, s(:lvasgn, locals[:hash, :el], hash_value_at(key))) children = locals.with(hash_element: locals[:hash, :el]) do node.children.map do |child, i| send :"#{child.type}_hash_element", child, key end end s(:begin, s(:and, s(:begin, s(:or, element_node, s(:true))), s(:begin, s(:or, *children)))) end
match_as_array_element(node, index)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 638 def match_as_array_element(node, index) match_as_clause(node, arr_item_at(index)) end
match_as_clause(node, right = s(:lvar, locals[:matchee]))
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 402 def match_as_clause(node, right = s(:lvar, locals[:matchee])) s(:begin, s(:and, send(:"#{node.children[0].type}_clause", node.children[0], right), match_var_clause(node.children[1], right))) end
match_as_hash_element(node, key)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 838 def match_as_hash_element(node, key) match_as_clause(node, hash_value_at(key)) end
match_nil_pattern_hash_element(node, _key = nil)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 847 def match_nil_pattern_hash_element(node, _key = nil) s(:send, s(:lvar, locals[:hash]), :empty?) end
match_rest_hash_element(node, _key = nil)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 853 def match_rest_hash_element(node, _key = nil) # case {}; in **; end return if node.children.empty? child = node.children[0] raise ArgumentError, "Unknown hash match_rest child: #{child.type}" unless child.type == :match_var match_var_clause(child, s(:lvar, locals[:hash])) end
match_var_array_element(node, index)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 634 def match_var_array_element(node, index) match_var_clause(node, arr_item_at(index)) end
match_var_clause(node, left = s(:lvar, locals[:matchee]))
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 409 def match_var_clause(node, left = s(:lvar, locals[:matchee])) return s(:true) if node.children[0] == :_ check_match_var_alternation! node.children[0] s(:begin, s(:or, s(:begin, s(:lvasgn, node.children[0], left)), s(:true))) end
match_var_hash_element(node, key = nil)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 842 def match_var_hash_element(node, key = nil) key ||= node.children[0] match_var_clause(node, hash_value_at(key)) end
match_var_hash_key(node)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 724 def match_var_hash_key(node) check_match_var_alternation! node.children[0] s(:sym, node.children[0]) end
method_missing(mid, *args, &block)
click to toggle source
Calls superclass method
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 938 def method_missing(mid, *args, &block) mid = mid.to_s return case_eq_clause(*args) if mid.match?(/_clause$/) return case_eq_array_element(*args) if mid.match?(/_array_element$/) return case_eq_hash_element(*args) if mid.match?(/_hash_element$/) super end
no_matching_pattern()
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 914 def no_matching_pattern raise_error( :NoMatchingPatternError, s(:send, s(:lvar, locals[:matchee]), :inspect) ) end
pair_hash_element(node, _key = nil)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 814 def pair_hash_element(node, _key = nil) key, val = *node.children send("#{val.type}_hash_element", val, key) end
pair_hash_key(node)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 720 def pair_hash_key(node) node.children[0] end
pin_array_element(node, index)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 642 def pin_array_element(node, index) case_eq_array_element node.children[0], index end
pin_clause(node, right = s(:lvar, locals[:matchee]))
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 420 def pin_clause(node, right = s(:lvar, locals[:matchee])) predicates.terminate! case_eq_clause node.children[0], right end
raise_error(type, msg = "")
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 922 def raise_error(type, msg = "") s(:send, s(:const, nil, :Kernel), :raise, s(:const, nil, type), msg.to_ast_node) end
respond_to_check(node, mid)
click to toggle source
Add respond_to? check
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 929 def respond_to_check(node, mid) s(:send, node, :respond_to?, mid.to_ast_node) end
respond_to_missing?(mid, *)
click to toggle source
Calls superclass method
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 933 def respond_to_missing?(mid, *) return true if mid.to_s.match?(/_(clause|array_element)/) super end
rewrite_case_in!(node, matchee, new_node)
click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 311 def rewrite_case_in!(node, matchee, new_node) replace(node.loc.keyword, "case; when (#{unparse(matchee)}) && false") remove(node.children[0].loc.expression) node.children[1..-1].each.with_index do |clause, i| if clause&.type == :in_pattern # handle multiline clauses differently if clause.loc.last_line > clause.children[0].loc.last_line + 1 height = clause.loc.last_line - clause.children[0].loc.last_line padding = "\n" * height body_indent = " " * clause.children[2].loc.column replace( clause.loc.expression, "when #{inline_blocks(unparse(new_node.children[i].children[0]))}" \ "#{padding}" \ "#{body_indent}#{clause.children[2].loc.expression.source}" ) else replace( clause.loc.keyword.end.join(clause.children[0].loc.expression.end), inline_blocks(unparse(new_node.children[i].children[0])) ) remove(clause.children[1].loc.expression) if clause.children[1] replace(clause.loc.keyword, "when ") end elsif clause.nil? insert_after(node.children[-2].loc.expression, "; else; #{unparse(new_node.children.last)}") end end end
with_guard(node, guard)
click to toggle source