Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NodePattern: Set optimization #111

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/rubocop/ast.rb
Expand Up @@ -20,6 +20,7 @@
require_relative 'ast/node_pattern/lexer'
require_relative 'ast/node_pattern/node'
require_relative 'ast/node_pattern/parser'
require_relative 'ast/node_pattern/sets'
require_relative 'ast/sexp'
require_relative 'ast/node'
require_relative 'ast/node/mixin/method_identifier_predicates'
Expand Down
7 changes: 6 additions & 1 deletion lib/rubocop/ast/node_pattern/builder.rb
Expand Up @@ -34,7 +34,8 @@ def emit_call(type, selector, args = nil)
def emit_union(begin_t, pattern_lists, end_t)
children = union_children(pattern_lists)

emit_list(:union, begin_t, children, end_t)
type = optimizable_as_set?(children) ? :set : :union
emit_list(type, begin_t, children, end_t)
end

def emit_subsequence(node_list)
Expand All @@ -45,6 +46,10 @@ def emit_subsequence(node_list)

private

def optimizable_as_set?(children)
children.all?(&:matches_within_set?)
end

def n(type, *args)
Node::MAP[type].new(type, *args)
end
Expand Down
5 changes: 5 additions & 0 deletions lib/rubocop/ast/node_pattern/compiler/atom_subcompiler.rb
Expand Up @@ -36,6 +36,11 @@ def visit_positional_parameter
compiler.positional_parameter(node.child)
end

def visit_set
set = node.children.map(&:child).to_set.freeze
NodePattern::Sets[set]
end

# Assumes other types are node patterns.
def visit_other_type
compiler.with_temp_variables do |compare|
Expand Down
9 changes: 9 additions & 0 deletions lib/rubocop/ast/node_pattern/node.rb
Expand Up @@ -8,6 +8,9 @@ class Node < ::Parser::AST::Node
extend Forwardable
include ::RuboCop::AST::Descendence

MATCHES_WITHIN_SET = %i[symbol number string].to_set.freeze
private_constant :MATCHES_WITHIN_SET

###
# To be overriden by subclasses
###
Expand Down Expand Up @@ -55,6 +58,12 @@ def variadic?
arity.is_a?(Range)
end

# @return [Boolean] returns true for nodes having a Ruby literal equivalent
# that matches withing a Set (e.g. `42`, `:sym` but not `/regexp/`)
def matches_within_set?
MATCHES_WITHIN_SET.include?(type)
end

# @return [Range] arity as a Range
def arity_range
a = arity
Expand Down
37 changes: 37 additions & 0 deletions lib/rubocop/ast/node_pattern/sets.rb
@@ -0,0 +1,37 @@
# frozen_string_literal: true

module RuboCop
module AST
class NodePattern
# Utility to assign a set of values to a constant
module Sets
REGISTRY = Hash.new do |h, set|
name = Sets.name(set).freeze
Sets.const_set(name, set)
h[set] = "::RuboCop::AST::NodePattern::Sets::#{name}"
end

MAX = 4
def self.name(set)
elements = set
elements = set.first(MAX - 1) << :etc if set.size > MAX
name = elements.to_a.join('_').upcase.gsub(/[^A-Z0-9_]/, '')
uniq("SET_#{name}")
end

def self.uniq(name)
return name unless Sets.const_defined?(name)

(2..Float::INFINITY).each do |i|
uniq = "#{name}_#{i}"
return uniq unless Sets.const_defined?(uniq)
end
end

def self.[](set)
REGISTRY[set]
end
end
end
end
end
10 changes: 10 additions & 0 deletions spec/rubocop/ast/node_pattern/parser_spec.rb
Expand Up @@ -61,6 +61,16 @@
)
end

it 'parses unions of literals as a set' do
expect_parsing(
s(:sequence, s(:set, s(:symbol, :a), s(:number, 42), s(:string, 'hello'))),
'({:a 42 "hello"})',
' ^ begin (set)
| ^ end (set)
| ~~ expression (set/1.number)'
)
end

it 'generates specialized nodes' do
source_file = Parser::Source::Buffer.new('(spec)', source: '($_)')
ast = parser.parse(source_file)
Expand Down
17 changes: 17 additions & 0 deletions spec/rubocop/ast/node_pattern/sets_spec.rb
@@ -0,0 +1,17 @@
# frozen_string_literal: true

RSpec.describe RuboCop::AST::NodePattern::Sets do
subject(:name) { described_class[set] }

let(:set) { Set[1, 2, 3, 4, 5, 6] }

it { is_expected.to eq '::RuboCop::AST::NodePattern::Sets::SET_1_2_3_ETC' }

it { is_expected.to eq described_class[Set[6, 5, 4, 3, 2, 1]] }

it { is_expected.not_to eq described_class[Set[1, 2, 3, 4, 5, 6, 7]] }

it 'creates a constant with the right value' do
expect(eval(name)).to eq set # rubocop:disable Security/Eval
end
end