Class: Ryac::Pipeline::ControlFlowSimplify

Inherits:
Stage
  • Object
show all
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.

Returns:

  • (Array[Integer])
[','.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.

Returns:

  • (Array[Class])
[
  Prism::LocalVariableWriteNode,
  Prism::LocalVariableOrWriteNode,
  Prism::LocalVariableAndWriteNode,
  Prism::LocalVariableOperatorWriteNode,
  Prism::LocalVariableTargetNode
].freeze
OPERATOR_START_BYTES =

Check if end keyword is followed by an operator (e.g., if...end*(expr)) In that case, ternary conversion needs parentheses.

Returns:

  • (Array[Integer])
[
  '*'.ord, '/'.ord, '%'.ord, '+'.ord, '-'.ord, '&'.ord,
  '|'.ord, '^'.ord, '<'.ord, '>'.ord, '.'.ord, '['.ord
].freeze

Instance Method Summary collapse

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

Parameters:

  • cond_node (Prism::Node)
  • cond (String)
  • then_node (Prism::Node, nil)
  • then_expr (String, nil)
  • else_expr (String, nil)

Returns:

  • (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.

Parameters:



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]

Parameters:

  • node (Prism::Node)

Returns:

  • (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]

Parameters:

  • node (Prism::Node, nil)

Returns:

  • (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.

Parameters:

  • node (Prism::Node, nil)
  • patches (Array[patch_entry])


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

Parameters:

  • predicate (Prism::Node)
  • body (Prism::Node, nil)

Returns:

  • (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.

Parameters:

  • node (Prism::Node)

Returns:

  • (Boolean)


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?

Parameters:

  • subsequent (Prism::Node, nil)
  • source (String)

Returns:

  • (String, nil)


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.

Returns:

  • (Boolean)


10
# File 'lib/ryac/pipeline/control_flow_simplify.rb', line 10

def fixpoint? = true

#if_end_followed_by_operator?(node, source) ⇒ Boolean

Parameters:

  • node (Prism::IfNode)
  • source (String)

Returns:

  • (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

Parameters:

  • node (Prism::Node)
  • source (String)

Returns:

  • (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.

Parameters:

  • pred (Prism::Node)
  • text (String)

Returns:

  • (String)


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

Parameters:

  • text (String, nil)

Returns:

  • (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.

Parameters:

  • node (Prism::Node, nil)

Yields:

  • (node)

Yield Parameters:

  • arg0 (Prism::Node)

Yield Returns:

  • (void)


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?

Parameters:

  • node (Prism::IfNode)
  • source (String)
  • statement_position (Boolean)

Returns:

  • (String, nil)


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?

Parameters:

  • node (Prism::WhileNode, Prism::UntilNode)
  • source (String)
  • statement_position (Boolean)
  • keyword (String)

Returns:

  • (String, nil)


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.

Parameters:

  • node (Prism::IfNode)
  • inner (Prism::Node)
  • source (String)

Returns:

  • (String, nil)


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.

Parameters:

  • node (Prism::DefNode)
  • patches (Array[patch_entry])


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?

Parameters:

  • node (Prism::UnlessNode)
  • source (String)
  • statement_position (Boolean)

Returns:

  • (String, nil)


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.)

Parameters:

  • node (Prism::Node)
  • source (String)
  • patches (Array[patch_entry])
  • statement_position (Boolean) (defaults to: false)


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