class RuboCop::AST::NodePattern::Compiler::SequenceSubcompiler

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

rubocop:disable Metrics/ClassLength

Constants

DELTA
POSITIVE

Attributes

cur_index[R]

@api private

in_sync[R]

@api private

Public Class Methods

new(compiler, sequence:, var:) click to toggle source

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`

Calls superclass method
# 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

Public Instance Methods

compile_sequence() click to toggle source
# 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

Protected Instance Methods

compile_terms(children = @seq.children, last_arity = 0..0) click to toggle source
# File lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb, line 226
def compile_terms(children = @seq.children, last_arity = 0..0)
  arities = remaining_arities(children, last_arity)
  total_arity = arities.shift
  guard = compile_child_nb_guard(total_arity)
  return guard if children.empty?

  @remaining_arity = total_arity
  terms = children.map do |child|
    use_index_from_end
    @remaining_arity = arities.shift
    handle_prev { compile(child) }
  end
  [guard, terms].join(" &&\n")
end
sync() { |code| ... } click to toggle source

yield `sync_code` iff not already in sync

# File lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb, line 242
def sync
  return if @in_sync

  code = compile_loop_advance("= #{compile_cur_index}")
  @in_sync = true
  yield code
end

Private Instance Methods

compile_and_advance(term) click to toggle source

Compilation helpers

# 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) click to toggle source
# 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_else() click to toggle source

@return [Array<String>] Else code, and init code (if any)

# 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) click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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() click to toggle source
# 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) click to toggle source
# 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) click to toggle source
# 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') click to toggle source
# 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) click to toggle source

Assumes `@cur_index` is already updated

# 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() click to toggle source
# 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_check() click to toggle source

@return [String] code that evaluates to `false` if the matched arity is too small

# 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() click to toggle source
# 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_union_forks() click to toggle source

@return [Hash] of {subcompiler => code}

# File lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb, line 373
def compile_union_forks
  compiler.each_union(node.children).map 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.to_h # we could avoid map if RUBY_VERSION >= 2.6...
end
empty_loop() click to toggle source
# 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() { || ... } click to toggle source
# 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) click to toggle source

Modifies in place `forks` Syncs our state

# 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) click to toggle source

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

# 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) click to toggle source

@return [Array<Range>] total arities (as Ranges) of remaining children nodes E.g. For sequence `(_ _? <_ _>)`, arities are: 1, 0..1, 2 and remaining arities are: 3..4, 2..3, 2..2, 0..0

# 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() click to toggle source

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

# 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() click to toggle source
# 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() click to toggle source
# 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() click to toggle source

Single node patterns are all handled here

# 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() click to toggle source
# 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() click to toggle source
# File lib/rubocop/ast/node_pattern/compiler/sequence_subcompiler.rb, line 159
def visit_rest
  empty_loop
end
visit_union() click to toggle source
# 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() { || ... } click to toggle source

NOTE: assumes `@cur_index != :seq_head`. Node types using `within_loop` must have `def in_sequence_head; :raise; end`

# 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