123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::AST::NodePattern::Compiler::SequenceSubcompiler

Relationships & Source Files
Extension / Inclusion / Inheritance Descendants
Subclasses:
RuboCop::AST::NodePattern::Compiler::Debug::SequenceSubcompiler
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Subcompiler
Instance Chain:
self, Subcompiler
Inherits: RuboCop::AST::NodePattern::Compiler::Subcompiler
Defined in: lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb

Overview

Compiles terms within a sequence to code that evalues to true or false. Compilation of the nodes that can match only a single term is deferred to NodePatternSubcompiler; only nodes that can match multiple terms are compiled here. Assumes the given var is a ::RuboCop::AST::Node

Doc on how this fits in the compiling process: /docs/modules/ROOT/pages/node_pattern.adoc

Constant Summary

Class Attribute Summary

Subcompiler - Inherited

Class Method Summary

Subcompiler - Inherited

Instance Attribute Summary

  • #in_sync readonly Internal use only Internal use only

Subcompiler - Inherited

Instance Method Summary

Subcompiler - Inherited

Constructor Details

.new(compiler, sequence:, var:) ⇒ SequenceSubcompiler

Calls compile_sequence; the actual compile method will be used for the different terms of the sequence. The only case of re-entrant call to compile is visit_capture

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 25

def initialize(compiler, sequence:, var:)
  @seq = sequence # The node to be compiled
  @seq_var = var  # Holds the name of the variable holding the AST::Node we are matching
  super(compiler)
end

Instance Attribute Details

#in_sync (readonly)

