Class: Ryac::Pipeline::ControlFlowSimplify
- Defined in:
- lib/ryac/pipeline/control_flow_simplify.rb,
sig/ryac/pipeline/control_flow_simplify.rbs
Constant Summary collapse
- COLLECTION_CONTEXT_BYTES =
Modifier if/unless/while/until is invalid inside array, hash, or argument contexts. Check if the byte before the node (skipping whitespace) is a comma, open bracket, or open paren.
[','.ord, '['.ord, '('.ord].freeze
- LOCAL_BINDING_NODES =
Every node kind that binds a local: plain writes, compound writes (||= &&= +=), and multi-assignment targets. In the modifier form the body parses before the condition runs, so a local the condition binds reads as a method call there — any of these kinds counts.
[ Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableTargetNode ].freeze
- OPERATOR_START_BYTES =
Check if
endkeyword is followed by an operator (e.g.,if...end*(expr)) In that case, ternary conversion needs parentheses. [ '*'.ord, '/'.ord, '%'.ord, '+'.ord, '-'.ord, '&'.ord, '|'.ord, '^'.ord, '<'.ord, '>'.ord, '.'.ord, '['.ord ].freeze
Instance Method Summary collapse
- #build_ternary(cond_node, cond, then_node, then_expr, else_expr) ⇒ String
- #collect(ctx, patches) ⇒ void
- #collect_assigned_vars(node) ⇒ Set[Symbol]
- #collect_read_vars(node) ⇒ Set[Symbol]
- #collect_tail_returns(node, patches) ⇒ void
- #condition_assigns_var_used_in_body?(predicate, body) ⇒ Boolean
-
#contains_tail_return_patch?(node) ⇒ Boolean
A same-pass conflict guard: a construct on a tail path may itself be offered as a modifier/ternary rewrite while the return inside it is already patched.
- #else_text_for_ternary(subsequent, source) ⇒ String?
-
#fixpoint? ⇒ Boolean
A collapse can make the enclosing construct collapsible in turn, so this runs to a fixed point.
- #if_end_followed_by_operator?(node, source) ⇒ Boolean
- #in_collection_context?(node, source) ⇒ Boolean
-
#logic_operand_text(pred, text) ⇒ String
The operand rule is the Compactor's — CFS emits into its dialect.
- #too_complex_for_ternary?(text) ⇒ Boolean
- #traverse(node) {|node| ... } ⇒ void
- #try_if(node, source, statement_position) ⇒ String?
- #try_loop(node, source, statement_position, keyword) ⇒ String?
-
#try_sole_nested_if(node, inner, source) ⇒ String?
if awrapping nothing butif bruns the inner body exactly when a && b: the fold drops a whole if;end frame, and the fixpoint then offers the merged conditional to the modifier and ternary forms. -
#try_tail_return(node, patches) ⇒ void
The value of a def is its last expression, and "last expression" extends through every construct whose value the def passes along: both arms of an if/unless, every when/in body, a begin's body, rescue and else.
- #try_unless(node, source, statement_position) ⇒ String?
-
#walk(node, source, patches, statement_position = false) ⇒ void
statement_position: the node is a direct child of a statements list.
Methods inherited from Stage
#analysis, #analysis_options, #call, #finish, #needs_analysis?, new
Methods included from SourcePatcher
#apply_patches, #mk, #src, #verify_parses
Instance Method Details
#build_ternary(cond_node, cond, then_node, then_expr, else_expr) ⇒ String
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 203 def build_ternary(cond_node, cond, then_node, then_expr, else_expr) if then_node && AstUtils.modifier_control_flow?(then_node) then_expr = "(#{then_expr})" then_node = nil end # and/or keywords have lower precedence than ?: — must wrap in parens needs_parens = cond_node.is_a?(Prism::AndNode) || cond_node.is_a?(Prism::OrNode) cond = "(#{cond})" if needs_parens if !needs_parens && AstUtils.needs_ternary_q_space?(cond_node) q_pre = ' ' q_post = ' ' else q_pre = '' q_post = '' end if then_node && (AstUtils.ends_with_name_char?(then_node) || AstUtils.ends_with_method_suffix?(then_node)) colon_pre = ' ' colon_post = ' ' elsif else_expr&.start_with?(':') colon_pre = '' colon_post = ' ' else colon_pre = '' colon_post = '' end "#{cond}#{q_pre}?#{q_post}#{then_expr}#{colon_pre}:#{colon_post}#{else_expr}" end |
#collect(ctx, patches) ⇒ void
This method returns an undefined value.
12 13 14 15 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 12 def collect(ctx, patches) @tail_return_ranges = [] #: Array[[Integer, Integer]] walk(ctx.ast, ctx.source, patches) end |
#collect_assigned_vars(node) ⇒ Set[Symbol]
310 311 312 313 314 315 316 317 318 319 320 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 310 def collect_assigned_vars(node) vars = Set.new traverse(node) do |n| case n when *LOCAL_BINDING_NODES # @type var n: Prism::LocalVariableWriteNode | Prism::LocalVariableOrWriteNode | Prism::LocalVariableAndWriteNode | Prism::LocalVariableOperatorWriteNode | Prism::LocalVariableTargetNode vars << n.name end end vars end |
#collect_read_vars(node) ⇒ Set[Symbol]
322 323 324 325 326 327 328 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 322 def collect_read_vars(node) vars = Set.new traverse(node) do |n| vars << n.name if n.is_a?(Prism::LocalVariableReadNode) end vars end |
#collect_tail_returns(node, patches) ⇒ void
This method returns an undefined value.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 71 def collect_tail_returns(node, patches) case node when Prism::ReturnNode args = node.arguments&.arguments return unless args && !args.empty? return if args.any? { |a| a.is_a?(Prism::SplatNode) } replacement = args.size == 1 ? args[0].slice : "[#{args.map(&:slice).join(',')}]" patches << mk(node, replacement) @tail_return_ranges << [node.location.start_offset, node.location.end_offset] when Prism::StatementsNode collect_tail_returns(node.body[-1], patches) when Prism::IfNode collect_tail_returns(node.statements, patches) collect_tail_returns(node.subsequent, patches) when Prism::UnlessNode collect_tail_returns(node.statements, patches) collect_tail_returns(node.else_clause, patches) when Prism::ElseNode, Prism::WhenNode, Prism::InNode collect_tail_returns(node.statements, patches) when Prism::CaseNode, Prism::CaseMatchNode node.conditions.each { |c| collect_tail_returns(c, patches) } collect_tail_returns(node.else_clause, patches) when Prism::BeginNode # The ensure body is not a value position, but its presence # changes nothing about the others: it runs on both spellings. collect_tail_returns(node.statements, patches) collect_tail_returns(node.rescue_clause, patches) collect_tail_returns(node.else_clause, patches) when Prism::RescueNode collect_tail_returns(node.statements, patches) collect_tail_returns(node.subsequent, patches) end end |
#condition_assigns_var_used_in_body?(predicate, body) ⇒ Boolean
291 292 293 294 295 296 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 291 def condition_assigns_var_used_in_body?(predicate, body) assigned = collect_assigned_vars(predicate) return false if assigned.empty? read = collect_read_vars(body) assigned.intersect?(read) end |
#contains_tail_return_patch?(node) ⇒ Boolean
A same-pass conflict guard: a construct on a tail path may itself be offered as a modifier/ternary rewrite while the return inside it is already patched. The return wins the pass; the fixpoint re-offers the outer rewrite on the return-free text.
110 111 112 113 114 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 110 def contains_tail_return_patch?(node) s = node.location.start_offset e = node.location.end_offset @tail_return_ranges.any? { |rs, re| rs >= s && re <= e } end |
#else_text_for_ternary(subsequent, source) ⇒ String?
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 172 def else_text_for_ternary(subsequent, source) case subsequent # An elsif chain that just ends: the if's value is nil when no # branch takes, and `()` says so in ternary position. when nil '()' when Prism::ElseNode stmts = subsequent.statements text = stmts ? src(source, stmts) : nil return nil if too_complex_for_ternary?(text) else_node = stmts&.body&.first return nil if else_node.is_a?(Prism::MultiWriteNode) text = "(#{text})" if else_node && AstUtils.modifier_control_flow?(else_node) text when Prism::IfNode cond = src(source, subsequent.predicate) stmts = subsequent.statements then_body = stmts ? src(source, stmts) : nil return nil if too_complex_for_ternary?(then_body) else_result = else_text_for_ternary(subsequent.subsequent, source) return nil if else_result.nil? then_node = stmts.body.first if stmts # same rule as the top-level then: a multiple assignment's comma # cannot live inside a ternary branch return nil if then_node.is_a?(Prism::MultiWriteNode) build_ternary(subsequent.predicate, cond, then_node, then_body, else_result) end end |
#fixpoint? ⇒ Boolean
A collapse can make the enclosing construct collapsible in turn, so this runs to a fixed point.
10 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 10 def fixpoint? = true |
#if_end_followed_by_operator?(node, source) ⇒ Boolean
343 344 345 346 347 348 349 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 343 def if_end_followed_by_operator?(node, source) end_loc = node.end_keyword_loc return false unless end_loc after = end_loc.end_offset return false if after >= source.bytesize OPERATOR_START_BYTES.include?(source.getbyte(after)) end |
#in_collection_context?(node, source) ⇒ Boolean
278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 278 def in_collection_context?(node, source) pos = node.location.start_offset - 1 while pos >= 0 byte = source.getbyte(pos) if byte == ' '.ord || byte == "\n".ord || byte == "\r".ord || byte == "\t".ord pos -= 1 else return COLLECTION_CONTEXT_BYTES.include?(byte) end end false end |
#logic_operand_text(pred, text) ⇒ String
The operand rule is the Compactor's — CFS emits into its dialect.
168 169 170 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 168 def logic_operand_text(pred, text) Compactor.loose_logic_operand?(pred, tight: true) ? "(#{text})" : text end |
#too_complex_for_ternary?(text) ⇒ Boolean
268 269 270 271 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 268 def too_complex_for_ternary?(text) text && (text.include?(';') || text.match?(/\A(?:return|break|next|yield) /)) end |
#traverse(node) {|node| ... } ⇒ void
This method returns an undefined value.
330 331 332 333 334 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 330 def traverse(node, &block) return unless node yield node node.compact_child_nodes.each { |child| traverse(child, &block) } end |
#try_if(node, source, statement_position) ⇒ String?
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 116 def try_if(node, source, statement_position) cond = src(source, node.predicate) stmts = node.statements if node.subsequent.nil? return nil unless stmts && AstUtils.single_statement_body?(stmts) if (folded = try_sole_nested_if(node, stmts.body.first, source)) return folded end body = src(source, stmts) return nil if body.include?(';') return nil if condition_assigns_var_used_in_body?(node.predicate, stmts) if statement_position return nil if in_collection_context?(node, source) "#{body} if #{cond}" else # As a value, the bare modifier would capture the surrounding # expression; parenthesized it stays a self-contained nil-or-body. "(#{body} if #{cond})" end else then_body = stmts ? src(source, stmts) : nil else_result = else_text_for_ternary(node.subsequent, source) return nil if else_result.nil? return nil if too_complex_for_ternary?(then_body) then_node = stmts.body.first if stmts return nil if then_node.is_a?(Prism::MultiWriteNode) result = build_ternary(node.predicate, cond, then_node, then_body, else_result) result = "(#{result})" if AstUtils.ternary_needs_parens?(node, source) || if_end_followed_by_operator?(node, source) result end end |
#try_loop(node, source, statement_position, keyword) ⇒ String?
256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 256 def try_loop(node, source, statement_position, keyword) return nil unless statement_position stmts = node.statements return nil unless stmts && AstUtils.single_statement_body?(stmts) body = src(source, stmts) return nil if body.include?(';') return nil if condition_assigns_var_used_in_body?(node.predicate, stmts) return nil if in_collection_context?(node, source) cond = src(source, node.predicate) "#{body} #{keyword} #{cond}" end |
#try_sole_nested_if(node, inner, source) ⇒ String?
if a wrapping nothing but if b runs the inner body exactly when
a && b: the fold drops a whole if;end frame, and the fixpoint then
offers the merged conditional to the modifier and ternary forms.
Evaluation order is untouched — a, then b, then the body — so no
assignment guard is needed. Only else-less if folds into else-less
if: an elsif node is never a body statement, and if_keyword_loc
rules out the ternary form.
158 159 160 161 162 163 164 165 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 158 def try_sole_nested_if(node, inner, source) return nil unless inner.is_a?(Prism::IfNode) && inner.if_keyword_loc return nil unless inner.subsequent.nil? && inner.statements left = logic_operand_text(node.predicate, src(source, node.predicate)) right = logic_operand_text(inner.predicate, src(source, inner.predicate)) "if #{left}&&#{right};#{src(source, inner.statements)};end" end |
#try_tail_return(node, patches) ⇒ void
This method returns an undefined value.
The value of a def is its last expression, and "last expression"
extends through every construct whose value the def passes along:
both arms of an if/unless, every when/in body, a begin's body,
rescue and else. A return expr in any of those tail positions
only restates the flow, so the keyword goes; return a, b is the
array [a, b] spelled longer. Loops and blocks are opaque — a
return there escapes the method for real — and a bare return
is not the statement's own value, so both stay. A splat builds a
value the bare expression would not.
67 68 69 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 67 def try_tail_return(node, patches) collect_tail_returns(node.body, patches) end |
#try_unless(node, source, statement_position) ⇒ String?
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 235 def try_unless(node, source, statement_position) stmts = node.statements body = stmts ? src(source, stmts) : nil cond = src(source, node.predicate) modifier_ok = statement_position && body && !body.include?(';') && !condition_assigns_var_used_in_body?(node.predicate, stmts) && !in_collection_context?(node, source) if AstUtils.simple_negatable?(node.predicate) neg_cond = "!#{cond}" if modifier_ok "#{body} if #{neg_cond}" else "if #{neg_cond};#{body || ''};end" end elsif modifier_ok "#{body} unless #{cond}" end end |
#walk(node, source, patches, statement_position = false) ⇒ void
This method returns an undefined value.
statement_position: the node is a direct child of a statements list.
A modifier rewrite is only sound there — in value position the
modifier captures the surrounding expression: x = if c; v; end
assigns nil when c is false, but x = v if c skips the assignment,
leaving x's previous value. (The compactor produces exactly that
shape from x = c ? v : nil.)
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 25 def walk(node, source, patches, statement_position = false) case node when Prism::IfNode # an IfNode with an end keyword always has its if/elsif keyword location if node.end_keyword_loc && !contains_tail_return_patch?(node) && source.byteslice(node.if_keyword_loc.start_offset, 2) == 'if' # steep:ignore NoMethod if (replacement = try_if(node, source, statement_position)) patches << mk(node, replacement) return end end when Prism::UnlessNode if node.end_keyword_loc && !contains_tail_return_patch?(node) if (replacement = try_unless(node, source, statement_position)) patches << mk(node, replacement) return end end when Prism::WhileNode, Prism::UntilNode if node.closing_loc keyword = node.is_a?(Prism::WhileNode) ? 'while' : 'until' if (replacement = try_loop(node, source, statement_position, keyword)) patches << mk(node, replacement) return end end when Prism::DefNode try_tail_return(node, patches) end child_position = node.is_a?(Prism::StatementsNode) node.compact_child_nodes.each { |child| walk(child, source, patches, child_position) } end |