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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +145 -0
- data/Rakefile +45 -0
- data/Steepfile +9 -0
- data/bin/console +11 -0
- data/bin/ryac +134 -0
- data/bin/setup +8 -0
- data/lib/ryac/analysis/constant/collection.rb +332 -0
- data/lib/ryac/analysis/constant/rename_mapping.rb +346 -0
- data/lib/ryac/analysis/cvar/collection.rb +37 -0
- data/lib/ryac/analysis/gvar/collection.rb +36 -0
- data/lib/ryac/analysis/gvar/rename_mapping.rb +82 -0
- data/lib/ryac/analysis/ivar/collection.rb +281 -0
- data/lib/ryac/analysis/keyword/collection.rb +213 -0
- data/lib/ryac/analysis/keyword/rename_mapping.rb +167 -0
- data/lib/ryac/analysis/local_scopes.rb +582 -0
- data/lib/ryac/analysis/method/collection.rb +298 -0
- data/lib/ryac/analysis/method/rename_mapping.rb +337 -0
- data/lib/ryac/analysis/method_aliases.rb +83 -0
- data/lib/ryac/analysis/method_aliasing.rb +65 -0
- data/lib/ryac/analysis/nesting.rb +123 -0
- data/lib/ryac/analysis/scope_management.rb +25 -0
- data/lib/ryac/analysis/site_bucket_mapping.rb +127 -0
- data/lib/ryac/analysis/type_oracle.rb +386 -0
- data/lib/ryac/ast_utils.rb +321 -0
- data/lib/ryac/errors.rb +32 -0
- data/lib/ryac/gem_resolver.rb +55 -0
- data/lib/ryac/minifier.rb +169 -0
- data/lib/ryac/name_generator.rb +68 -0
- data/lib/ryac/pipeline/analyzer.rb +264 -0
- data/lib/ryac/pipeline/attr_decl_shorten.rb +61 -0
- data/lib/ryac/pipeline/boolean_shorten.rb +26 -0
- data/lib/ryac/pipeline/char_shorten.rb +20 -0
- data/lib/ryac/pipeline/compactor.rb +1202 -0
- data/lib/ryac/pipeline/concatenator.rb +248 -0
- data/lib/ryac/pipeline/constant_aliaser.rb +240 -0
- data/lib/ryac/pipeline/constant_fold.rb +93 -0
- data/lib/ryac/pipeline/control_flow_simplify.rb +352 -0
- data/lib/ryac/pipeline/data_types.rb +179 -0
- data/lib/ryac/pipeline/endless_method.rb +60 -0
- data/lib/ryac/pipeline/errors.rb +94 -0
- data/lib/ryac/pipeline/file_collector.rb +360 -0
- data/lib/ryac/pipeline/method_renamer.rb +151 -0
- data/lib/ryac/pipeline/paren_optimizer.rb +161 -0
- data/lib/ryac/pipeline/rename_patcher.rb +274 -0
- data/lib/ryac/pipeline/source_patcher.rb +54 -0
- data/lib/ryac/pipeline/spelling_shorten.rb +60 -0
- data/lib/ryac/pipeline/stage.rb +72 -0
- data/lib/ryac/pipeline/stage_runner.rb +84 -0
- data/lib/ryac/pipeline/variable_renamer.rb +125 -0
- data/lib/ryac/union_find.rb +42 -0
- data/lib/ryac/version.rb +5 -0
- data/lib/ryac.rb +29 -0
- data/rbs_collection.lock.yaml +224 -0
- data/rbs_collection.yaml +25 -0
- data/sig/ryac/analysis/constant/collection.rbs +30 -0
- data/sig/ryac/analysis/constant/rename_mapping.rbs +60 -0
- data/sig/ryac/analysis/cvar/collection.rbs +11 -0
- data/sig/ryac/analysis/gvar/collection.rbs +9 -0
- data/sig/ryac/analysis/gvar/rename_mapping.rbs +23 -0
- data/sig/ryac/analysis/ivar/collection.rbs +26 -0
- data/sig/ryac/analysis/keyword/collection.rbs +27 -0
- data/sig/ryac/analysis/keyword/rename_mapping.rbs +31 -0
- data/sig/ryac/analysis/local_scopes.rbs +77 -0
- data/sig/ryac/analysis/method/collection.rbs +25 -0
- data/sig/ryac/analysis/method/rename_mapping.rbs +82 -0
- data/sig/ryac/analysis/method_aliases.rbs +10 -0
- data/sig/ryac/analysis/method_aliasing.rbs +9 -0
- data/sig/ryac/analysis/nesting.rbs +9 -0
- data/sig/ryac/analysis/scope_management.rbs +5 -0
- data/sig/ryac/analysis/site_bucket_mapping.rbs +26 -0
- data/sig/ryac/analysis/type_oracle.rbs +61 -0
- data/sig/ryac/ast_utils.rbs +34 -0
- data/sig/ryac/errors.rbs +21 -0
- data/sig/ryac/gem_resolver.rbs +28 -0
- data/sig/ryac/minifier.rbs +33 -0
- data/sig/ryac/name_generator.rbs +14 -0
- data/sig/ryac/pipeline/analyzer.rbs +34 -0
- data/sig/ryac/pipeline/attr_decl_shorten.rbs +17 -0
- data/sig/ryac/pipeline/boolean_shorten.rbs +11 -0
- data/sig/ryac/pipeline/char_shorten.rbs +11 -0
- data/sig/ryac/pipeline/compactor.rbs +125 -0
- data/sig/ryac/pipeline/concatenator.rbs +28 -0
- data/sig/ryac/pipeline/constant_aliaser.rbs +35 -0
- data/sig/ryac/pipeline/constant_fold.rbs +20 -0
- data/sig/ryac/pipeline/control_flow_simplify.rbs +34 -0
- data/sig/ryac/pipeline/data_types.rbs +151 -0
- data/sig/ryac/pipeline/endless_method.rbs +14 -0
- data/sig/ryac/pipeline/errors.rbs +46 -0
- data/sig/ryac/pipeline/file_collector.rbs +34 -0
- data/sig/ryac/pipeline/method_renamer.rbs +25 -0
- data/sig/ryac/pipeline/paren_optimizer.rbs +16 -0
- data/sig/ryac/pipeline/rename_patcher.rbs +38 -0
- data/sig/ryac/pipeline/source_patcher.rbs +12 -0
- data/sig/ryac/pipeline/spelling_shorten.rbs +14 -0
- data/sig/ryac/pipeline/stage.rbs +28 -0
- data/sig/ryac/pipeline/stage_runner.rbs +18 -0
- data/sig/ryac/pipeline/variable_renamer.rbs +21 -0
- data/sig/ryac/union_find.rbs +17 -0
- data/sig/ryac.rbs +13 -0
- data/sig/shims/env.rbs +19 -0
- data/sig/shims/typeprof/core/ast.rbs +299 -0
- data/sig/shims/typeprof/core/type.rbs +22 -0
- data/sig/shims/typeprof/core.rbs +73 -0
- data/tools/regen_pins.rb +45 -0
- metadata +201 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tsort'
|
|
4
|
+
|
|
5
|
+
module Ryac
|
|
6
|
+
module Pipeline
|
|
7
|
+
# Stage 2: File Concatenation
|
|
8
|
+
# Performs topological sort and concatenates files in dependency order
|
|
9
|
+
class Concatenator
|
|
10
|
+
# @param graph [DependencyGraph] From Stage 1
|
|
11
|
+
# @return [ConcatenatedSource] Ordered, concatenated source
|
|
12
|
+
# @raise [CircularDependencyError] If cycle detected in graph
|
|
13
|
+
def call(graph)
|
|
14
|
+
sorted_paths = topological_sort(graph)
|
|
15
|
+
concatenate_files(graph, sorted_paths)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# Perform topological sort using Ruby's TSort library
|
|
21
|
+
# @return [Array<String>] Paths in dependency order (dependencies first)
|
|
22
|
+
def topological_sort(graph)
|
|
23
|
+
sorter = GraphSorter.new(graph)
|
|
24
|
+
begin
|
|
25
|
+
sorter.tsort
|
|
26
|
+
rescue TSort::Cyclic
|
|
27
|
+
# Extract cycle from error message
|
|
28
|
+
cycle = extract_cycle_from_graph(graph)
|
|
29
|
+
raise CircularDependencyError.new(cycle)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Extract cycle path from graph for error reporting
|
|
34
|
+
def extract_cycle_from_graph(graph)
|
|
35
|
+
visited = Set.new
|
|
36
|
+
rec_stack = Set.new
|
|
37
|
+
path = [] #: Array[String]
|
|
38
|
+
|
|
39
|
+
graph.paths.each do |start|
|
|
40
|
+
if find_cycle_dfs(graph, start, visited, rec_stack, path)
|
|
41
|
+
return path
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Fallback: return all paths
|
|
46
|
+
graph.paths
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def find_cycle_dfs(graph, node, visited, rec_stack, path)
|
|
50
|
+
return false if visited.include?(node)
|
|
51
|
+
|
|
52
|
+
visited.add(node)
|
|
53
|
+
rec_stack.add(node)
|
|
54
|
+
path << node
|
|
55
|
+
|
|
56
|
+
entry = graph[node]
|
|
57
|
+
deps = entry&.dependencies
|
|
58
|
+
if deps
|
|
59
|
+
deps.each do |dep|
|
|
60
|
+
if rec_stack.include?(dep)
|
|
61
|
+
path << dep
|
|
62
|
+
return true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if find_cycle_dfs(graph, dep, visited, rec_stack, path)
|
|
66
|
+
return true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
rec_stack.delete(node)
|
|
72
|
+
path.pop
|
|
73
|
+
false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Helper class for TSort
|
|
77
|
+
class GraphSorter
|
|
78
|
+
include TSort
|
|
79
|
+
|
|
80
|
+
def initialize(graph)
|
|
81
|
+
@graph = graph
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def tsort_each_node(&block)
|
|
85
|
+
@graph.paths.each(&block)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def tsort_each_child(node, &block)
|
|
89
|
+
entry = @graph[node]
|
|
90
|
+
return unless entry
|
|
91
|
+
|
|
92
|
+
entry.dependencies.each(&block)
|
|
93
|
+
entry.in_class_dependencies.each(&block)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Concatenate files in sorted order
|
|
98
|
+
def concatenate_files(graph, sorted_paths)
|
|
99
|
+
content_parts = [] #: Array[String]
|
|
100
|
+
file_boundaries = [] #: Array[FileBoundary]
|
|
101
|
+
stdlib_requires = [] #: Array[String]
|
|
102
|
+
inlined = Set.new
|
|
103
|
+
current_line = 1
|
|
104
|
+
|
|
105
|
+
# Pre-clean all files: resolve in-class requires by inlining
|
|
106
|
+
cleaned_cache = {} #: Hash[String, String]
|
|
107
|
+
sorted_paths.each do |path|
|
|
108
|
+
entry = graph[path]
|
|
109
|
+
next unless entry
|
|
110
|
+
collect_stdlib_requires(entry, stdlib_requires)
|
|
111
|
+
cleaned_cache[path] = process_require_statements(entry, graph, inlined, cleaned_cache)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
sorted_paths.each do |path|
|
|
115
|
+
next if inlined.include?(path)
|
|
116
|
+
entry = graph[path]
|
|
117
|
+
next unless entry
|
|
118
|
+
|
|
119
|
+
cleaned_content = cleaned_cache[path]
|
|
120
|
+
lines = cleaned_content.count("\n") + 1
|
|
121
|
+
|
|
122
|
+
file_boundaries << FileBoundary.new(
|
|
123
|
+
path: path,
|
|
124
|
+
start_line: current_line,
|
|
125
|
+
end_line: current_line + lines - 1
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
content_parts << cleaned_content
|
|
129
|
+
current_line += lines
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
original_size = graph.files.values.sum { |f| f.content.bytesize }
|
|
133
|
+
|
|
134
|
+
ConcatenatedSource.new(
|
|
135
|
+
content: content_parts.join("\n"),
|
|
136
|
+
file_boundaries: file_boundaries,
|
|
137
|
+
original_size: original_size,
|
|
138
|
+
stdlib_requires: stdlib_requires.uniq,
|
|
139
|
+
rbs_files: graph.rbs_files
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Hoisting a require to the top of the output makes it run at load time.
|
|
144
|
+
# That is fine for one the file already ran at load time, but a require
|
|
145
|
+
# inside a method body runs only when the method is called and is often
|
|
146
|
+
# guarded — optcarrot loads stackprof only under --stackprof-mode, so
|
|
147
|
+
# hoisting it turns an optional dependency into a mandatory one.
|
|
148
|
+
def collect_stdlib_requires(entry, stdlib_requires)
|
|
149
|
+
entry.require_nodes.each do |node|
|
|
150
|
+
next unless node[:type] == :require_stdlib
|
|
151
|
+
next if node[:in_method]
|
|
152
|
+
stdlib_requires << node[:path]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Process require statements: remove top-level requires, inline in-class requires
|
|
157
|
+
def process_require_statements(entry, graph, inlined, cleaned_cache)
|
|
158
|
+
content = entry.content
|
|
159
|
+
require_nodes = entry.require_nodes
|
|
160
|
+
return content if require_nodes.empty?
|
|
161
|
+
|
|
162
|
+
in_class_deps = entry.in_class_dependencies.to_set
|
|
163
|
+
nodes_with_offsets = require_nodes.select { |n| n[:start_offset] }
|
|
164
|
+
|
|
165
|
+
if nodes_with_offsets.size == require_nodes.size
|
|
166
|
+
offset_based_processing(content, nodes_with_offsets, graph, in_class_deps, inlined, cleaned_cache)
|
|
167
|
+
else
|
|
168
|
+
line_based_processing(content, require_nodes)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def offset_based_processing(content, nodes, graph, in_class_deps, inlined, cleaned_cache)
|
|
173
|
+
sorted_nodes = nodes.sort_by { |n| n[:start_offset] }.reverse
|
|
174
|
+
# Prism offsets are byte offsets: splice on bytes, or any multibyte
|
|
175
|
+
# character before a require shifts every slice after it.
|
|
176
|
+
result = content.b
|
|
177
|
+
sorted_nodes.each do |node|
|
|
178
|
+
start_pos = node[:start_offset]
|
|
179
|
+
end_pos = start_pos + node[:length]
|
|
180
|
+
|
|
181
|
+
if node[:in_class] && !node[:in_method] && node[:type] != :require_stdlib
|
|
182
|
+
dep_path = resolve_node_path(node, graph)
|
|
183
|
+
if dep_path && graph[dep_path]
|
|
184
|
+
# the `graph[dep_path]` check above guarantees the entry exists
|
|
185
|
+
dep_content = cleaned_cache[dep_path] || graph[dep_path].content # steep:ignore NoMethod
|
|
186
|
+
stripped = strip_outer_nesting(dep_content)
|
|
187
|
+
# Only consume trailing semicolons (not newlines) for inline
|
|
188
|
+
while end_pos < result.size && result[end_pos] == ';'
|
|
189
|
+
end_pos += 1
|
|
190
|
+
end
|
|
191
|
+
result[start_pos...end_pos] = stripped.b
|
|
192
|
+
inlined.add(dep_path)
|
|
193
|
+
next
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# An in-method stdlib require is not hoisted, so it has to stay where
|
|
198
|
+
# it is — deleting it here would drop the require altogether.
|
|
199
|
+
next if node[:type] == :require_stdlib && node[:in_method]
|
|
200
|
+
|
|
201
|
+
# For removal: consume trailing semicolons and newlines
|
|
202
|
+
while end_pos < result.size && (result[end_pos] == ';' || result[end_pos] == "\n")
|
|
203
|
+
end_pos += 1
|
|
204
|
+
end
|
|
205
|
+
result[start_pos...end_pos] = ''
|
|
206
|
+
end
|
|
207
|
+
result.force_encoding(content.encoding)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def line_based_processing(content, require_nodes)
|
|
211
|
+
lines = content.lines
|
|
212
|
+
lines_to_remove = Set.new
|
|
213
|
+
require_nodes.each { |node| lines_to_remove.add(node[:line] - 1) }
|
|
214
|
+
lines.each_with_index.map do |line, idx|
|
|
215
|
+
lines_to_remove.include?(idx) ? '' : line.chomp
|
|
216
|
+
end.join("\n")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def resolve_node_path(node, graph)
|
|
220
|
+
resolved = node[:resolved_path]
|
|
221
|
+
return nil unless resolved
|
|
222
|
+
graph.files.key?(resolved) ? resolved : nil
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Strip outer module/class nesting from a file so it can be inlined
|
|
226
|
+
# inside the parent's class body. Peels single-child module/class layers
|
|
227
|
+
# until reaching the innermost new scope definition.
|
|
228
|
+
def strip_outer_nesting(content)
|
|
229
|
+
ast = Prism.parse(content).value
|
|
230
|
+
node = ast.statements
|
|
231
|
+
while node.is_a?(Prism::StatementsNode) && node.body.size == 1
|
|
232
|
+
child = node.body.first
|
|
233
|
+
break unless child.is_a?(Prism::ModuleNode) || child.is_a?(Prism::ClassNode)
|
|
234
|
+
inner_body = child.body
|
|
235
|
+
if inner_body.is_a?(Prism::StatementsNode) && inner_body.body.size == 1
|
|
236
|
+
inner_child = inner_body.body.first
|
|
237
|
+
if inner_child.is_a?(Prism::ModuleNode) || inner_child.is_a?(Prism::ClassNode)
|
|
238
|
+
node = inner_body
|
|
239
|
+
next
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
break
|
|
243
|
+
end
|
|
244
|
+
node.slice
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Ryac
|
|
6
|
+
module Pipeline
|
|
7
|
+
# Constant aliasing via source patching.
|
|
8
|
+
class ConstantAliaser < Stage
|
|
9
|
+
include RenamePatcher
|
|
10
|
+
|
|
11
|
+
def needs_analysis? = true
|
|
12
|
+
|
|
13
|
+
def initialize(rename_classes: false)
|
|
14
|
+
@rename_classes = rename_classes
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def collect(ctx, patches)
|
|
18
|
+
analysis = analysis(ctx)
|
|
19
|
+
@class_module_cpath_offsets = Set.new
|
|
20
|
+
# Declared optional on AnalysisResult but always present by the time
|
|
21
|
+
# constant patches are collected.
|
|
22
|
+
@constant_mapping = analysis.constant_mapping #: ConstantRenameMapping
|
|
23
|
+
# Short-name assignment is this stage's policy — it happens exactly
|
|
24
|
+
# once, before any constant patch is emitted, and no other stage
|
|
25
|
+
# reads the mapping.
|
|
26
|
+
if @constant_mapping
|
|
27
|
+
generator = NameGenerator.new([], upcase: true)
|
|
28
|
+
@constant_mapping.assign_short_names(generator, skip_class_modules: !@rename_classes)
|
|
29
|
+
end
|
|
30
|
+
collect_patches(ctx.ast, patches, analysis)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def finish(ctx)
|
|
34
|
+
mapping = @constant_mapping
|
|
35
|
+
return unless mapping
|
|
36
|
+
|
|
37
|
+
prefix_decls = mapping.generate_prefix_declarations
|
|
38
|
+
ctx.preamble = [ctx.preamble, prefix_decls.join(';')].reject(&:empty?).join(';') if prefix_decls.any?
|
|
39
|
+
|
|
40
|
+
alias_decls = mapping.generate_alias_declarations
|
|
41
|
+
ctx.aliases = [ctx.aliases, alias_decls.join(';')].reject(&:empty?).join(';') if alias_decls.any?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def collect_patches(node, patches, analysis)
|
|
47
|
+
walk_prism(node) do |subnode|
|
|
48
|
+
case subnode
|
|
49
|
+
when Prism::ConstantReadNode
|
|
50
|
+
patch_constant_ref(subnode, patches, analysis)
|
|
51
|
+
when Prism::ConstantPathNode
|
|
52
|
+
patch_constant_ref(subnode, patches, analysis)
|
|
53
|
+
next :skip_children
|
|
54
|
+
when Prism::ConstantWriteNode
|
|
55
|
+
patch_constant_write(subnode, patches, analysis)
|
|
56
|
+
when Prism::ConstantTargetNode
|
|
57
|
+
# `A, B = ...` — Prism models each target as a ConstantTargetNode,
|
|
58
|
+
# not a ConstantWriteNode. TypeProf still reports one constant per
|
|
59
|
+
# target at the same location, so without this the references get
|
|
60
|
+
# renamed while the definitions keep their original names.
|
|
61
|
+
patch_constant_write(subnode, patches, analysis)
|
|
62
|
+
when Prism::ConstantPathWriteNode
|
|
63
|
+
patch_constant_path_write(subnode, patches, analysis)
|
|
64
|
+
when Prism::ClassNode
|
|
65
|
+
patch_class_node(subnode, patches, analysis)
|
|
66
|
+
when Prism::ModuleNode
|
|
67
|
+
patch_module_node(subnode, patches, analysis)
|
|
68
|
+
when Prism::DefNode
|
|
69
|
+
patch_def_receiver(subnode.receiver, patches, analysis) if subnode.receiver && !subnode.receiver.is_a?(Prism::SelfNode)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def patch_constant_ref(node, patches, analysis)
|
|
75
|
+
return if @class_module_cpath_offsets.include?(node.location.start_offset)
|
|
76
|
+
|
|
77
|
+
key = prism_location_key(node)
|
|
78
|
+
resolved_cpath = analysis.const_resolution_map[key]
|
|
79
|
+
full_path = analysis.const_full_path_map[key]
|
|
80
|
+
prefix_alias = full_path && @constant_mapping&.short_name_for_prefix(full_path)
|
|
81
|
+
|
|
82
|
+
if resolved_cpath && @constant_mapping&.user_defined_path?(resolved_cpath)
|
|
83
|
+
short = if node.is_a?(Prism::ConstantReadNode)
|
|
84
|
+
# Bare name reference (e.g., CONST) — use only the leaf short name.
|
|
85
|
+
# Expanding to a fully qualified path would break constants defined
|
|
86
|
+
# in `class << self` (metaclass constants are not accessible as Foo::X).
|
|
87
|
+
@constant_mapping.short_name_for_path(resolved_cpath) || node.name.to_s
|
|
88
|
+
else
|
|
89
|
+
get_short_cpath(resolved_cpath)
|
|
90
|
+
end
|
|
91
|
+
loc = node.location
|
|
92
|
+
patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
|
|
93
|
+
elsif node.is_a?(Prism::ConstantPathNode) && resolved_cpath &&
|
|
94
|
+
(short = build_renamed_via_user_prefix(node, analysis))
|
|
95
|
+
loc = node.location
|
|
96
|
+
patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
|
|
97
|
+
elsif prefix_alias
|
|
98
|
+
short = "#{prefix_alias}::#{node.name}"
|
|
99
|
+
loc = node.location
|
|
100
|
+
patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def patch_constant_write(node, patches, analysis)
|
|
105
|
+
key = prism_location_key(node)
|
|
106
|
+
static_cpath = analysis.const_write_cpath_map[key]
|
|
107
|
+
return unless static_cpath
|
|
108
|
+
|
|
109
|
+
short_name = @constant_mapping.short_name_for_path(static_cpath)
|
|
110
|
+
return unless short_name
|
|
111
|
+
|
|
112
|
+
# A ConstantTargetNode is entirely the name; a ConstantWriteNode also
|
|
113
|
+
# spans `= value`, so only its name_loc belongs to us.
|
|
114
|
+
name_loc = node.is_a?(Prism::ConstantTargetNode) ? node.location : node.name_loc
|
|
115
|
+
patches << { start: name_loc.start_offset, end: name_loc.end_offset, replacement: short_name }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def patch_constant_path_write(node, patches, analysis)
|
|
119
|
+
key = prism_location_key(node)
|
|
120
|
+
static_cpath = analysis.const_write_cpath_map[key]
|
|
121
|
+
return unless static_cpath
|
|
122
|
+
|
|
123
|
+
path_str = render_short_cpath(static_cpath)
|
|
124
|
+
target_loc = node.target.location
|
|
125
|
+
patches << { start: target_loc.start_offset, end: target_loc.end_offset, replacement: path_str }
|
|
126
|
+
@class_module_cpath_offsets << target_loc.start_offset
|
|
127
|
+
mark_constant_children(node.target)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def patch_class_node(node, patches, analysis)
|
|
131
|
+
key = prism_location_key(node)
|
|
132
|
+
patch_definition_path(node, patches, analysis.class_cpath_map[key], analysis)
|
|
133
|
+
|
|
134
|
+
if node.superclass
|
|
135
|
+
superclass_path = analysis.superclass_resolution_map[key]
|
|
136
|
+
superclass_path ||= analysis.const_resolution_map[prism_location_key(node.superclass)]
|
|
137
|
+
if superclass_path
|
|
138
|
+
short = get_short_cpath(superclass_path)
|
|
139
|
+
sc_loc = node.superclass.location
|
|
140
|
+
patches << { start: sc_loc.start_offset, end: sc_loc.end_offset, replacement: short }
|
|
141
|
+
@class_module_cpath_offsets << sc_loc.start_offset
|
|
142
|
+
mark_constant_children(node.superclass)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def patch_module_node(node, patches, analysis)
|
|
148
|
+
patch_definition_path(node, patches, analysis.class_cpath_map[prism_location_key(node)], analysis)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# A definition's written path names the class relative to its lexical
|
|
152
|
+
# nesting, and the enclosing modules are renamed right along with it —
|
|
153
|
+
# so the renamed spelling only needs the written segments' short names.
|
|
154
|
+
# Re-qualifying with the full path (`module A; class A::B`) costs bytes
|
|
155
|
+
# and says nothing the nesting doesn't already say: `class B` inside
|
|
156
|
+
# `module A` creates and reopens exactly A::B.
|
|
157
|
+
def patch_definition_path(node, patches, class_cpath, analysis)
|
|
158
|
+
return unless class_cpath
|
|
159
|
+
|
|
160
|
+
# class_cpath exists only when path_segments succeeded on this node,
|
|
161
|
+
# and in the absolute case it equals the segments outright.
|
|
162
|
+
segments, = Nesting.path_segments(node.constant_path) #: [Array[Symbol], bool]
|
|
163
|
+
rendered = render_short_cpath(class_cpath, from: class_cpath.size - segments.size)
|
|
164
|
+
if rendered != node.constant_path.slice
|
|
165
|
+
cpath_loc = node.constant_path.location
|
|
166
|
+
patches << { start: cpath_loc.start_offset, end: cpath_loc.end_offset, replacement: rendered }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
@class_module_cpath_offsets << node.constant_path.location.start_offset
|
|
170
|
+
mark_constant_children(node.constant_path)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Each prefix of the cpath may carry its own short name; render from
|
|
174
|
+
# `from` onward, falling back to the original segment where none was
|
|
175
|
+
# assigned.
|
|
176
|
+
def render_short_cpath(cpath, from: 0)
|
|
177
|
+
(from...cpath.size).map { |i|
|
|
178
|
+
@constant_mapping.short_name_for_path(cpath[0..i]) || cpath[i].to_s
|
|
179
|
+
}.join('::')
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def mark_constant_children(node)
|
|
183
|
+
case node
|
|
184
|
+
when Prism::ConstantPathNode
|
|
185
|
+
@class_module_cpath_offsets << node.location.start_offset
|
|
186
|
+
mark_constant_children(node.parent) if node.parent
|
|
187
|
+
when Prism::ConstantReadNode
|
|
188
|
+
@class_module_cpath_offsets << node.location.start_offset
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def build_renamed_via_user_prefix(node, analysis)
|
|
193
|
+
parent_node = node.parent
|
|
194
|
+
return nil unless parent_node
|
|
195
|
+
|
|
196
|
+
parent_key = prism_location_key(parent_node)
|
|
197
|
+
parent_resolved = analysis.const_resolution_map[parent_key]
|
|
198
|
+
return nil unless parent_resolved
|
|
199
|
+
|
|
200
|
+
if @constant_mapping.user_defined_path?(parent_resolved)
|
|
201
|
+
parent_short = if parent_node.is_a?(Prism::ConstantReadNode)
|
|
202
|
+
@constant_mapping.short_name_for_path(parent_resolved) || parent_node.name.to_s
|
|
203
|
+
else
|
|
204
|
+
get_short_cpath(parent_resolved)
|
|
205
|
+
end
|
|
206
|
+
"#{parent_short}::#{node.name}"
|
|
207
|
+
elsif parent_node.is_a?(Prism::ConstantPathNode)
|
|
208
|
+
parent_renamed = build_renamed_via_user_prefix(parent_node, analysis)
|
|
209
|
+
parent_renamed ? "#{parent_renamed}::#{node.name}" : nil
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def get_short_cpath(cpath)
|
|
214
|
+
if @constant_mapping.user_defined_path?(cpath)
|
|
215
|
+
render_short_cpath(cpath)
|
|
216
|
+
else
|
|
217
|
+
cpath.map(&:to_s).join('::')
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def patch_def_receiver(receiver, patches, analysis)
|
|
222
|
+
return unless @constant_mapping
|
|
223
|
+
|
|
224
|
+
@class_module_cpath_offsets.add(receiver.location.start_offset)
|
|
225
|
+
key = prism_location_key(receiver)
|
|
226
|
+
resolved_cpath = case receiver
|
|
227
|
+
when Prism::ConstantReadNode
|
|
228
|
+
analysis.const_resolution_map[key] || [receiver.name]
|
|
229
|
+
when Prism::ConstantPathNode
|
|
230
|
+
analysis.const_resolution_map[key]
|
|
231
|
+
end
|
|
232
|
+
return unless resolved_cpath && @constant_mapping.user_defined_path?(resolved_cpath)
|
|
233
|
+
|
|
234
|
+
short = get_short_cpath(resolved_cpath)
|
|
235
|
+
loc = receiver.location
|
|
236
|
+
patches << { start: loc.start_offset, end: loc.end_offset, replacement: short }
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
class ConstantFold < Stage
|
|
6
|
+
FOLDABLE_OPS = %i[+ - * / % ** << >> & | ^].freeze
|
|
7
|
+
INTEGER_ONLY_OPS = %i[<< >> & | ^].freeze
|
|
8
|
+
|
|
9
|
+
def collect(ctx, patches)
|
|
10
|
+
walk(ctx.ast, patches)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def walk(node, patches)
|
|
16
|
+
if node.is_a?(Prism::CallNode)
|
|
17
|
+
folded = try_constant_fold(node)
|
|
18
|
+
if folded
|
|
19
|
+
replacement = folded.to_s
|
|
20
|
+
if replacement.bytesize < node.location.length
|
|
21
|
+
patches << mk(node, replacement)
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
node.compact_child_nodes.each { |child| walk(child, patches) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def try_constant_fold(node)
|
|
30
|
+
case node
|
|
31
|
+
when Prism::IntegerNode then node.value
|
|
32
|
+
when Prism::FloatNode then node.value
|
|
33
|
+
when Prism::ParenthesesNode
|
|
34
|
+
body = node.body
|
|
35
|
+
if body.is_a?(Prism::StatementsNode) && body.body.size == 1
|
|
36
|
+
try_constant_fold(body.body.first)
|
|
37
|
+
end
|
|
38
|
+
when Prism::CallNode
|
|
39
|
+
if node.receiver && node.arguments.nil? && node.name == :-@
|
|
40
|
+
inner = try_constant_fold(node.receiver)
|
|
41
|
+
return nil unless inner.is_a?(Numeric)
|
|
42
|
+
-inner
|
|
43
|
+
else
|
|
44
|
+
try_fold_binary_op(node)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def try_fold_binary_op(node)
|
|
50
|
+
return nil unless node.receiver
|
|
51
|
+
return nil unless node.arguments&.arguments&.size == 1
|
|
52
|
+
return nil if node.block
|
|
53
|
+
|
|
54
|
+
op = node.name
|
|
55
|
+
return nil unless FOLDABLE_OPS.include?(op)
|
|
56
|
+
|
|
57
|
+
lhs = try_constant_fold(node.receiver)
|
|
58
|
+
return nil unless lhs.is_a?(Numeric)
|
|
59
|
+
|
|
60
|
+
# arguments proved non-nil above
|
|
61
|
+
rhs = try_constant_fold(node.arguments.arguments.first) # steep:ignore NoMethod
|
|
62
|
+
return nil unless rhs.is_a?(Numeric)
|
|
63
|
+
|
|
64
|
+
return nil if INTEGER_ONLY_OPS.include?(op) && !(lhs.is_a?(Integer) && rhs.is_a?(Integer))
|
|
65
|
+
return nil if (op == :/ || op == :%) && rhs == 0
|
|
66
|
+
|
|
67
|
+
result = apply_op(lhs, op, rhs)
|
|
68
|
+
return nil unless result.is_a?(Integer) || result.is_a?(Float)
|
|
69
|
+
return nil if result.is_a?(Float) && (result.nan? || result.infinite?)
|
|
70
|
+
|
|
71
|
+
result
|
|
72
|
+
rescue StandardError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def apply_op(lhs, op, rhs)
|
|
77
|
+
case op
|
|
78
|
+
when :+ then lhs + rhs
|
|
79
|
+
when :- then lhs - rhs
|
|
80
|
+
when :* then lhs * rhs
|
|
81
|
+
when :/ then lhs / rhs
|
|
82
|
+
when :% then lhs % rhs
|
|
83
|
+
when :** then lhs ** rhs
|
|
84
|
+
when :<< then lhs << rhs
|
|
85
|
+
when :>> then lhs >> rhs
|
|
86
|
+
when :& then lhs & rhs
|
|
87
|
+
when :| then lhs | rhs
|
|
88
|
+
when :^ then lhs ^ rhs
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|