This method is for internal use only.
[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 251

attr_reader :in_sync, :cur_index

Instance Method Details

#compile_and_advance(term) (private)

Compilation helpers

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 165

def compile_and_advance(term)
  case @cur_index
  when :variadic_mode
    "#{term} && #{compile_loop_advance}"
  when :seq_head
    # @in_sync = false # already the case
    @cur_index = 0
    term
  else
    @in_sync = false
    @cur_index += 1
    term
  end
end

#compile_any_order_branches(matched_var) (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 128

def compile_any_order_branches(matched_var)
  node.term_nodes.map.with_index do |node, i|
    code = compiler.compile_as_node_pattern(node, var: @cur_child_var, seq_head: false)
    var = "#{matched_var}[#{i}]"
    "when !#{var} && #{code} then #{var} = true"
  end
end

#compile_any_order_elseArray<String> (private)

Returns:

  • (Array<String>)

    Else code, and init code (if any)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 137

def compile_any_order_else
  rest = node.rest_node
  if !rest
    'false'
  elsif rest.capture?
    capture_rest = compiler.next_capture
    init = "#{capture_rest} = [];"
    ["#{capture_rest} << #{@cur_child_var}", init]
  else
    'true'
  end
end

#compile_captured_repetition(child_code, child_captures) (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 180

def compile_captured_repetition(child_code, child_captures)
  captured_range = "#{compiler.captures - child_captures}...#{compiler.captures}"
  captured = "captures[#{captured_range}]"
  compiler.with_temp_variables do |accumulate|
    code = "#{child_code} && #{accumulate}.push(#{captured})"
    <<~RUBY
      (#{accumulate} = Array.new) &&
      #{compile_loop(code)} &&
      (#{captured} = if #{accumulate}.empty?
        (#{captured_range}).map{[]} # Transpose hack won't work for empty case
      else
        #{accumulate}.transpose
      end) \\
    RUBY
  end
end

#compile_case(when_branches, else_code) (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 119

def compile_case(when_branches, else_code)
  <<~RUBY
    case
    #{when_branches.join('    ')}
    else #{else_code}
    end \\
  RUBY
end

#compile_child_nb_guard(arity_range) (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 361

def compile_child_nb_guard(arity_range)
  case arity_range.max
  when Float::INFINITY
    "#{compile_remaining} >= #{arity_range.begin}"
  when arity_range.begin
    "#{compile_remaining} == #{arity_range.begin}"
  else
    "(#{arity_range.begin}..#{arity_range.max}).cover?(#{compile_remaining})"
  end
end

#compile_cur_index (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 319

def compile_cur_index
  return @cur_index_var if @in_sync

  compile_index
end

#compile_index(cur = @cur_index) (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 325

def compile_index(cur = @cur_index)
  return cur if cur >= 0

  "#{@seq_var}.children.size - #{-(cur + DELTA)}"
end

#compile_loop(term) (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 353

def compile_loop(term)
  <<~RUBY
    (#{compile_max_matched}).times do
      break #{compile_min_check} unless #{term}
    end \\
  RUBY
end

#compile_loop_advance(to = '+=1') (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 347

def compile_loop_advance(to = '+=1')
  # The `#{@cur_child_var} ||` is just to avoid unused variable warning
  "(#{@cur_child_var} = #{@seq_var}.children[#{@cur_index_var} #{to}]; " \
    "#{@cur_child_var} || true)"
end

#compile_matched(kind) (private)

Assumes @cur_index is already updated

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 198

def compile_matched(kind)
  to = compile_cur_index
  from = if @prev_index == :variadic_mode
           @prev_index_used = true
           @prev_index_var
         else
           compile_index(@prev_index)
         end
  case kind
  when :range
    "#{from}...#{to}"
  when :length
    "#{to} - #{from}"
  end
end

#compile_max_matched (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 304

def compile_max_matched
  return node.arity unless node.variadic?

  min_remaining_children = "#{compile_remaining} - #{@remaining_arity.begin}"
  return min_remaining_children if node.arity.end.infinite?

  "[#{min_remaining_children}, #{node.arity.max}].min"
end

#compile_min_checkString (private)

Returns:

  • (String)

    code that evaluates to false if the matched arity is too small

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 270

def compile_min_check
  return 'false' unless node.variadic?

  unless @remaining_arity.end.infinite?
    not_too_much_remaining = "#{compile_remaining} <= #{@remaining_arity.max}"
  end
  min_to_match = node.arity_range.begin
  if min_to_match.positive?
    enough_matched = "#{compile_matched(:length)} >= #{min_to_match}"
  end
  return 'true' unless not_too_much_remaining || enough_matched

  [not_too_much_remaining, enough_matched].compact.join(' && ')
end

#compile_remaining (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 285

def compile_remaining
  offset = case @cur_index
           when :seq_head
             ' + 1'
           when :variadic_mode
             " - #{@cur_index_var}"
           when 0
             ''
           when POSITIVE
             " - #{@cur_index}"
           else
             # odd compiling condition, result may not be expected
             # E.g: `(... {a | b c})` => the b c branch can never match
             return - (@cur_index + DELTA)
           end

  "#{@seq_var}.children.size #{offset}"
end

#compile_sequence

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 31

def compile_sequence
  # rubocop:disable Layout/CommentIndentation
  compiler.with_temp_variables do |cur_child, cur_index, previous_index|
    @cur_child_var = cur_child        # To hold the current child node
    @cur_index_var = cur_index        # To hold the current child index (always >= 0)
    @prev_index_var = previous_index  # To hold the child index before we enter the
                                      # variadic nodes
    @cur_index = :seq_head            # Can be any of:
                                      # :seq_head : when the current child is actually the
                                      #             sequence head
                                      # :variadic_mode : child index held by @cur_index_var
                                      # >= 0 : when the current child index is known
                                      #        (from the beginning)
                                      # < 0 :  when the index is known from the end,
                                      #        where -1 is *past the end*,
                                      #        -2 is the last child, etc...
                                      #        This shift of 1 from standard Ruby indices
                                      #        is stored in DELTA
    @in_sync = false                  # `true` iff `@cur_child_var` and `@cur_index_var`
                                      # correspond to `@cur_index`
                                      # Must be true if `@cur_index` is `:variadic_mode`
    compile_terms
  end
  # rubocop:enable Layout/CommentIndentation
end

#compile_union_forksHash (private)

Returns:

  • (Hash)

    of ⇒ code

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 373

def compile_union_forks
  compiler.each_union(node.children).to_h do |child|
    subsequence_terms = child.is_a?(Node::Subsequence) ? child.children : [child]
    fork = dup
    code = fork.compile_terms(subsequence_terms, @remaining_arity)
    @in_sync = false if @cur_index != :variadic_mode
    [fork, code]
  end
end

#empty_loop (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 313

def empty_loop
  @cur_index = -@remaining_arity.begin - DELTA
  @in_sync = false
  'true'
end

#handle_prev (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 214

def handle_prev
  @prev_index = @cur_index
  @prev_index_used = false
  code = yield
  if @prev_index_used
    @prev_index_used = false
    code = "(#{@prev_index_var} = #{@cur_index_var}; true) && #{code}"
  end

  code
end

#merge_forks!(forks) (private)

Modifies in place forks Syncs our state

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 400

def merge_forks!(forks)
  sub_compilers = forks.keys
  if !node.variadic? # e.g {a b | c d}
    @cur_index = sub_compilers.first.cur_index # all cur_index should be equivalent
  elsif use_index_from_end
    # nothing to do
  else
    # can't use index from end, so we must sync all forks
    @cur_index = :variadic_mode
    forks.each do |sub, code|
      sub.sync { |sync_code| forks[sub] = "#{code} && #{sync_code}" }
    end
  end
  @in_sync = sub_compilers.all?(&:in_sync)
end

#preserve_union_start(forks) (private)

Modifies in place forks to insure that cur_{child|index}_var are ok

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 384

def preserve_union_start(forks)
  return if @cur_index != :variadic_mode || forks.size <= 1

  compiler.with_temp_variables do |union_reset|
    cur = "(#{union_reset} = [#{@cur_child_var}, #{@cur_index_var}]) && "
    reset = "(#{@cur_child_var}, #{@cur_index_var} = #{union_reset}) && "
    forks.transform_values! do |code|
      code = "#{cur}#{code}"
      cur = reset
      code
    end
  end
end

#remaining_arities(children, last_arity) ⇒ Array<Range> (private)

E.g. For sequence (_ ? < _>), arities are: 1, 0..1, 2 and remaining arities are: 3..4, 2..3, 2..2, 0..0

Returns:

  • (Array<Range>)

    total arities (as Ranges) of remaining children nodes

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 259

def remaining_arities(children, last_arity)
  last = last_arity
  arities = children
            .reverse
            .map(&:arity_range)
            .map { |r| last = (last.begin + r.begin)..(last.max + r.max) }
            .reverse!
  arities.push last_arity
end

#use_index_from_end (private)

returns truthy iff @cur_index switched to relative from end mode (i.e. < 0)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 341

def use_index_from_end
  return if @cur_index == :seq_head || @remaining_arity.begin != @remaining_arity.max

  @cur_index = -@remaining_arity.begin - DELTA
end

#visit_any_order (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 88

def visit_any_order
  within_loop do
    compiler.with_temp_variables do |matched|
      case_terms = compile_any_order_branches(matched)
      else_code, init = compile_any_order_else
      term = "#{compile_case(case_terms, else_code)} && #{compile_loop_advance}"

      all_matched_check = "&&\n#{matched}.size == #{node.term_nodes.size}" if node.rest_node
      <<~RUBY
        (#{init}#{matched} = {}; true) &&
        #{compile_loop(term)} #{all_matched_check} \\
      RUBY
    end
  end
end

#visit_capture (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 150

def visit_capture
  return visit_other_type if node.child.arity == 1

  storage = compiler.next_capture
  term = compile(node.child)
  capture = "#{@seq_var}.children[#{compile_matched(:range)}]"
  "#{term} && (#{storage} = #{capture})"
end

#visit_other_type (private)

Single node patterns are all handled here

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 62

def visit_other_type
  access = case @cur_index
           when :seq_head
             { var: @seq_var,
               seq_head: true }
           when :variadic_mode
             { var: @cur_child_var }
           else
             idx = @cur_index + (@cur_index.negative? ? DELTA : 0)
             { access: "#{@seq_var}.children[#{idx}]" }
           end

  term = compiler.compile_as_node_pattern(node, **access)
  compile_and_advance(term)
end

#visit_repetition (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 78

def visit_repetition
  within_loop do
    child_captures = node.child.nb_captures
    child_code = compile(node.child)
    next compile_loop(child_code) if child_captures.zero?

    compile_captured_repetition(child_code, child_captures)
  end
end

#visit_rest (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 159

def visit_rest
  empty_loop
end

#visit_union (private)

[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 104

def visit_union
  return visit_other_type if node.arity == 1

  # The way we implement complex unions is by "forking", i.e.
  # making a copy of the present subcompiler to compile each branch
  # of the union.
  # We then use the resulting state of the subcompilers to
  # reset ourselves.
  forks = compile_union_forks
  preserve_union_start(forks)
  merge_forks!(forks)
  expr = forks.values.join(" || \n")
  "(#{expr})"
end

#within_loop (private)

Note
assumes @cur_index != :seq_head. Node types using within_loop must have def in_sequence_head; :raise; end
[ GitHub ]

  
# File 'lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb', line 333

def within_loop
  sync do |sync_code|
    @cur_index = :variadic_mode
    "#{sync_code} && #{yield}"
  end || yield
end