Class: Ryac::Minifier

Inherits:
Object
  • Object
show all
Includes:
Pipeline::SourcePatcher
Defined in:
lib/ryac/minifier.rb,
sig/ryac/minifier.rbs

Constant Summary collapse

OPTIMIZE_PRE =

Stage lists are ordered — phase is list position. The optimizers that run before the rename batch:

Returns:

  • (Array[stage_entry])
[
  # ControlFlowSimplify must precede EndlessMethod: a def body has to
  # collapse to a single statement before the def can become endless.
  [Pipeline::ControlFlowSimplify],
  [Pipeline::EndlessMethod],
  [Pipeline::ConstantFold],
  [Pipeline::BooleanShorten],
  [Pipeline::CharShorten],
  [Pipeline::SpellingShorten],
].freeze
OPTIMIZE_POST =

Paren removal reasons about the final spelling of every call, so it runs after all renames.

Returns:

  • (Array[stage_entry])
[
  [Pipeline::ParenOptimizer],
].freeze
DEFAULT_LEVEL =

Returns:

  • (Symbol)
:stable
ALL_VAR_WITH_ATTR =

Returns:

  • (Hash[Symbol, Hash[Symbol, bool]])
{ features: { keywords: true, ivars: true, cvars: true, gvars: true, attr_ivars: true } }.freeze
STABLE_STAGES =

The two levels, named for their promise rather than a number.

:stable is the boundary the optcarrot test certifies frame-for-frame on a real program: class, constant and variable renaming, plus method renaming under the :safe policy — a group renames only when type inference resolved every caller and no dynamic escape hatch (a string mention, a dynamic-ivar class, an uncalled def) touches its name. "attrs renamed" is one fact spelled by three co-moving switches: AttrDeclShorten rename_attrs (declarations), attr_ivars (backing ivars) and MethodRenamer (call sites) — all three live here together.

:unstable switches method renaming to :aggressive, which a program can defeat by construction — names survive inside strings, eval'd source and computed send targets, out of reach of any static analysis. It is certified by self-hosting and works only when the program plays along.

Finer configurations are not levels: individual steps stay composable by passing an explicit stage list in place of a level name.

Returns:

  • (Array[stage_entry])
[
  *OPTIMIZE_PRE,
  [Pipeline::ConstantAliaser, { rename_classes: true }],
  [Pipeline::AttrDeclShorten, { rename_attrs: true }],
  [Pipeline::VariableRenamer, ALL_VAR_WITH_ATTR],
  [Pipeline::MethodRenamer, { policy: :safe }],
  *OPTIMIZE_POST,
].freeze
STAGES =

