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

rubocop:disable Metrics/ClassLength
/docs/modules/ROOT/pages/node_pattern.adoc
Doc on how this fits in the compiling process:
Assumes the given ‘var` is a `::RuboCop::AST::Node`
compiled here.
`NodePatternSubcompiler`; only nodes that can match multiple terms are
Compilation of the nodes that can match only a single term is deferred to
Compiles terms within a sequence to code that evalues to true or false.

def compile_and_advance(term)

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

def compile_any_order_branches(matched_var)

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

def compile_any_order_else

Returns:
  • (Array) - Else code, and init code (if any)
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

def compile_captured_repetition(child_code, child_captures)

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

def compile_case(when_branches, else_code)

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

def compile_child_nb_guard(arity_range)

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

def compile_cur_index

def compile_cur_index
  return @cur_index_var if @in_sync
  compile_index
end

def compile_index(cur = @cur_index)

def compile_index(cur = @cur_index)
  return cur if cur >= 0
  "#{@seq_var}.children.size - #{-(cur + DELTA)}"
end

def compile_loop(term)

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

def compile_loop_advance(to = '+=1')

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

def compile_matched(kind)

Assumes `@cur_index` is already updated
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

def compile_max_matched

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

def compile_min_check

Returns:
  • (String) - code that evaluates to `false` if the matched arity is too small
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

def compile_remaining

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

def compile_sequence

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 begining)
                                      # < 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

def compile_terms(children = @seq.children, last_arity = 0..0)

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

def compile_union_forks

Returns:
  • (Hash) - of {subcompiler => code}
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

def empty_loop

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

def handle_prev

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

def initialize(compiler, sequence:, var:)

The only case of re-entrant call to `compile` is `visit_capture`
will be used for the different terms of the sequence.
Calls `compile_sequence`; the actual `compile` method
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

def merge_forks!(forks)

Syncs our state
Modifies in place `forks`
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

def preserve_union_start(forks)

Modifies in place `forks` to insure that `cur_{child|index}_var` are ok
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

def remaining_arities(children, last_arity)

Returns:
  • (Array) - total arities (as Ranges) of remaining children nodes
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

def sync

yield `sync_code` iff not already in sync
def sync
  return if @in_sync
  code = compile_loop_advance("= #{compile_cur_index}")
  @in_sync = true
  yield code
end

def use_index_from_end

returns truthy iff `@cur_index` switched to relative from end mode (i.e. < 0)
def use_index_from_end
  return if @cur_index == :seq_head || @remaining_arity.begin != @remaining_arity.max
  @cur_index = -@remaining_arity.begin - DELTA
end

def visit_any_order

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

def visit_capture

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

def visit_other_type

Single node patterns are all handled here
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

def visit_repetition

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

def visit_rest

def visit_rest
  empty_loop
end

def visit_union

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

def within_loop

have `def in_sequence_head; :raise; end`
Note: assumes `@cur_index != :seq_head`. Node types using `within_loop` must
def within_loop
  sync do |sync_code|
    @cur_index = :variadic_mode
    "#{sync_code} && #{yield}"
  end || yield
end