ryac 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +145 -0
  4. data/Rakefile +45 -0
  5. data/Steepfile +9 -0
  6. data/bin/console +11 -0
  7. data/bin/ryac +134 -0
  8. data/bin/setup +8 -0
  9. data/lib/ryac/analysis/constant/collection.rb +332 -0
  10. data/lib/ryac/analysis/constant/rename_mapping.rb +346 -0
  11. data/lib/ryac/analysis/cvar/collection.rb +37 -0
  12. data/lib/ryac/analysis/gvar/collection.rb +36 -0
  13. data/lib/ryac/analysis/gvar/rename_mapping.rb +82 -0
  14. data/lib/ryac/analysis/ivar/collection.rb +281 -0
  15. data/lib/ryac/analysis/keyword/collection.rb +213 -0
  16. data/lib/ryac/analysis/keyword/rename_mapping.rb +167 -0
  17. data/lib/ryac/analysis/local_scopes.rb +582 -0
  18. data/lib/ryac/analysis/method/collection.rb +298 -0
  19. data/lib/ryac/analysis/method/rename_mapping.rb +337 -0
  20. data/lib/ryac/analysis/method_aliases.rb +83 -0
  21. data/lib/ryac/analysis/method_aliasing.rb +65 -0
  22. data/lib/ryac/analysis/nesting.rb +123 -0
  23. data/lib/ryac/analysis/scope_management.rb +25 -0
  24. data/lib/ryac/analysis/site_bucket_mapping.rb +127 -0
  25. data/lib/ryac/analysis/type_oracle.rb +386 -0
  26. data/lib/ryac/ast_utils.rb +321 -0
  27. data/lib/ryac/errors.rb +32 -0
  28. data/lib/ryac/gem_resolver.rb +55 -0
  29. data/lib/ryac/minifier.rb +169 -0
  30. data/lib/ryac/name_generator.rb +68 -0
  31. data/lib/ryac/pipeline/analyzer.rb +264 -0
  32. data/lib/ryac/pipeline/attr_decl_shorten.rb +61 -0
  33. data/lib/ryac/pipeline/boolean_shorten.rb +26 -0
  34. data/lib/ryac/pipeline/char_shorten.rb +20 -0
  35. data/lib/ryac/pipeline/compactor.rb +1202 -0
  36. data/lib/ryac/pipeline/concatenator.rb +248 -0
  37. data/lib/ryac/pipeline/constant_aliaser.rb +240 -0
  38. data/lib/ryac/pipeline/constant_fold.rb +93 -0
  39. data/lib/ryac/pipeline/control_flow_simplify.rb +352 -0
  40. data/lib/ryac/pipeline/data_types.rb +179 -0
  41. data/lib/ryac/pipeline/endless_method.rb +60 -0
  42. data/lib/ryac/pipeline/errors.rb +94 -0
  43. data/lib/ryac/pipeline/file_collector.rb +360 -0
  44. data/lib/ryac/pipeline/method_renamer.rb +151 -0
  45. data/lib/ryac/pipeline/paren_optimizer.rb +161 -0
  46. data/lib/ryac/pipeline/rename_patcher.rb +274 -0
  47. data/lib/ryac/pipeline/source_patcher.rb +54 -0
  48. data/lib/ryac/pipeline/spelling_shorten.rb +60 -0
  49. data/lib/ryac/pipeline/stage.rb +72 -0
  50. data/lib/ryac/pipeline/stage_runner.rb +84 -0
  51. data/lib/ryac/pipeline/variable_renamer.rb +125 -0
  52. data/lib/ryac/union_find.rb +42 -0
  53. data/lib/ryac/version.rb +5 -0
  54. data/lib/ryac.rb +29 -0
  55. data/rbs_collection.lock.yaml +224 -0
  56. data/rbs_collection.yaml +25 -0
  57. data/sig/ryac/analysis/constant/collection.rbs +30 -0
  58. data/sig/ryac/analysis/constant/rename_mapping.rbs +60 -0
  59. data/sig/ryac/analysis/cvar/collection.rbs +11 -0
  60. data/sig/ryac/analysis/gvar/collection.rbs +9 -0
  61. data/sig/ryac/analysis/gvar/rename_mapping.rbs +23 -0
  62. data/sig/ryac/analysis/ivar/collection.rbs +26 -0
  63. data/sig/ryac/analysis/keyword/collection.rbs +27 -0
  64. data/sig/ryac/analysis/keyword/rename_mapping.rbs +31 -0
  65. data/sig/ryac/analysis/local_scopes.rbs +77 -0
  66. data/sig/ryac/analysis/method/collection.rbs +25 -0
  67. data/sig/ryac/analysis/method/rename_mapping.rbs +82 -0
  68. data/sig/ryac/analysis/method_aliases.rbs +10 -0
  69. data/sig/ryac/analysis/method_aliasing.rbs +9 -0
  70. data/sig/ryac/analysis/nesting.rbs +9 -0
  71. data/sig/ryac/analysis/scope_management.rbs +5 -0
  72. data/sig/ryac/analysis/site_bucket_mapping.rbs +26 -0
  73. data/sig/ryac/analysis/type_oracle.rbs +61 -0
  74. data/sig/ryac/ast_utils.rbs +34 -0
  75. data/sig/ryac/errors.rbs +21 -0
  76. data/sig/ryac/gem_resolver.rbs +28 -0
  77. data/sig/ryac/minifier.rbs +33 -0
  78. data/sig/ryac/name_generator.rbs +14 -0
  79. data/sig/ryac/pipeline/analyzer.rbs +34 -0
  80. data/sig/ryac/pipeline/attr_decl_shorten.rbs +17 -0
  81. data/sig/ryac/pipeline/boolean_shorten.rbs +11 -0
  82. data/sig/ryac/pipeline/char_shorten.rbs +11 -0
  83. data/sig/ryac/pipeline/compactor.rbs +125 -0
  84. data/sig/ryac/pipeline/concatenator.rbs +28 -0
  85. data/sig/ryac/pipeline/constant_aliaser.rbs +35 -0
  86. data/sig/ryac/pipeline/constant_fold.rbs +20 -0
  87. data/sig/ryac/pipeline/control_flow_simplify.rbs +34 -0
  88. data/sig/ryac/pipeline/data_types.rbs +151 -0
  89. data/sig/ryac/pipeline/endless_method.rbs +14 -0
  90. data/sig/ryac/pipeline/errors.rbs +46 -0
  91. data/sig/ryac/pipeline/file_collector.rbs +34 -0
  92. data/sig/ryac/pipeline/method_renamer.rbs +25 -0
  93. data/sig/ryac/pipeline/paren_optimizer.rbs +16 -0
  94. data/sig/ryac/pipeline/rename_patcher.rbs +38 -0
  95. data/sig/ryac/pipeline/source_patcher.rbs +12 -0
  96. data/sig/ryac/pipeline/spelling_shorten.rbs +14 -0
  97. data/sig/ryac/pipeline/stage.rbs +28 -0
  98. data/sig/ryac/pipeline/stage_runner.rbs +18 -0
  99. data/sig/ryac/pipeline/variable_renamer.rbs +21 -0
  100. data/sig/ryac/union_find.rbs +17 -0
  101. data/sig/ryac.rbs +13 -0
  102. data/sig/shims/env.rbs +19 -0
  103. data/sig/shims/typeprof/core/ast.rbs +299 -0
  104. data/sig/shims/typeprof/core/type.rbs +22 -0
  105. data/sig/shims/typeprof/core.rbs +73 -0
  106. data/tools/regen_pins.rb +45 -0
  107. metadata +201 -0
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ryac
4
+ module Pipeline
5
+ # Shared infrastructure for L2-L5 rename pipeline stages.
6
+ # Each stage includes this module and implements its own collect_patches.
7
+ module RenamePatcher
8
+ private
9
+
10
+ def walk_prism(node, &block)
11
+ return unless node
12
+ result = yield node
13
+ return if result == :skip_children
14
+
15
+ children = node.compact_child_nodes
16
+ if node.is_a?(Prism::MatchWriteNode)
17
+ children = children.reject { |c| c.is_a?(Prism::LocalVariableTargetNode) }
18
+ end
19
+ children.each { |child| walk_prism(child, &block) }
20
+ end
21
+
22
+ # Same key the analysis stored the rename under. Kept as its own name
23
+ # because the patchers only ever pass Prism nodes, but it must not grow a
24
+ # second implementation — the two sides sharing one definition is the
25
+ # point.
26
+ def prism_location_key(node)
27
+ AstUtils.location_key(node)
28
+ end
29
+
30
+ def patch_variable(node, patches, rename_map)
31
+ key = prism_location_key(node)
32
+ short = rename_map[key]
33
+ return unless short
34
+
35
+ loc = node.location
36
+ patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
37
+ end
38
+
39
+ def patch_variable_name_only(node, patches, rename_map)
40
+ key = prism_location_key(node)
41
+ short = rename_map[key]
42
+ return unless short
43
+
44
+ name_loc = node.name_loc
45
+ patches << { start: name_loc.start_offset, end: name_loc.end_offset, replacement: short }
46
+ end
47
+
48
+ def patch_def_params(node, patches, analysis, param_names_key)
49
+ return unless node.parameters
50
+
51
+ syntax_key = AstUtils.line_col_key(node)
52
+ data = analysis.syntax_data[syntax_key]
53
+ return unless data
54
+
55
+ # any? rather than empty?: `empty?` on a receiver TypeProf can prove
56
+ # is a Hash gets rewritten to `=={}` — but only when it can prove it,
57
+ # and whether it can differs between the original and the minified
58
+ # text. Self-hosting then never reaches a fixed point. `any?` has no
59
+ # such rewrite.
60
+ param_names = data[param_names_key] || {} #: Hash[Symbol, String]
61
+ return unless param_names.any?
62
+
63
+ params = node.parameters
64
+ patch_required_params(params.requireds, param_names, patches)
65
+ patch_optional_params(params.optionals, param_names, patches)
66
+ patch_rest_param(params.rest, param_names, patches)
67
+ patch_post_params(params.posts, param_names, patches)
68
+ patch_keyword_params(params.keywords, param_names, patches)
69
+ patch_keyword_rest(params.keyword_rest, param_names, patches)
70
+ patch_block_param_node(params.block, param_names, patches)
71
+ end
72
+
73
+ def patch_required_params(requireds, param_names, patches)
74
+ return unless requireds
75
+ requireds.each do |p|
76
+ if p.is_a?(Prism::MultiTargetNode)
77
+ patch_multi_target_params(p, param_names, patches)
78
+ elsif p.is_a?(Prism::RequiredParameterNode)
79
+ short = param_names[p.name]
80
+ next unless short && short != p.name.to_s
81
+ patches << { start: p.location.start_offset, end: p.location.end_offset, replacement: short }
82
+ end
83
+ end
84
+ end
85
+
86
+ def patch_multi_target_params(node, param_names, patches)
87
+ node.lefts.each do |p|
88
+ if p.is_a?(Prism::RequiredParameterNode)
89
+ short = param_names[p.name]
90
+ next unless short && short != p.name.to_s
91
+ patches << { start: p.location.start_offset, end: p.location.end_offset, replacement: short }
92
+ elsif p.is_a?(Prism::MultiTargetNode)
93
+ patch_multi_target_params(p, param_names, patches)
94
+ end
95
+ end
96
+ if node.rest.is_a?(Prism::SplatNode) &&
97
+ node.rest.expression.is_a?(Prism::RequiredParameterNode)
98
+ p = node.rest.expression
99
+ short = param_names[p.name]
100
+ if short && short != p.name.to_s
101
+ patches << { start: p.location.start_offset, end: p.location.end_offset, replacement: short }
102
+ end
103
+ end
104
+ end
105
+
106
+ def patch_optional_params(optionals, param_names, patches)
107
+ return unless optionals
108
+ optionals.each do |p|
109
+ short = param_names[p.name]
110
+ next unless short && short != p.name.to_s
111
+ patches << { start: p.name_loc.start_offset, end: p.name_loc.end_offset, replacement: short }
112
+ end
113
+ end
114
+
115
+ # Shared tail of the rest/**rest/&block patchers: Prism types their
116
+ # name_loc as nullable, but a named parameter always carries one.
117
+ def patch_named_param(name, name_loc, param_names, patches)
118
+ short = param_names[name]
119
+ return unless name_loc && short && short != name.to_s
120
+ patches << { start: name_loc.start_offset, end: name_loc.end_offset, replacement: short }
121
+ end
122
+
123
+ def patch_rest_param(rest, param_names, patches)
124
+ return unless rest.is_a?(Prism::RestParameterNode) && rest.name
125
+ patch_named_param(rest.name, rest.name_loc, param_names, patches)
126
+ end
127
+
128
+ def patch_post_params(posts, param_names, patches)
129
+ return unless posts
130
+ posts.each do |p|
131
+ next unless p.is_a?(Prism::RequiredParameterNode)
132
+ short = param_names[p.name]
133
+ next unless short && short != p.name.to_s
134
+ patches << { start: p.location.start_offset, end: p.location.end_offset, replacement: short }
135
+ end
136
+ end
137
+
138
+ def patch_keyword_params(keywords, param_names, patches)
139
+ return unless keywords
140
+ keywords.each do |p|
141
+ short = param_names[p.name]
142
+ next unless short && short != p.name.to_s
143
+ patches << { start: p.name_loc.start_offset, end: p.name_loc.end_offset, replacement: "#{short}:" }
144
+ end
145
+ end
146
+
147
+ def patch_keyword_rest(keyword_rest, param_names, patches)
148
+ return unless keyword_rest.is_a?(Prism::KeywordRestParameterNode) && keyword_rest.name
149
+ patch_named_param(keyword_rest.name, keyword_rest.name_loc, param_names, patches)
150
+ end
151
+
152
+ def patch_block_param_node(block, param_names, patches)
153
+ return unless block.is_a?(Prism::BlockParameterNode) && block.name
154
+ patch_named_param(block.name, block.name_loc, param_names, patches)
155
+ end
156
+
157
+ def patch_block_params(node, patches, block_param_names_map)
158
+ return unless node.block.is_a?(Prism::BlockNode)
159
+ block = node.block
160
+ return unless block.parameters.is_a?(Prism::BlockParametersNode)
161
+
162
+ key = prism_location_key(node)
163
+ param_map = block_param_names_map[key]
164
+ return unless param_map
165
+
166
+ mangled_values = param_map.values
167
+ block_params = block.parameters.parameters
168
+ has_non_required = block_params && (
169
+ block_params.optionals&.any? ||
170
+ block_params.rest ||
171
+ block_params.posts&.any? ||
172
+ block_params.keywords&.any? ||
173
+ block_params.keyword_rest ||
174
+ block_params.block
175
+ )
176
+ if mangled_values.any? { |v| v.match?(/^_\d+$/) } && !has_non_required
177
+ bp_loc = block.parameters.location
178
+ patches << { start: bp_loc.start_offset, end: bp_loc.end_offset, replacement: '' }
179
+ return
180
+ end
181
+
182
+ block_params = block.parameters.parameters
183
+ return unless block_params
184
+
185
+ patch_required_params(block_params.requireds, param_map, patches)
186
+ patch_optional_params(block_params.optionals, param_map, patches)
187
+ patch_rest_param(block_params.rest, param_map, patches)
188
+ patch_post_params(block_params.posts, param_map, patches)
189
+ patch_keyword_params(block_params.keywords, param_map, patches)
190
+ patch_keyword_rest(block_params.keyword_rest, param_map, patches)
191
+ patch_block_param_node(block_params.block, param_map, patches)
192
+ end
193
+
194
+ def patch_for_node(node, patches, analysis, mangled_key)
195
+ syntax_key = AstUtils.line_col_key(node)
196
+ data = analysis.syntax_data[syntax_key]
197
+ return unless data
198
+
199
+ target_name = data[mangled_key] #: String?
200
+ return unless target_name
201
+
202
+ idx = node.index
203
+ return unless idx.is_a?(Prism::LocalVariableTargetNode)
204
+ patches << { start: idx.location.start_offset, end: idx.location.end_offset, replacement: target_name }
205
+ end
206
+
207
+ def patch_assoc(node, patches, rename_map, keyword_arg: false)
208
+ key_node = node.key
209
+ val_node = node.value
210
+
211
+ return unless key_node.is_a?(Prism::SymbolNode)
212
+ return unless key_node.value.to_s.match?(/\A[a-zA-Z_][a-zA-Z0-9_]*\z/)
213
+
214
+ original_key = key_node.value.to_s
215
+ renamed_key = rename_map[prism_location_key(key_node)]
216
+
217
+ if val_node.is_a?(Prism::ImplicitNode)
218
+ inner = val_node.value
219
+ if inner.is_a?(Prism::LocalVariableReadNode)
220
+ mangled_var = rename_map[prism_location_key(inner)] || inner.name.to_s
221
+ key_name = keyword_arg ? (renamed_key || original_key) : original_key
222
+ if key_name == mangled_var
223
+ if key_name != original_key
224
+ patches << { start: key_node.location.start_offset, end: val_node.location.end_offset,
225
+ replacement: "#{key_name}:" }
226
+ end
227
+ else
228
+ sep = mangled_var.start_with?(':') ? ': ' : ':'
229
+ patches << { start: key_node.location.start_offset, end: val_node.location.end_offset,
230
+ replacement: "#{key_name}#{sep}#{mangled_var}" }
231
+ end
232
+ else
233
+ if keyword_arg && renamed_key
234
+ patches << { start: key_node.location.start_offset, end: val_node.location.end_offset,
235
+ replacement: "#{renamed_key}:" }
236
+ end
237
+ end
238
+ return :skip_children
239
+ end
240
+
241
+ key_name = renamed_key || original_key
242
+
243
+ if val_node.is_a?(Prism::LocalVariableReadNode)
244
+ mangled_var = rename_map[prism_location_key(val_node)] || val_node.name.to_s
245
+ if key_name == mangled_var
246
+ patches << { start: key_node.location.start_offset, end: val_node.location.end_offset,
247
+ replacement: "#{key_name}:" }
248
+ return :skip_children
249
+ end
250
+ end
251
+
252
+ if renamed_key
253
+ loc = key_node.location
254
+ suffix = loc.slice.end_with?(':') ? ':' : ''
255
+ patches << { start: loc.start_offset, end: loc.end_offset, replacement: "#{renamed_key}#{suffix}" }
256
+ end
257
+ end
258
+
259
+ def patch_keyword_hash(node, patches, rename_map, &block)
260
+ node.elements.each do |elem|
261
+ if elem.is_a?(Prism::AssocNode)
262
+ result = patch_assoc(elem, patches, rename_map, keyword_arg: true)
263
+ if result != :skip_children
264
+ elem.compact_child_nodes.each { |child| walk_prism(child, &block) }
265
+ end
266
+ else
267
+ walk_prism(elem, &block)
268
+ end
269
+ end
270
+ end
271
+
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ryac
4
+ module Pipeline
5
+ module SourcePatcher
6
+ private
7
+
8
+ def mk(node, replacement)
9
+ { start: node.location.start_offset, end: node.location.end_offset, replacement: replacement }
10
+ end
11
+
12
+ # The one patch applier: every stage splices byte patches through
13
+ # here. Patches must be disjoint — an overlap means two rewrites
14
+ # fought over the same bytes, and applying either order silently
15
+ # corrupts the output, so it raises with both patches named. The
16
+ # end-position tie-break keeps same-start insertions deterministic.
17
+ def apply_patches(source, patches)
18
+ return source if patches.none?
19
+ sorted = patches.sort_by { |p| [-p[:start], -p[:end]] }
20
+ sorted.each_cons(2) do |after, before|
21
+ # each_cons(2) yields only full pairs; the destructuring alone is
22
+ # what makes the elements look nilable.
23
+ # @type var after: { start: Integer, end: Integer, replacement: String }
24
+ # @type var before: { start: Integer, end: Integer, replacement: String }
25
+ if before[:end] > after[:start]
26
+ raise MinifyError, "overlapping patches: #{before.inspect} / #{after.inspect}"
27
+ end
28
+ end
29
+ result = source.b.dup
30
+ sorted.each do |patch|
31
+ result[patch[:start]...patch[:end]] = patch[:replacement].b
32
+ end
33
+ result.force_encoding(source.encoding)
34
+ end
35
+
36
+ # byteslice is typed String? but node offsets always lie inside the
37
+ # source the node was parsed from, so the result is never nil.
38
+ def src(source, node) # steep:ignore MethodBodyTypeMismatch
39
+ source.byteslice(node.location.start_offset, node.location.length)
40
+ end
41
+
42
+ # The output of every pipeline permutation must at minimum be valid
43
+ # Ruby. Prism also rejects duplicated parameter names, so a rename that
44
+ # collides inside a signature is caught here, at the stage that
45
+ # produced it.
46
+ def verify_parses(text, label)
47
+ return if text.empty?
48
+
49
+ errors = Prism.parse(text).errors
50
+ raise InvalidOutputError.new(label, errors) unless errors.empty?
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ryac
4
+ module Pipeline
5
+ # Same value, shorter spelling: symbol and string array literals collapse
6
+ # to their percent forms when that is strictly smaller, and the English
7
+ # spellings of Ruby's special globals collapse to their punctuation
8
+ # forms — the same storage cell under either name. The pairing lives in
9
+ # GvarRenameMapping::PERL_SPELLINGS, which also shields both spellings
10
+ # from the gvar renamer.
11
+ class SpellingShorten < Stage
12
+ # Words a percent literal can carry verbatim: nothing that needs
13
+ # escaping and nothing that could close or balance the delimiter.
14
+ SAFE_WORD = %r{\A[A-Za-z0-9_/.:=!?-]+\z}
15
+
16
+ def collect(ctx, patches)
17
+ AstUtils.each_node(ctx.ast) do |node|
18
+ case node
19
+ when Prism::ArrayNode
20
+ try_percent_array(node, patches)
21
+ when Prism::GlobalVariableReadNode, Prism::GlobalVariableWriteNode
22
+ short = GvarRenameMapping::PERL_SPELLINGS[node.name]
23
+ if short
24
+ loc = node.is_a?(Prism::GlobalVariableWriteNode) ? node.name_loc : node.location
25
+ patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def try_percent_array(node, patches)
34
+ return unless node.opening == '['
35
+
36
+ elements = node.elements
37
+ return if elements.empty?
38
+
39
+ sigil = if elements.all? { |e| plain?(e, Prism::SymbolNode) }
40
+ '%i'
41
+ elsif elements.all? { |e| plain?(e, Prism::StringNode) }
42
+ '%w'
43
+ end
44
+ return unless sigil
45
+
46
+ # Both node types carry their text as unescaped; SAFE_WORD already
47
+ # guaranteed the text survives a percent literal verbatim.
48
+ words = elements.map { |e| e.unescaped } # steep:ignore NoMethod
49
+ new_size = 4 + words.sum(&:bytesize) + words.size - 1
50
+ return unless new_size < node.location.length
51
+
52
+ patches << mk(node, "#{sigil}[#{words.join(' ')}]")
53
+ end
54
+
55
+ def plain?(node, klass)
56
+ node.is_a?(klass) && SAFE_WORD.match?(node.unescaped) # steep:ignore NoMethod
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stage#call applies patches at load-definition time via SourcePatcher —
4
+ # the module must be concatenated in front of this file.
5
+ require_relative 'source_patcher'
6
+
7
+ module Ryac
8
+ module Pipeline
9
+ # Everything a stage sees while collecting: the current text, its parse,
10
+ # and — inside an analysis batch — the one AnalysisResult. aliases and
11
+ # preamble accumulate finish contributions in list order.
12
+ StageContext = Struct.new(:source, :ast, :analysis, :aliases, :preamble)
13
+
14
+ # The one stage contract.
15
+ #
16
+ # A stage declares what it needs (needs_analysis?), whether it must be
17
+ # re-run to a fixed point (fixpoint?), collects byte patches against the
18
+ # context, and contributes aliases/preamble in finish. Parsing, patch
19
+ # application and iteration belong to the runner, never to the stage.
20
+ class Stage
21
+ include SourcePatcher
22
+
23
+ # true → member of an analysis batch: the runner parses and runs
24
+ # TypeProf once per maximal run of consecutive analysis stages, and
25
+ # every member collects against that single AnalysisResult.
26
+ def needs_analysis? = false
27
+
28
+ # true → the runner re-parses and re-collects until no patches remain.
29
+ def fixpoint? = false
30
+
31
+ def collect(ctx, patches)
32
+ raise NotImplementedError, "#{self.class}#collect must be implemented"
33
+ end
34
+
35
+ # Post-application contribution (aliases/preamble). Stages with
36
+ # nothing to add do not override this — no identity fakes. Only the
37
+ # runner collects the contribution; the standalone driver below
38
+ # returns text alone.
39
+ def finish(ctx) = nil
40
+
41
+ # Driver for a standalone syntactic stage: the per-stage public API.
42
+ def call(input)
43
+ if needs_analysis?
44
+ raise InternalError, "#{self.class} needs analysis; run it through StageRunner"
45
+ end
46
+
47
+ source = input
48
+ loop do
49
+ ctx = StageContext.new(source, Prism.parse(source).value, nil, '', '')
50
+ patches = [] #: Array[patch_entry]
51
+ collect(ctx, patches)
52
+ break if patches.none?
53
+
54
+ new_source = apply_patches(source, patches)
55
+ break if fixpoint? && new_source == source
56
+
57
+ source = new_source
58
+ break unless fixpoint?
59
+ end
60
+ source
61
+ end
62
+
63
+ private
64
+
65
+ # Analysis stages run only inside the runner's batch, which always
66
+ # supplies the AnalysisResult.
67
+ def analysis(ctx)
68
+ ctx.analysis #: AnalysisResult
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ryac
4
+ module Pipeline
5
+ # The one place that executes a stage list.
6
+ #
7
+ # The list is ordered — phase is position, nothing else. Compaction is
8
+ # the fixed first step. A maximal run of consecutive needs_analysis?
9
+ # stages forms the analysis batch: TypeProf runs once for it, every
10
+ # member collects into one patch pool, the pool is applied in a single
11
+ # splice, and each member's finish then contributes aliases/preamble in
12
+ # list order. A list that would need a second batch implies a second
13
+ # TypeProf run — that is a composition error, and it raises before
14
+ # running anything half-way.
15
+ class StageRunner
16
+ include SourcePatcher
17
+
18
+ def initialize(stdlib_requires: [], rbs_files: {})
19
+ @stdlib_requires = stdlib_requires
20
+ @rbs_files = rbs_files
21
+ end
22
+
23
+ def call(code, stage_defs)
24
+ stages = stage_defs.map { |entry| instantiate(entry) }
25
+
26
+ batches = stages.chunk_while { |a, b| a.needs_analysis? == b.needs_analysis? }.to_a
27
+ analysis_batches = batches.count { |chunk| chunk.fetch(0).needs_analysis? }
28
+ if analysis_batches > 1
29
+ raise ArgumentError,
30
+ "stage list needs #{analysis_batches} analysis batches; " \
31
+ "TypeProf runs once, so all analysis stages must be consecutive"
32
+ end
33
+
34
+ result = Compactor.new.call(code)
35
+ # The compacted text is the dialect every stage downstream assumes.
36
+ # If it does not parse, the Compactor is the culprit — name it here
37
+ # instead of letting whichever stage trips first inherit the blame.
38
+ verify_parses(result, 'compacted')
39
+ aliases = ''
40
+ preamble = ''
41
+
42
+ batches.each do |chunk|
43
+ if chunk.fetch(0).needs_analysis?
44
+ result, aliases, preamble = run_analysis_batch(result, chunk)
45
+ else
46
+ chunk.each { |stage| result = stage.call(result) }
47
+ end
48
+ end
49
+
50
+ verify_parses(result, 'code')
51
+ verify_parses(aliases, 'aliases')
52
+ RenameResult.new(code: result, aliases: aliases, preamble: preamble)
53
+ end
54
+
55
+ private
56
+
57
+ def instantiate(entry)
58
+ klass, kwargs = entry
59
+ # A stage entry is data ([class] or [class, kwargs]); configuration
60
+ # becomes constructor state here and nowhere else.
61
+ kwargs ? klass.new(**kwargs) : klass.new
62
+ end
63
+
64
+ def run_analysis_batch(code, batch)
65
+ source = ConcatenatedSource.new(
66
+ content: code,
67
+ stdlib_requires: @stdlib_requires,
68
+ rbs_files: @rbs_files
69
+ )
70
+ analysis = Analyzer.new.call(source)
71
+
72
+ ctx = StageContext.new(code, analysis.prism_ast, analysis, '', '')
73
+ patches = [] #: Array[patch_entry]
74
+ batch.each { |stage| stage.collect(ctx, patches) }
75
+
76
+ result = apply_patches(code, patches)
77
+ ctx.source = result
78
+ batch.each { |stage| stage.finish(ctx) }
79
+
80
+ [result, ctx.aliases, ctx.preamble]
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ryac
4
+ module Pipeline
5
+ # Configurable variable renaming stage.
6
+ # Default (no features): renames locals only.
7
+ # features: { keywords: true } → locals + keywords (Level 3)
8
+ # features: { keywords: true, ivars: true, cvars: true, gvars: true } → all variables (Level 4)
9
+ class VariableRenamer < Stage
10
+ include RenamePatcher
11
+
12
+ FEATURES = { locals: true, keywords: false, ivars: false, cvars: false, gvars: false }.freeze
13
+
14
+ def needs_analysis? = true
15
+
16
+ def initialize(features: {})
17
+ @features = FEATURES.merge(features)
18
+ end
19
+
20
+ def collect(ctx, patches)
21
+ analysis = analysis(ctx)
22
+ rename_map = build_rename_map(analysis)
23
+ block_param_names_map = analysis.block_param_names_map
24
+ collect_patches(ctx.ast, patches, analysis, rename_map, block_param_names_map)
25
+ end
26
+
27
+ private
28
+
29
+ def build_rename_map(analysis)
30
+ map = {} #: Hash[location_key, String]
31
+ map.merge!(analysis.local_rename_entries) if @features[:locals]
32
+ map.merge!(analysis.keyword_rename_entries) if @features[:keywords]
33
+ if @features[:ivars]
34
+ map.merge!(analysis.ivar_rename_entries)
35
+ map.merge!(analysis.attr_ivar_entries) if @features[:attr_ivars]
36
+ end
37
+ map.merge!(analysis.cvar_rename_entries) if @features[:cvars]
38
+ map.merge!(analysis.gvar_rename_entries) if @features[:gvars]
39
+ map
40
+ end
41
+
42
+ def collect_patches(node, patches, analysis, rename_map, block_param_names_map)
43
+ callback = proc { |subnode| handle_node(subnode, patches, analysis, rename_map, block_param_names_map) } #: ^(Prism::Node) -> (Symbol | nil)
44
+ walk_prism(node, &callback)
45
+ end
46
+
47
+ def handle_node(subnode, patches, analysis, rename_map, block_param_names_map)
48
+ case subnode
49
+ # Local variables
50
+ when Prism::LocalVariableReadNode
51
+ patch_variable(subnode, patches, rename_map)
52
+ when Prism::LocalVariableWriteNode
53
+ patch_variable_name_only(subnode, patches, rename_map)
54
+ when Prism::LocalVariableTargetNode
55
+ patch_variable(subnode, patches, rename_map)
56
+ when Prism::LocalVariableOperatorWriteNode,
57
+ Prism::LocalVariableOrWriteNode,
58
+ Prism::LocalVariableAndWriteNode
59
+ patch_variable_name_only(subnode, patches, rename_map)
60
+
61
+ # Instance variables
62
+ when Prism::InstanceVariableReadNode
63
+ patch_variable(subnode, patches, rename_map) if @features[:ivars]
64
+ when Prism::InstanceVariableWriteNode
65
+ patch_variable_name_only(subnode, patches, rename_map) if @features[:ivars]
66
+ when Prism::InstanceVariableOperatorWriteNode,
67
+ Prism::InstanceVariableOrWriteNode,
68
+ Prism::InstanceVariableAndWriteNode
69
+ patch_variable_name_only(subnode, patches, rename_map) if @features[:ivars]
70
+ when Prism::InstanceVariableTargetNode
71
+ patch_variable(subnode, patches, rename_map) if @features[:ivars]
72
+
73
+ # Class variables
74
+ when Prism::ClassVariableReadNode
75
+ patch_variable(subnode, patches, rename_map) if @features[:cvars]
76
+ when Prism::ClassVariableWriteNode
77
+ patch_variable_name_only(subnode, patches, rename_map) if @features[:cvars]
78
+ when Prism::ClassVariableOperatorWriteNode,
79
+ Prism::ClassVariableOrWriteNode,
80
+ Prism::ClassVariableAndWriteNode
81
+ patch_variable_name_only(subnode, patches, rename_map) if @features[:cvars]
82
+ when Prism::ClassVariableTargetNode
83
+ patch_variable(subnode, patches, rename_map) if @features[:cvars]
84
+
85
+ # Global variables
86
+ when Prism::GlobalVariableReadNode
87
+ patch_variable(subnode, patches, rename_map) if @features[:gvars]
88
+ when Prism::GlobalVariableWriteNode
89
+ patch_variable_name_only(subnode, patches, rename_map) if @features[:gvars]
90
+ when Prism::GlobalVariableOperatorWriteNode,
91
+ Prism::GlobalVariableOrWriteNode,
92
+ Prism::GlobalVariableAndWriteNode
93
+ patch_variable_name_only(subnode, patches, rename_map) if @features[:gvars]
94
+ when Prism::GlobalVariableTargetNode
95
+ patch_variable(subnode, patches, rename_map) if @features[:gvars]
96
+
97
+ # Def params
98
+ when Prism::DefNode
99
+ patch_def_params(subnode, patches, analysis, :param_names)
100
+
101
+ # Block params
102
+ when Prism::CallNode
103
+ patch_block_params(subnode, patches, block_param_names_map)
104
+
105
+ # For node — patch index and walk body/collection manually to avoid
106
+ # double-patching the index via LocalVariableTargetNode handler
107
+ when Prism::ForNode
108
+ patch_for_node(subnode, patches, analysis, :for_index_mangled)
109
+ callback = proc { |n| handle_node(n, patches, analysis, rename_map, block_param_names_map) } #: ^(Prism::Node) -> (Symbol | nil)
110
+ walk_prism(subnode.collection, &callback)
111
+ walk_prism(subnode.statements, &callback)
112
+ :skip_children
113
+
114
+ # Hash assoc shorthand
115
+ when Prism::KeywordHashNode
116
+ callback = proc { |n| handle_node(n, patches, analysis, rename_map, block_param_names_map) } #: ^(Prism::Node) -> (Symbol | nil)
117
+ patch_keyword_hash(subnode, patches, rename_map, &callback)
118
+ :skip_children
119
+ when Prism::AssocNode
120
+ patch_assoc(subnode, patches, rename_map)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end