Returns:

  • (Hash[Symbol, Array[stage_entry]])
{
  stable: STABLE_STAGES,
  unstable: derive_unstable(STABLE_STAGES),
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Pipeline::SourcePatcher

#apply_patches, #mk, #src, #verify_parses

Constructor Details

#initialize ⇒ Minifier

Returns a new instance of Minifier.



112
113
114
115
# File 'lib/ryac/minifier.rb', line 112

def initialize
  @file_collector = Pipeline::FileCollector.new
  @concatenator = Pipeline::Concatenator.new
end

Instance Attribute Details

#result ⇒ Pipeline::MinifiedResult? (readonly)

Returns the value of attribute result.

Returns:



110
111
112
# File 'lib/ryac/minifier.rb', line 110

def result
  @result
end

Class Method Details

.derive_unstable(stable) ⇒ Array[stage_entry]

:unstable is derived from :stable, never hand-copied — the superset law lives here as code. The one switch between them is the method renamer's policy: :safe touches only names whose every caller type inference resolved; :aggressive also takes the bets.

Parameters:

  • stable (Array[stage_entry])

Returns:

  • (Array[stage_entry])


61
62
63
64
65
66
# File 'lib/ryac/minifier.rb', line 61

def self.derive_unstable(stable)
  stable.map { |entry|
    entry[0].equal?(Pipeline::MethodRenamer) ? [Pipeline::MethodRenamer] #: stage_entry
      : entry
  }.freeze
end

.resolve_level(value) ⇒ Symbol

Parameters:

  • value (String, Symbol)

Returns:

  • (Symbol)

Raises:

  • (ArgumentError)


48
49
50
51
52
53
# File 'lib/ryac/minifier.rb', line 48

def self.resolve_level(value)
  level = value.to_sym
  return level if STAGES.key?(level)

  raise ArgumentError, "Invalid compress level: #{value} (valid: #{STAGES.keys.join(', ')})"
end

.run_stages(code, stages, stdlib_requires: [], rbs_files: {}, lazy_files: []) ⇒ Pipeline::RenameResult

code is raw (uncompacted) text — compaction is the runner's fixed first step, so every stage list starts from the same dialect.

Parameters:

  • code (String)
  • stages (Array[stage_entry])
  • stdlib_requires: (Array[String]) (defaults to: [])
  • rbs_files: (Hash[String, String]) (defaults to: {})
  • lazy_files: (Array[String]) (defaults to: [])

Returns:



103
104
105
106
# File 'lib/ryac/minifier.rb', line 103

def self.run_stages(code, stages, stdlib_requires: [], rbs_files: {}, lazy_files: [])
  Pipeline::StageRunner.new(stdlib_requires: stdlib_requires, rbs_files: rbs_files, lazy_files: lazy_files)
                       .call(code, stages)
end

Instance Method Details

#build_output(code, stdlib_requires, preamble = '') ⇒ String

Parameters:

  • code (String)
  • stdlib_requires (Array[String])
  • preamble (String) (defaults to: '')

Returns:

  • (String)


163
164
165
166
167
168
# File 'lib/ryac/minifier.rb', line 163

def build_output(code, stdlib_requires, preamble = '')
  parts = stdlib_requires.map { |lib| "require \"#{lib}\"" }
  parts << preamble unless preamble.empty?
  parts << code
  parts.join(';')
end

#build_result(rename_result, source, file_count) ⇒ Pipeline::MinifiedResult

Parameters:

Returns:



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/ryac/minifier.rb', line 141

def build_result(rename_result, source, file_count)
  content = build_output(rename_result.code, source.stdlib_requires, rename_result.preamble)
  # code and aliases were verified at the runner's exit; the assembled
  # content is the one string the runner never sees.
  verify_parses(content, 'content')
  size = content.bytesize

  stats = Pipeline::CompressionStats.new(
    original_size: source.original_size,
    minified_size: size,
    compression_ratio: size.to_f / source.original_size,
    file_count: file_count
  )

  @result = Pipeline::MinifiedResult.new(
    content: content,
    aliases: rename_result.aliases,
    preamble: rename_result.preamble,
    stats: stats
  )
end

#call(entry_path, level: DEFAULT_LEVEL, project_root: nil, gem_names: [], gem_require_paths: []) ⇒ Pipeline::MinifiedResult

Parameters:

  • entry_path (String, Array[String])
  • level: (Symbol, Array[stage_entry]) (defaults to: DEFAULT_LEVEL)
  • project_root: (String, Array[String], nil) (defaults to: nil)
  • gem_names: (Array[String]) (defaults to: [])
  • gem_require_paths: (Array[String]) (defaults to: [])

Returns:



117
118
119
120
121
122
123
124
125
126
# File 'lib/ryac/minifier.rb', line 117

def call(entry_path, level: DEFAULT_LEVEL, project_root: nil, gem_names: [], gem_require_paths: [])
  graph = @file_collector.call(entry_path, project_root: project_root, gem_names: gem_names, gem_require_paths: gem_require_paths)
  source = @concatenator.call(graph)
  # Captured while the boundaries still describe the text; from
  # compaction on they are input provenance, not current positions.
  file_count = source.file_boundaries.size

  result = run_pipeline(source, level)
  build_result(result, source, file_count)
end

#run_pipeline(source, target_level) ⇒ Pipeline::RenameResult

target_level is a preset name, or — for callers composing their own pipeline out of steps — an explicit stage list.

Parameters:

Returns:



132
133
134
135
136
137
138
139
# File 'lib/ryac/minifier.rb', line 132

def run_pipeline(source, target_level)
  stages = target_level.is_a?(Array) ? target_level : STAGES.fetch(self.class.resolve_level(target_level))
  self.class.run_stages(source.content, stages,
    stdlib_requires: source.stdlib_requires,
    rbs_files: source.rbs_files,
    lazy_files: source.lazy_files
  )
end