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
data/lib/ryac/errors.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The error hierarchy lives in its own required file, not in ryac.rb:
|
|
4
|
+
# superclass references execute at load, and the concatenated self-host
|
|
5
|
+
# artifact places dependencies first and the root file's own body last —
|
|
6
|
+
# a class defined in the root body can never be a parent for a class
|
|
7
|
+
# defined in a required file.
|
|
8
|
+
module Ryac
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
# The program being minified is at fault — fix the input.
|
|
12
|
+
class InputError < Error; end
|
|
13
|
+
|
|
14
|
+
# ryac itself is at fault — please report it.
|
|
15
|
+
class InternalError < Error; end
|
|
16
|
+
|
|
17
|
+
# The input uses a construct minification cannot handle.
|
|
18
|
+
class MinifyError < InputError; end
|
|
19
|
+
|
|
20
|
+
# The input does not parse. Carries the coordinates of the first error.
|
|
21
|
+
class SyntaxError < InputError
|
|
22
|
+
attr_reader :path, :line, :column
|
|
23
|
+
|
|
24
|
+
def initialize(message, path: nil, line: nil, column: nil)
|
|
25
|
+
@path = path
|
|
26
|
+
@line = line
|
|
27
|
+
@column = column
|
|
28
|
+
message = "#{path}:#{line}:#{column}: #{message}" if path
|
|
29
|
+
super(message)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
class GemResolver
|
|
5
|
+
GemResolution = Data.define(:entry_path, :project_root, :require_paths)
|
|
6
|
+
|
|
7
|
+
# The one spelling of "minify these installed gems": folds each gem's
|
|
8
|
+
# resolution into the Minifier#call keyword arguments, so the CLI and
|
|
9
|
+
# anything replicating it assemble the same invocation.
|
|
10
|
+
def self.minifier_args(gem_names)
|
|
11
|
+
resolver = new
|
|
12
|
+
resolutions = gem_names.map { |name| resolver.call(name) }
|
|
13
|
+
entry = resolutions.map(&:entry_path)
|
|
14
|
+
entry = entry[0] if entry.size == 1
|
|
15
|
+
{
|
|
16
|
+
entry: entry,
|
|
17
|
+
project_root: resolutions.map(&:project_root),
|
|
18
|
+
gem_require_paths: resolutions.flat_map(&:require_paths),
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(gem_name)
|
|
23
|
+
spec = Gem::Specification.find_by_name(gem_name)
|
|
24
|
+
entry_path = find_entry_file(spec, gem_name)
|
|
25
|
+
raise Pipeline::GemNotFoundError.new(gem_name) unless entry_path
|
|
26
|
+
|
|
27
|
+
GemResolution.new(
|
|
28
|
+
entry_path: entry_path,
|
|
29
|
+
project_root: spec.gem_dir,
|
|
30
|
+
require_paths: spec.full_require_paths
|
|
31
|
+
)
|
|
32
|
+
rescue Gem::MissingSpecError
|
|
33
|
+
raise Pipeline::GemNotFoundError.new(gem_name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def find_entry_file(spec, gem_name)
|
|
39
|
+
candidates = [
|
|
40
|
+
gem_name, # json
|
|
41
|
+
gem_name.tr('-', '_'), # concurrent-ruby -> concurrent_ruby
|
|
42
|
+
gem_name.tr('-', '/'), # unicode-display_width -> unicode/display_width
|
|
43
|
+
].uniq
|
|
44
|
+
|
|
45
|
+
spec.full_require_paths.each do |dir|
|
|
46
|
+
candidates.each do |name|
|
|
47
|
+
path = File.join(dir, "#{name}.rb")
|
|
48
|
+
return path if File.exist?(path)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pipeline/stage'
|
|
4
|
+
require_relative 'pipeline/data_types'
|
|
5
|
+
require_relative 'pipeline/errors'
|
|
6
|
+
require_relative 'pipeline/file_collector'
|
|
7
|
+
require_relative 'pipeline/concatenator'
|
|
8
|
+
require_relative 'pipeline/analyzer'
|
|
9
|
+
require_relative 'pipeline/source_patcher'
|
|
10
|
+
require_relative 'pipeline/compactor'
|
|
11
|
+
require_relative 'pipeline/boolean_shorten'
|
|
12
|
+
require_relative 'pipeline/char_shorten'
|
|
13
|
+
require_relative 'pipeline/spelling_shorten'
|
|
14
|
+
require_relative 'pipeline/constant_fold'
|
|
15
|
+
require_relative 'pipeline/control_flow_simplify'
|
|
16
|
+
require_relative 'pipeline/endless_method'
|
|
17
|
+
require_relative 'pipeline/paren_optimizer'
|
|
18
|
+
require_relative 'pipeline/rename_patcher'
|
|
19
|
+
require_relative 'pipeline/stage_runner'
|
|
20
|
+
require_relative 'pipeline/constant_aliaser'
|
|
21
|
+
require_relative 'pipeline/attr_decl_shorten'
|
|
22
|
+
require_relative 'pipeline/variable_renamer'
|
|
23
|
+
require_relative 'pipeline/method_renamer'
|
|
24
|
+
|
|
25
|
+
module Ryac
|
|
26
|
+
class Minifier
|
|
27
|
+
# Stage lists are ordered — phase is list position. The optimizers that
|
|
28
|
+
# run before the rename batch:
|
|
29
|
+
OPTIMIZE_PRE = [
|
|
30
|
+
# ControlFlowSimplify must precede EndlessMethod: a def body has to
|
|
31
|
+
# collapse to a single statement before the def can become endless.
|
|
32
|
+
[Pipeline::ControlFlowSimplify],
|
|
33
|
+
[Pipeline::EndlessMethod],
|
|
34
|
+
[Pipeline::ConstantFold],
|
|
35
|
+
[Pipeline::BooleanShorten],
|
|
36
|
+
[Pipeline::CharShorten],
|
|
37
|
+
[Pipeline::SpellingShorten],
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
# Paren removal reasons about the final spelling of every call, so it
|
|
41
|
+
# runs after all renames.
|
|
42
|
+
OPTIMIZE_POST = [
|
|
43
|
+
[Pipeline::ParenOptimizer],
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
DEFAULT_LEVEL = :stable
|
|
47
|
+
|
|
48
|
+
def self.resolve_level(value)
|
|
49
|
+
level = value.to_sym
|
|
50
|
+
return level if STAGES.key?(level)
|
|
51
|
+
|
|
52
|
+
raise ArgumentError, "Invalid compress level: #{value} (valid: #{STAGES.keys.join(', ')})"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
ALL_VAR_FEATURES = { features: { keywords: true, ivars: true, cvars: true, gvars: true } }.freeze
|
|
56
|
+
ALL_VAR_WITH_ATTR = { features: { keywords: true, ivars: true, cvars: true, gvars: true, attr_ivars: true } }.freeze
|
|
57
|
+
|
|
58
|
+
# :unstable is derived from :stable, never hand-copied — the superset
|
|
59
|
+
# law lives here as code. "attrs renamed" is one fact spelled by three
|
|
60
|
+
# co-moving switches: AttrDeclShorten rename_attrs (declarations),
|
|
61
|
+
# attr_ivars (backing ivars) and MethodRenamer (call sites). This
|
|
62
|
+
# transform flips all three together; a hand-composed list must too.
|
|
63
|
+
def self.derive_unstable(stable)
|
|
64
|
+
stable.flat_map do |entry|
|
|
65
|
+
if entry[0].equal?(Pipeline::AttrDeclShorten)
|
|
66
|
+
[[Pipeline::AttrDeclShorten, { rename_attrs: true }]] #: Array[stage_entry]
|
|
67
|
+
elsif entry[0].equal?(Pipeline::VariableRenamer)
|
|
68
|
+
[[Pipeline::VariableRenamer, ALL_VAR_WITH_ATTR], [Pipeline::MethodRenamer]] #: Array[stage_entry]
|
|
69
|
+
else
|
|
70
|
+
[entry]
|
|
71
|
+
end
|
|
72
|
+
end.freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The two levels, named for their promise rather than a number.
|
|
76
|
+
#
|
|
77
|
+
# :stable is the boundary the optcarrot test certifies frame-for-frame on
|
|
78
|
+
# a real program: everything up to class, constant and variable renaming,
|
|
79
|
+
# which closed-world analysis can keep sound.
|
|
80
|
+
#
|
|
81
|
+
# :unstable adds method renaming, which a program can defeat by
|
|
82
|
+
# construction — names survive inside strings, eval'd source and computed
|
|
83
|
+
# send targets, out of reach of any static analysis. It is certified by
|
|
84
|
+
# self-hosting and works only when the program plays along.
|
|
85
|
+
#
|
|
86
|
+
# Finer configurations are not levels: individual steps stay composable
|
|
87
|
+
# by passing an explicit stage list in place of a level name.
|
|
88
|
+
STABLE_STAGES = [
|
|
89
|
+
*OPTIMIZE_PRE,
|
|
90
|
+
[Pipeline::ConstantAliaser, { rename_classes: true }],
|
|
91
|
+
[Pipeline::AttrDeclShorten],
|
|
92
|
+
[Pipeline::VariableRenamer, ALL_VAR_FEATURES],
|
|
93
|
+
*OPTIMIZE_POST,
|
|
94
|
+
].freeze
|
|
95
|
+
|
|
96
|
+
STAGES = {
|
|
97
|
+
stable: STABLE_STAGES,
|
|
98
|
+
unstable: derive_unstable(STABLE_STAGES),
|
|
99
|
+
}.freeze
|
|
100
|
+
|
|
101
|
+
# code is raw (uncompacted) text — compaction is the runner's fixed
|
|
102
|
+
# first step, so every stage list starts from the same dialect.
|
|
103
|
+
def self.run_stages(code, stages, stdlib_requires: [], rbs_files: {})
|
|
104
|
+
Pipeline::StageRunner.new(stdlib_requires: stdlib_requires, rbs_files: rbs_files)
|
|
105
|
+
.call(code, stages)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
include Pipeline::SourcePatcher
|
|
109
|
+
|
|
110
|
+
attr_reader :result
|
|
111
|
+
|
|
112
|
+
def initialize
|
|
113
|
+
@file_collector = Pipeline::FileCollector.new
|
|
114
|
+
@concatenator = Pipeline::Concatenator.new
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def call(entry_path, level: DEFAULT_LEVEL, project_root: nil, gem_names: [], gem_require_paths: [])
|
|
118
|
+
graph = @file_collector.call(entry_path, project_root: project_root, gem_names: gem_names, gem_require_paths: gem_require_paths)
|
|
119
|
+
source = @concatenator.call(graph)
|
|
120
|
+
# Captured while the boundaries still describe the text; from
|
|
121
|
+
# compaction on they are input provenance, not current positions.
|
|
122
|
+
file_count = source.file_boundaries.size
|
|
123
|
+
|
|
124
|
+
result = run_pipeline(source, level)
|
|
125
|
+
build_result(result, source, file_count)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
# target_level is a preset name, or — for callers composing their own
|
|
131
|
+
# pipeline out of steps — an explicit stage list.
|
|
132
|
+
def run_pipeline(source, target_level)
|
|
133
|
+
stages = target_level.is_a?(Array) ? target_level : STAGES.fetch(self.class.resolve_level(target_level))
|
|
134
|
+
self.class.run_stages(source.content, stages,
|
|
135
|
+
stdlib_requires: source.stdlib_requires,
|
|
136
|
+
rbs_files: source.rbs_files
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build_result(rename_result, source, file_count)
|
|
141
|
+
content = build_output(rename_result.code, source.stdlib_requires, rename_result.preamble)
|
|
142
|
+
# code and aliases were verified at the runner's exit; the assembled
|
|
143
|
+
# content is the one string the runner never sees.
|
|
144
|
+
verify_parses(content, 'content')
|
|
145
|
+
size = content.bytesize
|
|
146
|
+
|
|
147
|
+
stats = Pipeline::CompressionStats.new(
|
|
148
|
+
original_size: source.original_size,
|
|
149
|
+
minified_size: size,
|
|
150
|
+
compression_ratio: size.to_f / source.original_size,
|
|
151
|
+
file_count: file_count
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@result = Pipeline::MinifiedResult.new(
|
|
155
|
+
content: content,
|
|
156
|
+
aliases: rename_result.aliases,
|
|
157
|
+
preamble: rename_result.preamble,
|
|
158
|
+
stats: stats
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def build_output(code, stdlib_requires, preamble = '')
|
|
163
|
+
parts = stdlib_requires.map { |lib| "require \"#{lib}\"" }
|
|
164
|
+
parts << preamble unless preamble.empty?
|
|
165
|
+
parts << code
|
|
166
|
+
parts.join(';')
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
# Generates sequential short names for variable mangling
|
|
5
|
+
# a, b, ..., z, a0, a1, ..., z9, a0a, a0b, ..., z9z, ...
|
|
6
|
+
class NameGenerator
|
|
7
|
+
LETTERS = ('a'..'z').to_a.freeze
|
|
8
|
+
DIGITS = ('0'..'9').to_a.freeze
|
|
9
|
+
|
|
10
|
+
# A one- or two-character name is as short as this generator's own
|
|
11
|
+
# output; the rename families keep such names untouched, so a second
|
|
12
|
+
# minification pass reassigns nothing and the output is a fixed point.
|
|
13
|
+
KEPT_NAME_MAX = 2
|
|
14
|
+
|
|
15
|
+
# additional_excluded holds FINAL names — prefix and case included —
|
|
16
|
+
# which is what every caller has in hand (kept originals, reserved
|
|
17
|
+
# names). Comparing before prefixing would make seeding useless to the
|
|
18
|
+
# prefixed families and force each to re-check by hand.
|
|
19
|
+
def initialize(additional_excluded = [], prefix: "", upcase: false)
|
|
20
|
+
@index = 0
|
|
21
|
+
@prefix = prefix
|
|
22
|
+
@upcase = upcase
|
|
23
|
+
@excluded = Set.new(additional_excluded)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def next_name
|
|
27
|
+
loop do
|
|
28
|
+
name = index_to_name(@index)
|
|
29
|
+
@index += 1
|
|
30
|
+
name = name.upcase if @upcase
|
|
31
|
+
final = "#{@prefix}#{name}"
|
|
32
|
+
next if @excluded.include?(final)
|
|
33
|
+
return final
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Convert index to name using letter-digit alternating scheme:
|
|
40
|
+
# a-z, a0-z9, a0a-z9z, a0a0-z9z9, ...
|
|
41
|
+
def index_to_name(index)
|
|
42
|
+
return LETTERS[index] if index < 26
|
|
43
|
+
|
|
44
|
+
index -= 26
|
|
45
|
+
length = 2
|
|
46
|
+
capacity = 26 * 10
|
|
47
|
+
|
|
48
|
+
while index >= capacity
|
|
49
|
+
index -= capacity
|
|
50
|
+
length += 1
|
|
51
|
+
capacity *= (length.odd? ? 26 : 10)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
result = ""
|
|
55
|
+
(length - 1).downto(0) do |pos|
|
|
56
|
+
if pos.even?
|
|
57
|
+
result = LETTERS[index % 26] + result
|
|
58
|
+
index /= 26
|
|
59
|
+
else
|
|
60
|
+
result = DIGITS[index % 10] + result
|
|
61
|
+
index /= 10
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
result
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
|
|
5
|
+
module Ryac
|
|
6
|
+
module Pipeline
|
|
7
|
+
# Stage 3: Analysis
|
|
8
|
+
# Parses source with TypeProf, builds scope mappings,
|
|
9
|
+
# collects constants and their references, and freezes mappings.
|
|
10
|
+
class Analyzer
|
|
11
|
+
include AnalysisPhases
|
|
12
|
+
|
|
13
|
+
def self.prism_only(source)
|
|
14
|
+
prism_only_from_string(source.content, source: source)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.prism_only_from_string(content, source: nil)
|
|
18
|
+
prism_result = Prism.parse(content)
|
|
19
|
+
|
|
20
|
+
analyzer = new
|
|
21
|
+
syntax_data = analyzer.syntax_data_for(prism_result.value)
|
|
22
|
+
|
|
23
|
+
source ||= ConcatenatedSource.new(content: content)
|
|
24
|
+
|
|
25
|
+
AnalysisResult.new(
|
|
26
|
+
prism_ast: prism_result.value,
|
|
27
|
+
scope_mappings: {},
|
|
28
|
+
constant_mapping: nil,
|
|
29
|
+
rename_map: {},
|
|
30
|
+
method_alias_map: {},
|
|
31
|
+
method_transform_map: {},
|
|
32
|
+
source: source,
|
|
33
|
+
attr_rename_map: {},
|
|
34
|
+
block_param_names_map: {},
|
|
35
|
+
syntax_data: syntax_data,
|
|
36
|
+
const_resolution_map: {},
|
|
37
|
+
const_full_path_map: {},
|
|
38
|
+
const_write_cpath_map: {},
|
|
39
|
+
class_cpath_map: {},
|
|
40
|
+
superclass_resolution_map: {},
|
|
41
|
+
meta_node_map: {}
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def call(source)
|
|
46
|
+
prism_result, @oracle = without_stdout_pollution { setup_typeprof(source) }
|
|
47
|
+
@prism_root = prism_result.value
|
|
48
|
+
@boot_constant_roots = BootConstants.for(source.stdlib_requires)
|
|
49
|
+
@syntax_data = collect_syntax_data(@prism_root)
|
|
50
|
+
|
|
51
|
+
analyze_keywords_and_scopes
|
|
52
|
+
analyze_methods_phase
|
|
53
|
+
method_alias_map, method_transform_map = resolve_method_aliases_and_transforms(@prism_root)
|
|
54
|
+
analyze_variables_phase
|
|
55
|
+
|
|
56
|
+
rename_map = @method_rename_mapping.node_mapping.dup
|
|
57
|
+
attr_ivar_entries = {} #: Hash[location_key, String]
|
|
58
|
+
attr_rename_map = coordinate_attr_renames(@prism_root, rename_map, attr_ivar_entries)
|
|
59
|
+
|
|
60
|
+
analyze_constants_phase
|
|
61
|
+
local_rename_entries = precompute_rename_entries
|
|
62
|
+
precompute_constant_resolution
|
|
63
|
+
precompute_meta_nodes
|
|
64
|
+
|
|
65
|
+
build_analysis_result(
|
|
66
|
+
prism_result, source, rename_map, method_alias_map, method_transform_map,
|
|
67
|
+
attr_rename_map, attr_ivar_entries, local_rename_entries
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def syntax_data_for(prism_ast)
|
|
72
|
+
collect_syntax_data(prism_ast)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# TypeProf prints diagnostics for node types it does not recognize
|
|
78
|
+
# straight to stdout, which is also where the minified program is
|
|
79
|
+
# written. Divert anything it emits to stderr so it can never end up
|
|
80
|
+
# inside the output.
|
|
81
|
+
def without_stdout_pollution
|
|
82
|
+
original = $stdout
|
|
83
|
+
capture = StringIO.new
|
|
84
|
+
$stdout = capture
|
|
85
|
+
begin
|
|
86
|
+
yield
|
|
87
|
+
ensure
|
|
88
|
+
$stdout = original
|
|
89
|
+
noise = capture.string
|
|
90
|
+
$stderr.print(noise) unless noise.empty?
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def setup_typeprof(source)
|
|
95
|
+
prism_result = Prism.parse(source.content)
|
|
96
|
+
# Every input file was parse-checked at collection, so a failure
|
|
97
|
+
# here means an upstream stage broke the text.
|
|
98
|
+
unless prism_result.errors.none?
|
|
99
|
+
raise InvalidOutputError.new('pre-rename source', prism_result.errors)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
[prism_result, TypeOracle.boot(source.content, source.rbs_files)]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def analyze_keywords_and_scopes
|
|
106
|
+
@local_scopes = LocalScopes.new(@prism_root)
|
|
107
|
+
|
|
108
|
+
@keyword_rename_mapping = KeywordRenameMapping.new
|
|
109
|
+
collect_keyword_info(@prism_root)
|
|
110
|
+
@keyword_rename_mapping.assign_short_names
|
|
111
|
+
|
|
112
|
+
# Inline rather than through locals: a local passed as the same-named
|
|
113
|
+
# keyword gets hint-aligned and collapses to shorthand, and the
|
|
114
|
+
# re-minified form of that shorthand no longer carries the hint — the
|
|
115
|
+
# names then oscillate between passes instead of reaching a fixed
|
|
116
|
+
# point under self-hosting.
|
|
117
|
+
@local_scopes.allocate(
|
|
118
|
+
kw_def_map: @keyword_rename_mapping.def_node_mapping(@keyword_def_node_registry || {}),
|
|
119
|
+
var_hints: @keyword_rename_mapping.build_variable_hints { |val_node| @local_scopes.scope_id_of(val_node) }
|
|
120
|
+
)
|
|
121
|
+
@local_scopes.resolve
|
|
122
|
+
@scope_mappings = @local_scopes.scope_mappings
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def analyze_methods_phase
|
|
126
|
+
@method_rename_mapping = MethodRenameMapping.new
|
|
127
|
+
collect_method_definitions(@prism_root)
|
|
128
|
+
resolve_method_calls
|
|
129
|
+
collect_alias_undef_methods(@prism_root)
|
|
130
|
+
scan_dynamic_method_references(@prism_root)
|
|
131
|
+
collect_visibility_modifier_methods(@prism_root)
|
|
132
|
+
collect_attr_write_exclusions(@prism_root)
|
|
133
|
+
collect_shorthand_pun_methods(@prism_root)
|
|
134
|
+
@method_rename_mapping.assign_short_names(@scope_mappings, @oracle)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def analyze_variables_phase
|
|
138
|
+
@ivar_rename_mapping = SiteBucketMapping.new(prefix: '@')
|
|
139
|
+
attr_backed = collect_attr_backed_ivars(@prism_root)
|
|
140
|
+
collect_ivar_definitions(@prism_root, attr_backed)
|
|
141
|
+
scan_dynamic_ivar_access(@prism_root)
|
|
142
|
+
merge_inherited_ivars
|
|
143
|
+
reserve_attr_ivar_names(@prism_root)
|
|
144
|
+
@ivar_rename_mapping.assign_short_names
|
|
145
|
+
|
|
146
|
+
@cvar_rename_mapping = SiteBucketMapping.new(prefix: '@@')
|
|
147
|
+
collect_cvar_definitions(@prism_root)
|
|
148
|
+
scan_dynamic_cvar_access(@prism_root)
|
|
149
|
+
merge_inherited_cvars
|
|
150
|
+
@cvar_rename_mapping.assign_short_names
|
|
151
|
+
|
|
152
|
+
@gvar_rename_mapping = GvarRenameMapping.new
|
|
153
|
+
collect_gvar_definitions(@prism_root)
|
|
154
|
+
scan_alias_globals(@prism_root)
|
|
155
|
+
@gvar_rename_mapping.assign_short_names
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def analyze_constants_phase
|
|
159
|
+
@constant_mapping = ConstantRenameMapping.new(boot_roots: @boot_constant_roots)
|
|
160
|
+
collect_constants(@prism_root)
|
|
161
|
+
exclude_private_constants(@prism_root)
|
|
162
|
+
count_constant_references(@prism_root)
|
|
163
|
+
augment_constant_counts_via_oracle
|
|
164
|
+
collect_external_references(@prism_root)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def precompute_rename_entries
|
|
168
|
+
@local_scopes.def_param_names.each do |key, names|
|
|
169
|
+
(@syntax_data[key] ||= {})[:param_names] = names
|
|
170
|
+
end
|
|
171
|
+
@local_scopes.for_index_names.each do |key, name|
|
|
172
|
+
data = @syntax_data[key]
|
|
173
|
+
data[:for_index_mangled] = name if data&.[](:index_name)
|
|
174
|
+
end
|
|
175
|
+
@block_param_names_map = @local_scopes.block_param_names
|
|
176
|
+
@local_scopes.rename_entries
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def build_analysis_result(prism_result, source, rename_map, method_alias_map,
|
|
180
|
+
method_transform_map, attr_rename_map, attr_ivar_entries,
|
|
181
|
+
local_rename_entries)
|
|
182
|
+
AnalysisResult.new(
|
|
183
|
+
prism_ast: prism_result.value,
|
|
184
|
+
scope_mappings: @scope_mappings,
|
|
185
|
+
constant_mapping: @constant_mapping,
|
|
186
|
+
rename_map: rename_map,
|
|
187
|
+
method_alias_map: method_alias_map,
|
|
188
|
+
method_transform_map: method_transform_map,
|
|
189
|
+
source: source,
|
|
190
|
+
attr_rename_map: attr_rename_map,
|
|
191
|
+
block_param_names_map: @block_param_names_map,
|
|
192
|
+
syntax_data: @syntax_data,
|
|
193
|
+
const_resolution_map: @const_resolution_map,
|
|
194
|
+
const_full_path_map: @const_full_path_map,
|
|
195
|
+
const_write_cpath_map: @const_write_cpath_map,
|
|
196
|
+
class_cpath_map: @class_cpath_map,
|
|
197
|
+
superclass_resolution_map: @superclass_resolution_map,
|
|
198
|
+
meta_node_map: @meta_node_map,
|
|
199
|
+
local_rename_entries: local_rename_entries,
|
|
200
|
+
keyword_rename_entries: @keyword_rename_mapping.node_mapping,
|
|
201
|
+
ivar_rename_entries: @ivar_rename_mapping.node_mapping,
|
|
202
|
+
attr_ivar_entries: attr_ivar_entries,
|
|
203
|
+
cvar_rename_entries: @cvar_rename_mapping.node_mapping,
|
|
204
|
+
gvar_rename_entries: @gvar_rename_mapping.node_mapping
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
META_CALL_METHODS = %i[include attr_reader attr_accessor].freeze
|
|
209
|
+
|
|
210
|
+
# Calls the renamer rewrites structurally instead of as plain calls.
|
|
211
|
+
def precompute_meta_nodes
|
|
212
|
+
@meta_node_map = {}
|
|
213
|
+
Nesting.each_meta_call(@prism_root, META_CALL_METHODS) do |call_node, _cpath, _singleton|
|
|
214
|
+
case call_node.name
|
|
215
|
+
when :attr_reader, :attr_accessor
|
|
216
|
+
next unless call_node.arguments
|
|
217
|
+
|
|
218
|
+
@meta_node_map[AstUtils.location_key(call_node)] =
|
|
219
|
+
{ type: call_node.name, args: AstUtils.symbol_arguments(call_node) }
|
|
220
|
+
when :include
|
|
221
|
+
@meta_node_map[AstUtils.location_key(call_node)] = { type: :include }
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def collect_syntax_data(prism_ast)
|
|
227
|
+
data = {} #: syntax_data_map
|
|
228
|
+
traverse_prism(prism_ast, data)
|
|
229
|
+
data
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def traverse_prism(node, data)
|
|
233
|
+
key = AstUtils.line_col_key(node)
|
|
234
|
+
case node
|
|
235
|
+
when Prism::DefNode
|
|
236
|
+
data[key] = { self_receiver: node.receiver.is_a?(Prism::SelfNode) }
|
|
237
|
+
when Prism::ArrayNode
|
|
238
|
+
data[key] = { opening: node.opening }
|
|
239
|
+
when Prism::RangeNode
|
|
240
|
+
data[key] = { exclude_end: node.exclude_end? }
|
|
241
|
+
when Prism::RegularExpressionNode
|
|
242
|
+
data[key] = { content: node.content, flags: node.closing.delete("/") }
|
|
243
|
+
when Prism::InterpolatedRegularExpressionNode
|
|
244
|
+
data[key] = { flags: node.closing.delete("/") }
|
|
245
|
+
when Prism::ForNode
|
|
246
|
+
idx = node.index
|
|
247
|
+
if idx.is_a?(Prism::LocalVariableTargetNode)
|
|
248
|
+
data[key] = { index_name: idx.name }
|
|
249
|
+
else
|
|
250
|
+
data[key] = { index_slice: idx.slice }
|
|
251
|
+
end
|
|
252
|
+
when Prism::DefinedNode
|
|
253
|
+
data[key] = { value_slice: node.value.slice }
|
|
254
|
+
when Prism::RationalNode, Prism::ImaginaryNode, Prism::LambdaNode,
|
|
255
|
+
Prism::MatchLastLineNode, Prism::InterpolatedMatchLastLineNode,
|
|
256
|
+
Prism::FlipFlopNode, Prism::AliasGlobalVariableNode,
|
|
257
|
+
Prism::BackReferenceReadNode
|
|
258
|
+
data[key] = { slice: node.slice }
|
|
259
|
+
end
|
|
260
|
+
node.compact_child_nodes.each { |child| traverse_prism(child, data) }
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
# `attr_reader :a` → `attr :a` (the same method, four bytes shorter) and
|
|
6
|
+
# single-name attr_accessor → the two-argument attr form, for declarations
|
|
7
|
+
# type analysis recognized as meta calls. This stage owns the attr rewrite
|
|
8
|
+
# at every level: with rename_attrs the declared symbols are additionally
|
|
9
|
+
# mapped through the analysis' attr rename map, keeping declarations in
|
|
10
|
+
# sync with call sites the method renamer renames.
|
|
11
|
+
class AttrDeclShorten < Stage
|
|
12
|
+
include RenamePatcher
|
|
13
|
+
|
|
14
|
+
def needs_analysis? = true
|
|
15
|
+
|
|
16
|
+
def initialize(rename_attrs: false)
|
|
17
|
+
@rename_attrs = rename_attrs
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def collect(ctx, patches)
|
|
21
|
+
analysis = analysis(ctx)
|
|
22
|
+
attr_rename_map = analysis.attr_rename_map if @rename_attrs
|
|
23
|
+
walk_prism(ctx.ast) do |subnode|
|
|
24
|
+
next unless subnode.is_a?(Prism::CallNode)
|
|
25
|
+
|
|
26
|
+
key = prism_location_key(subnode)
|
|
27
|
+
meta = analysis.meta_node_map[key]
|
|
28
|
+
next unless meta
|
|
29
|
+
|
|
30
|
+
args = meta[:args] || []
|
|
31
|
+
next unless args.any?
|
|
32
|
+
|
|
33
|
+
names = args.map { |sym| attr_rename_map&.dig(key, sym) || sym.to_s }
|
|
34
|
+
replacement = render_attr_declaration(meta[:type], names)
|
|
35
|
+
next unless replacement && replacement != subnode.slice
|
|
36
|
+
|
|
37
|
+
loc = subnode.location
|
|
38
|
+
patches << { start: loc.start_offset, end: loc.end_offset, replacement: replacement }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# The compact spelling of an attr declaration: attr_reader collapses to
|
|
45
|
+
# attr, a single-name attr_accessor takes the two-argument attr form, a
|
|
46
|
+
# multi-name accessor has no shorter spelling.
|
|
47
|
+
def render_attr_declaration(type, names)
|
|
48
|
+
case type
|
|
49
|
+
when :attr_reader
|
|
50
|
+
"attr #{names.map { |n| ":#{n}" }.join(',')}"
|
|
51
|
+
when :attr_accessor
|
|
52
|
+
if names.size == 1
|
|
53
|
+
"attr :#{names[0]},!!1"
|
|
54
|
+
else
|
|
55
|
+
"attr_accessor #{names.map { |n| ":#{n}" }.join(',')}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
class BooleanShorten < Stage
|
|
6
|
+
def collect(ctx, patches)
|
|
7
|
+
walk(ctx.ast, patches)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def walk(node, patches, inside_block_params: false)
|
|
13
|
+
case node
|
|
14
|
+
when Prism::BlockParametersNode
|
|
15
|
+
node.compact_child_nodes.each { |child| walk(child, patches, inside_block_params: true) }
|
|
16
|
+
return
|
|
17
|
+
when Prism::TrueNode
|
|
18
|
+
patches << mk(node, '!!1') unless inside_block_params
|
|
19
|
+
when Prism::FalseNode
|
|
20
|
+
patches << mk(node, '!1') unless inside_block_params
|
|
21
|
+
end
|
|
22
|
+
node.compact_child_nodes.each { |child| walk(child, patches, inside_block_params: inside_block_params) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module Pipeline
|
|
5
|
+
class CharShorten < Stage
|
|
6
|
+
def collect(ctx, patches)
|
|
7
|
+
walk(ctx.ast, patches)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def walk(node, patches)
|
|
13
|
+
if node.is_a?(Prism::StringNode) && node.opening_loc && node.content.match?(/\A[a-zA-Z0-9_]\z/)
|
|
14
|
+
patches << mk(node, "?#{node.content}")
|
|
15
|
+
end
|
|
16
|
+
node.compact_child_nodes.each { |child| walk(child, patches) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|