# frozen_string_literal: truemoduleRuboCopmoduleASTclassNodePatternclassCompiler# 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/ClassLengthclassSequenceSubcompiler<SubcompilerDELTA=1POSITIVE=:positive?.to_procprivate_constant:POSITIVE# 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`definitialize(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 matchingsuper(compiler)enddefcompile_sequence# rubocop:disable Layout/CommentIndentationcompiler.with_temp_variablesdo|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_termsend# rubocop:enable Layout/CommentIndentationendprivateprivate:compile# Not meant to be called from outside# Single node patterns are all handled heredefvisit_other_typeaccess=case@cur_indexwhen:seq_head{var: @seq_var,seq_head: true}when:variadic_mode{var: @cur_child_var}elseidx=@cur_index+(@cur_index.negative??DELTA:0){access: "#{@seq_var}.children[#{idx}]"}endterm=compiler.compile_as_node_pattern(node,**access)compile_and_advance(term)enddefvisit_repetitionwithin_loopdochild_captures=node.child.nb_captureschild_code=compile(node.child)nextcompile_loop(child_code)ifchild_captures.zero?compile_captured_repetition(child_code,child_captures)endenddefvisit_any_orderwithin_loopdocompiler.with_temp_variablesdo|matched|case_terms=compile_any_order_branches(matched)else_code,init=compile_any_order_elseterm="#{compile_case(case_terms,else_code)} && #{compile_loop_advance}"all_matched_check="&&\n#{matched}.size == #{node.term_nodes.size}"ifnode.rest_node<<~RUBY
(#{init}#{matched} = {}; true) &&
#{compile_loop(term)}#{all_matched_check}\\ RUBYendendenddefvisit_unionreturnvisit_other_typeifnode.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_forkspreserve_union_start(forks)merge_forks!(forks)expr=forks.values.join(" || \n")"(#{expr})"enddefcompile_case(when_branches,else_code)<<~RUBY
case
#{when_branches.join(' ')}
else #{else_code}
end \\ RUBYenddefcompile_any_order_branches(matched_var)node.term_nodes.map.with_indexdo|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"endend# @return [Array<String>] Else code, and init code (if any)defcompile_any_order_elserest=node.rest_nodeif!rest'false'elsifrest.capture?capture_rest=compiler.next_captureinit="#{capture_rest} = [];"["#{capture_rest} << #{@cur_child_var}",init]else'true'endenddefvisit_capturereturnvisit_other_typeifnode.child.arity==1storage=compiler.next_captureterm=compile(node.child)capture="#{@seq_var}.children[#{compile_matched(:range)}]""#{term} && (#{storage} = #{capture})"enddefvisit_restempty_loopend# Compilation helpersdefcompile_and_advance(term)case@cur_indexwhen:variadic_mode"#{term} && #{compile_loop_advance}"when:seq_head# @in_sync = false # already the case@cur_index=0termelse@in_sync=false@cur_index+=1termendenddefcompile_captured_repetition(child_code,child_captures)captured_range="#{compiler.captures-child_captures}...#{compiler.captures}"captured="captures[#{captured_range}]"compiler.with_temp_variablesdo|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) \\ RUBYendend# Assumes `@cur_index` is already updateddefcompile_matched(kind)to=compile_cur_indexfrom=if@prev_index==:variadic_mode@prev_index_used=true@prev_index_varelsecompile_index(@prev_index)endcasekindwhen:range"#{from}...#{to}"when:length"#{to} - #{from}"endenddefhandle_prev@prev_index=@cur_index@prev_index_used=falsecode=yieldif@prev_index_used@prev_index_used=falsecode="(#{@prev_index_var} = #{@cur_index_var}; true) && #{code}"endcodeenddefcompile_terms(children=@seq.children,last_arity=0..0)arities=remaining_arities(children,last_arity)total_arity=arities.shiftguard=compile_child_nb_guard(total_arity)returnguardifchildren.empty?@remaining_arity=total_arityterms=children.mapdo|child|use_index_from_end@remaining_arity=arities.shifthandle_prev{compile(child)}end[guard,terms].join(" &&\n")end# yield `sync_code` iff not already in syncdefsyncreturnif@in_synccode=compile_loop_advance("= #{compile_cur_index}")@in_sync=trueyieldcodeend# @api privateattr_reader:in_sync,:cur_indexpublic:in_syncprotected:cur_index,:compile_terms,:sync# @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..0defremaining_arities(children,last_arity)last=last_arityarities=children.reverse.map(&:arity_range).map{|r|last=last.begin+r.begin..last.max+r.max}.reverse!arities.pushlast_arityend# @return [String] code that evaluates to `false` if the matched arity is too smalldefcompile_min_checkreturn'false'unlessnode.variadic?unless@remaining_arity.end.infinite?not_too_much_remaining="#{compile_remaining} <= #{@remaining_arity.max}"endmin_to_match=node.arity_range.beginifmin_to_match.positive?enough_matched="#{compile_matched(:length)} >= #{min_to_match}"endreturn'true'unlessnot_too_much_remaining||enough_matched[not_too_much_remaining,enough_matched].compact.join(' && ')enddefcompile_remainingoffset=case@cur_indexwhen:seq_head' + 1'when:variadic_mode" - #{@cur_index_var}"when0''whenPOSITIVE" - #{@cur_index}"else# odd compiling condition, result may not be expected# E.g: `(... {a | b c})` => the b c branch can never matchreturn-(@cur_index+DELTA)end"#{@seq_var}.children.size #{offset}"enddefcompile_max_matchedreturnnode.arityunlessnode.variadic?min_remaining_children="#{compile_remaining} - #{@remaining_arity.begin}"returnmin_remaining_childrenifnode.arity.end.infinite?"[#{min_remaining_children}, #{node.arity.max}].min"enddefempty_loop@cur_index=-@remaining_arity.begin-DELTA@in_sync=false'true'enddefcompile_cur_indexreturn@cur_index_varif@in_synccompile_indexenddefcompile_index(cur=@cur_index)returncurifcur>=0"#{@seq_var}.children.size - #{-(cur+DELTA)}"end# Note: assumes `@cur_index != :seq_head`. Node types using `within_loop` must# have `def in_sequence_head; :raise; end`defwithin_loopsyncdo|sync_code|@cur_index=:variadic_mode"#{sync_code} && #{yield}"end||yieldend# returns truthy iff `@cur_index` switched to relative from end mode (i.e. < 0)defuse_index_from_endreturnif@cur_index==:seq_head||@remaining_arity.begin!=@remaining_arity.max@cur_index=-@remaining_arity.begin-DELTAenddefcompile_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)"enddefcompile_loop(term)<<~RUBY
(#{compile_max_matched}).times do
break #{compile_min_check} unless #{term}
end \\ RUBYenddefcompile_child_nb_guard(arity_range)casearity_range.maxwhenFloat::INFINITY"#{compile_remaining} >= #{arity_range.begin}"whenarity_range.begin"#{compile_remaining} == #{arity_range.begin}"else"(#{arity_range.begin}..#{arity_range.max}).cover?(#{compile_remaining})"endend# @return [Hash] of {subcompiler => code}defcompile_union_forkscompiler.each_union(node.children).mapdo|child|subsequence_terms=child.is_a?(Node::Subsequence)?child.children:[child]fork=dupcode=fork.compile_terms(subsequence_terms,@remaining_arity)@in_sync=falseif@cur_index!=:variadic_mode[fork,code]end.to_h# we could avoid map if RUBY_VERSION >= 2.6...end# Modifies in place `forks` to insure that `cur_{child|index}_var` are okdefpreserve_union_start(forks)returnif@cur_index!=:variadic_mode||forks.size<=1compiler.with_temp_variablesdo|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=resetcodeendendend# Modifies in place `forks`# Syncs our statedefmerge_forks!(forks)sub_compilers=forks.keysif!node.variadic?# e.g {a b | c d}@cur_index=sub_compilers.first.cur_index# all cur_index should be equivalentelsifuse_index_from_end# nothing to doelse# can't use index from end, so we must sync all forks@cur_index=:variadic_modeforks.eachdo|sub,code|sub.sync{|sync_code|forks[sub]="#{code} && #{sync_code}"}endend@in_sync=sub_compilers.all?(&:in_sync)endend# rubocop:enable Metrics/ClassLengthendendendend