ryac 0.2.0 → 0.3.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 +4 -4
- data/README.md +70 -16
- data/Steepfile +2 -0
- data/bin/ryac +41 -2
- data/lib/ryac/analysis/constant/collection.rb +49 -0
- data/lib/ryac/analysis/constant/rename_mapping.rb +15 -1
- data/lib/ryac/analysis/ivar/collection.rb +28 -2
- data/lib/ryac/analysis/lazy_regions.rb +87 -0
- data/lib/ryac/analysis/local_scopes.rb +110 -12
- data/lib/ryac/analysis/method/collection.rb +100 -3
- data/lib/ryac/analysis/method/rename_mapping.rb +17 -12
- data/lib/ryac/analysis/type_oracle.rb +7 -2
- data/lib/ryac/driver_file.rb +47 -0
- data/lib/ryac/minifier.rb +27 -26
- data/lib/ryac/packer.rb +127 -0
- data/lib/ryac/pipeline/analyzer.rb +25 -3
- data/lib/ryac/pipeline/concatenator.rb +127 -8
- data/lib/ryac/pipeline/data_types.rb +13 -5
- data/lib/ryac/pipeline/file_collector.rb +64 -4
- data/lib/ryac/pipeline/method_renamer.rb +15 -0
- data/lib/ryac/pipeline/stage.rb +4 -0
- data/lib/ryac/pipeline/stage_runner.rb +7 -3
- data/lib/ryac/version.rb +1 -1
- data/lib/ryac.rb +3 -0
- data/sig/ryac/analysis/constant/collection.rbs +3 -0
- data/sig/ryac/analysis/constant/rename_mapping.rbs +2 -1
- data/sig/ryac/analysis/ivar/collection.rbs +1 -0
- data/sig/ryac/analysis/lazy_regions.rbs +10 -0
- data/sig/ryac/analysis/local_scopes.rbs +9 -1
- data/sig/ryac/analysis/method/collection.rbs +3 -0
- data/sig/ryac/analysis/method/rename_mapping.rbs +3 -2
- data/sig/ryac/analysis/type_oracle.rbs +1 -1
- data/sig/ryac/driver_file.rbs +8 -0
- data/sig/ryac/minifier.rbs +1 -2
- data/sig/ryac/packer.rbs +12 -0
- data/sig/ryac/pipeline/analyzer.rbs +6 -0
- data/sig/ryac/pipeline/concatenator.rbs +16 -1
- data/sig/ryac/pipeline/data_types.rbs +8 -5
- data/sig/ryac/pipeline/file_collector.rbs +8 -3
- data/sig/ryac/pipeline/method_renamer.rbs +3 -0
- data/sig/ryac/pipeline/stage.rbs +1 -0
- data/sig/ryac/pipeline/stage_runner.rbs +2 -1
- metadata +15 -8
- data/bin/console +0 -11
- data/bin/setup +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e028fd595a5109347a3c20ddca86a61fa1ade6a043276471e771ab60c4723a9
|
|
4
|
+
data.tar.gz: aa11c81ee53ce49056f8f7dfd2679549b75855e9b4a912558e461d80348ef79a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 980ad6bcee3e5db39ee429715b10555e831401b1fcd18f5f918d7128d4548c33185a441c5bb8b7554f0836f15b677067119e9a1cc26b8b8bd06a4ea488ba6e3e
|
|
7
|
+
data.tar.gz: b847884f580ed660733e9d23fa71aa706257257771f7a1e1bf3cc2c27801e400e6b38b289adbe7498e574c984c9627a1e91676470f465550322b79c268938dbe
|
data/README.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
A Ruby code minifier that uses [TypeProf](https://github.com/ruby/typeprof) for type-aware analysis and AST-based transformations to achieve high compression rates while preserving functional equivalence.
|
|
6
6
|
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
ryac is under active development and no interface is settled yet. The architecture — the pipeline stages, the rename policies, the alias and lazy-region mechanisms — and with it the CLI options, the `Ryac::Minifier` API and the shape of the output may all change between releases. Pin an exact version if you depend on any of them.
|
|
10
|
+
|
|
7
11
|
## Philosophy
|
|
8
12
|
|
|
9
13
|
This project takes an **aggressive optimization** approach. In Ruby, even comment removal requires context awareness — for example, `foo\n# comment\n.to_s` works because the comment connects the method chain, but removing it causes a syntax error. Whether a given comment is safe to remove depends entirely on context, making the boundary between safe and unsafe transformations inherently blurry. Since conservative minification is already difficult in Ruby, we choose to optimize as aggressively as possible. TypeProf's type analysis helps make these transformations more informed, but the goal is always maximum compression.
|
|
@@ -11,9 +15,10 @@ This project takes an **aggressive optimization** approach. In Ruby, even commen
|
|
|
11
15
|
## Features
|
|
12
16
|
|
|
13
17
|
- **Multi-file support**: Follows `require_relative` and `autoload` to collect and concatenate dependencies into a single output
|
|
18
|
+
- **Dynamic requires**: A `require_relative "driver/#{name}"` inside a method bundles every file under that directory as a lazy region — registered at load, run the moment the require would have run — so a driver's own `require "ffi"` keeps its optional-dependency timing while the whole program shares one rename table (see [Dynamic requires](#dynamic-requires))
|
|
14
19
|
- **Whitespace & comment removal**: Strips all unnecessary whitespace and comments
|
|
15
20
|
- **AST transformations**: Boolean/char shortening, constant folding, control flow simplification, endless methods, parenthesis optimization
|
|
16
|
-
- **Constant aliasing**: Renames user-defined constants and shortens repeated external constant paths
|
|
21
|
+
- **Constant aliasing**: Renames user-defined constants and shortens repeated external constant paths, emitting backward-compat aliases; once a program's dynamically loaded files are bundled, the only reader left outside is a launcher, which spells the class/module skeleton (`Optcarrot::NES.new.run`) and never a value constant — so value-constant aliases are dropped and the skeleton's kept
|
|
17
22
|
- **Variable renaming**: Shortens local variables, keyword arguments, instance/class/global variables
|
|
18
23
|
- **Method renaming**: Shortens method names with `send(:sym)` coordination and attr-backed ivar optimization
|
|
19
24
|
- **Method alias shortening**: Replaces long stdlib method names with shorter aliases (e.g., `collect` → `map`)
|
|
@@ -22,6 +27,12 @@ This project takes an **aggressive optimization** approach. In Ruby, even commen
|
|
|
22
27
|
|
|
23
28
|
## Installation
|
|
24
29
|
|
|
30
|
+
```bash
|
|
31
|
+
gem install ryac
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or in a Gemfile:
|
|
35
|
+
|
|
25
36
|
```ruby
|
|
26
37
|
gem 'ryac'
|
|
27
38
|
```
|
|
@@ -32,28 +43,34 @@ gem 'ryac'
|
|
|
32
43
|
|
|
33
44
|
```bash
|
|
34
45
|
# Minify a file (follows require_relative automatically)
|
|
35
|
-
|
|
46
|
+
ryac path/to/entry.rb
|
|
36
47
|
|
|
37
48
|
# Specify compression level (stable or unstable)
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
ryac path/to/entry.rb -c stable
|
|
50
|
+
ryac path/to/entry.rb -c unstable
|
|
40
51
|
|
|
41
52
|
# Write to output file
|
|
42
|
-
|
|
53
|
+
ryac path/to/entry.rb -o minified.rb
|
|
43
54
|
|
|
44
55
|
# Write constant aliases to a separate file
|
|
45
|
-
|
|
56
|
+
ryac path/to/entry.rb -o minified.rb -a aliases.rb
|
|
57
|
+
|
|
58
|
+
# Emit a self-extracting packed file (self = zero dependencies, zlib = smaller)
|
|
59
|
+
ryac path/to/entry.rb -o packed.rb --pack self
|
|
60
|
+
|
|
61
|
+
# Keep the program a pure library and put its lazy regions in a runner
|
|
62
|
+
ryac path/to/entry.rb -o minify.rb --driver driver.rb
|
|
46
63
|
|
|
47
64
|
# Multiple entry points
|
|
48
|
-
|
|
65
|
+
ryac file1.rb file2.rb
|
|
49
66
|
|
|
50
67
|
# Minify installed gem(s) by name (resolved via Gem::Specification)
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
ryac -g rack
|
|
69
|
+
ryac -g rack,rack-session -o bundle.rb
|
|
53
70
|
|
|
54
71
|
# Show version / help
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
ryac -v
|
|
73
|
+
ryac -h
|
|
57
74
|
```
|
|
58
75
|
|
|
59
76
|
### Ruby API
|
|
@@ -80,21 +97,58 @@ There are exactly two levels, named for their promise. The default is **`stable`
|
|
|
80
97
|
|
|
81
98
|
| Level | Transformations | Promise |
|
|
82
99
|
|-------|----------------|---------|
|
|
83
|
-
| `stable` | AST compaction and folding, constant/class/module renaming with compatibility aliases, external prefix aliasing, local/keyword/instance/class/global variable renaming | Verified frame-for-frame on a real program (Optcarrot). Sound under closed-world analysis; reflection over *names the program renames* is the caveat. |
|
|
84
|
-
| `unstable` |
|
|
100
|
+
| `stable` | AST compaction and folding, constant/class/module renaming with compatibility aliases, external prefix aliasing, local/keyword/instance/class/global variable renaming, and method renaming under the `:safe` policy — a name is renamed only when type inference resolved every caller and no dynamic escape hatch (a string mention, a dynamic-ivar class, an uncalled def) touches it; attr declarations, their backing ivars and their call sites move together | Verified frame-for-frame on a real program (Optcarrot). Sound under closed-world analysis; reflection over *names the program renames* is the caveat. |
|
|
101
|
+
| `unstable` | The same stages; method renaming switches to `:aggressive`, which also renames names whose callers type inference could not resolve, betting that a same-named call is the same method | A program can defeat that bet by construction (names inside strings, `eval`'d source, `send(computed)`), so this works only when the program plays along. Verified by self-hosting. |
|
|
85
102
|
|
|
86
103
|
Finer configurations are not levels: the pipeline is built from steps, and callers can pass an explicit stage list in place of a level name (`Minifier#call(path, level: [...stage defs...])`). The unit tests pin each step's behavior through exactly that mechanism.
|
|
87
104
|
|
|
105
|
+
### Dynamic requires
|
|
106
|
+
|
|
107
|
+
A file the program loads by a computed path — optcarrot's `require_relative "driver/#{name}_#{type}"` inside `Driver.load_each` — is outside any static dependency graph, yet it runs against the program: subclassing its classes, reading its instance variables, overriding its methods. Renaming one side and not the other breaks exactly there. When the computed path starts with a static directory, ryac bundles every `.rb` file under it (and whatever those files require that nothing static did) into the same closed world, so the rename table covers both sides, and writes each as a *lazy region*:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
RYAC_LAZY["optcarrot/driver/sdl2_video"] = -> { ...the file, minified... }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The region's body runs when the original require would have run — a short loader keeps Ruby's contract (once and `true`, `false` on a repeat, a failed run stays retryable, an unregistered path falls through to a real `require_relative` relative to the bundle), and the require sites are rewritten to call it. So `require "ffi"` and `ffi_lib "SDL2"` at a driver's top level still execute only when that driver is selected, and the auto-selection's `rescue LoadError` still walks on to the next one.
|
|
114
|
+
|
|
115
|
+
The output is still one file. For optcarrot, it stands in for `lib/optcarrot.rb` under upstream's unmodified `bin/optcarrot`:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
ryac gem_tests/optcarrot/lib/optcarrot.rb -o /path/to/optcarrot/lib/optcarrot.rb
|
|
119
|
+
cd /path/to/optcarrot && bin/optcarrot examples/Lan_Master.nes
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A constant only a region defines (`SDL2Video`, the `SDL2` module) keeps its name — it does not exist until the region runs, so no alias at the end of the file could restore it — and an external constant a region references is never hoisted into the preamble. Neither costs much: a region's own names are few, and its references to the core rename like everything else.
|
|
123
|
+
|
|
124
|
+
Or keep the program a pure library and put the regions in a runner of their own:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
ryac gem_tests/optcarrot/lib/optcarrot.rb -o minify.rb --driver driver.rb
|
|
128
|
+
ruby driver.rb minify.rb --exec "Optcarrot::NES.new.run" examples/Lan_Master.nes
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`minify.rb` is the loader, the core and its aliases — nothing that runs. `driver.rb` loads it, registers the regions and evals the `--exec` expression at top level, its own two arguments already gone from `ARGV` so the program's option parsing sees only what follows (anything after `--` is never read). The split assumes the program loads its dynamic files after boot, from a method — the shape that made them lazy; a dynamic require the core ran while loading would miss its region. The expression is code outside the bundle, so it can spell only what survives outside: the class/module skeleton, which the aliases restore, and methods the program itself never calls — under `stable`'s safe policy an uncalled def keeps its name, and a launcher's entry point (`NES#run`) is exactly that. `--driver` needs `-o`, and cannot be combined with `-a` or with `--pack` (a packed core cannot be loaded).
|
|
132
|
+
|
|
133
|
+
### Packed output
|
|
134
|
+
|
|
135
|
+
`--pack` is an output format, orthogonal to the levels: it wraps the minified program in a self-extracting stub — a short plain-Ruby decoder followed by the compressed bytes after `__END__`. On optcarrot it takes the artifact from 37% of the original source to 19.4% (`self`) or 15.1% (`zlib`).
|
|
136
|
+
|
|
137
|
+
- `--pack self` inlines a pure-Ruby LZSS decoder (282 bytes) — no `require` at all, runs anywhere Ruby runs
|
|
138
|
+
- `--pack zlib` deflates via the zlib default gem — smaller, but dead on a Ruby built without it
|
|
139
|
+
|
|
140
|
+
A packed file is a main-program format: the stub reads its own `__END__` data and `eval`s the program with `$0` as the file name, so `ruby packed.rb` runs it and a `$PROGRAM_NAME == __FILE__` launcher still fires. It cannot be `require`d — only the main script has `DATA` — and a program that itself uses `__END__` or reads `DATA` is refused with an error.
|
|
141
|
+
|
|
88
142
|
See [`tests/ryac/pipeline/`](tests/ryac/pipeline/) for per-stage transformation examples, and [`tests/ryac/levels/`](tests/ryac/levels/) for end-to-end compression examples.
|
|
89
143
|
|
|
90
144
|
### What the levels are verified against
|
|
91
145
|
|
|
92
146
|
The supported boundary is defined by two programs, both verified in CI:
|
|
93
147
|
|
|
94
|
-
- **[Optcarrot](https://github.com/mame/optcarrot) at `stable`** — the minified emulator matches the original frame-for-frame across the 180-frame demo and three scripted play scenarios (1,820 frames of title menus, piece rotation, pausing and button mashing) ([`tests/test_optcarrot.rb`](tests/test_optcarrot.rb))
|
|
148
|
+
- **[Optcarrot](https://github.com/mame/optcarrot) at `stable`** — the minified emulator matches the original frame-for-frame across the 180-frame demo and three scripted play scenarios (1,820 frames of title menus, piece rotation, pausing and button mashing), and, standing in for `lib/optcarrot.rb` under upstream's own `bin/optcarrot`, its bundled png and wav drivers write the same frame and samples as the original's ([`tests/test_optcarrot.rb`](tests/test_optcarrot.rb))
|
|
95
149
|
- **This minifier itself at `unstable`** — the minified minifier re-minifies the original source to identical output, and minifying its own output is a byte-identical fixed point ([`tests/test_integration.rb`](tests/test_integration.rb))
|
|
96
150
|
|
|
97
|
-
Whether `unstable` holds for a given program depends on the program. Optcarrot stops at `stable` because it defeats method renaming by construction: it builds its CPU/PPU cores as source strings and `eval`s them, scans that text for `@ivar` names with a regexp, and dispatches through `send(computed_symbol)` — method names survive inside strings, out of reach of static analysis. The sinatra and rubocop suites run in CI as regression canaries on a keyword-only step composition, but they sit outside this boundary and do not define it.
|
|
151
|
+
Whether `unstable` holds for a given program depends on the program. Optcarrot stops at `stable` because it defeats aggressive method renaming by construction: it builds its CPU/PPU cores as source strings and `eval`s them, scans that text for `@ivar` names with a regexp, and dispatches through `send(computed_symbol)` — method names survive inside strings, out of reach of static analysis. The sinatra and rubocop suites run in CI as regression canaries on a keyword-only step composition, but they sit outside this boundary and do not define it.
|
|
98
152
|
|
|
99
153
|
## Development
|
|
100
154
|
|
|
@@ -133,7 +187,7 @@ FileCollector → Concatenator → StageRunner (Compactor → stage list) → Ou
|
|
|
133
187
|
2. **Concatenator** — Topologically sorts files and concatenates them into a single source
|
|
134
188
|
3. **StageRunner** — Compacts the source (AST rebuilt into minimal whitespace form), then walks the level's ordered stage list. Every stage implements one contract — `needs_analysis?` / `fixpoint?` / `collect(ctx, patches)` / `finish(ctx)` — and phase is list position:
|
|
135
189
|
- **Syntactic stages** (no analysis): `ControlFlowSimplify`, `EndlessMethod`, `ConstantFold`, `BooleanShorten`, `CharShorten` before the rename batch; `ParenOptimizer` after it
|
|
136
|
-
- **Analysis stages**: `ConstantAliaser`, `AttrDeclShorten`, `VariableRenamer` (
|
|
190
|
+
- **Analysis stages**: `ConstantAliaser`, `AttrDeclShorten`, `VariableRenamer`, `MethodRenamer` (`:safe` at `stable`, `:aggressive` at `unstable`) — consecutive analysis stages form one batch sharing a single TypeProf pass, and the runner rejects a list that would need two
|
|
137
191
|
|
|
138
192
|
## Dependencies
|
|
139
193
|
|
data/Steepfile
CHANGED
data/bin/ryac
CHANGED
|
@@ -8,7 +8,9 @@ options = {
|
|
|
8
8
|
output: nil,
|
|
9
9
|
aliases: nil,
|
|
10
10
|
level: nil,
|
|
11
|
-
gems: nil
|
|
11
|
+
gems: nil,
|
|
12
|
+
pack: nil,
|
|
13
|
+
driver: nil
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
parser = OptionParser.new do |opts|
|
|
@@ -36,6 +38,20 @@ parser = OptionParser.new do |opts|
|
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
opts.on("-p", "--pack FORMAT", "Emit a self-extracting file (self or zlib)") do |value|
|
|
42
|
+
begin
|
|
43
|
+
options[:pack] = Ryac::Packer.resolve_format(value)
|
|
44
|
+
rescue ArgumentError => e
|
|
45
|
+
warn "Error: #{e.message}"
|
|
46
|
+
warn opts
|
|
47
|
+
exit 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
opts.on("-d", "--driver FILE", "Write the lazy regions to FILE as a runner (ruby FILE CORE --exec EXPR)") do |file|
|
|
52
|
+
options[:driver] = file
|
|
53
|
+
end
|
|
54
|
+
|
|
39
55
|
opts.on("-h", "--help", "Display this help message") do
|
|
40
56
|
puts opts
|
|
41
57
|
exit 0
|
|
@@ -55,6 +71,21 @@ rescue OptionParser::InvalidOption => e
|
|
|
55
71
|
exit 1
|
|
56
72
|
end
|
|
57
73
|
|
|
74
|
+
if options[:driver]
|
|
75
|
+
unless options[:output]
|
|
76
|
+
warn "Error: --driver needs -o for the core file"
|
|
77
|
+
exit 1
|
|
78
|
+
end
|
|
79
|
+
if options[:aliases]
|
|
80
|
+
warn "Error: --driver cannot be combined with -a (the runner loads the core with its aliases)"
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
if options[:pack]
|
|
84
|
+
warn "Error: --driver cannot be combined with --pack (a packed core cannot be loaded)"
|
|
85
|
+
exit 1
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
58
89
|
if options[:gems]
|
|
59
90
|
unless ARGV.empty?
|
|
60
91
|
warn "Error: Cannot specify both --gem and file arguments"
|
|
@@ -108,9 +139,17 @@ begin
|
|
|
108
139
|
result.full_content
|
|
109
140
|
end
|
|
110
141
|
|
|
142
|
+
content = Ryac::Packer.pack(content, options[:pack]) if options[:pack]
|
|
143
|
+
|
|
144
|
+
if options[:driver]
|
|
145
|
+
content, driver = Ryac::DriverFile.split(content)
|
|
146
|
+
File.write(options[:driver], driver)
|
|
147
|
+
end
|
|
148
|
+
|
|
111
149
|
if options[:output]
|
|
112
|
-
File.
|
|
150
|
+
File.binwrite(options[:output], content)
|
|
113
151
|
else
|
|
152
|
+
$stdout.binmode if options[:pack]
|
|
114
153
|
puts content
|
|
115
154
|
end
|
|
116
155
|
|
|
@@ -101,6 +101,52 @@ module Ryac
|
|
|
101
101
|
(value.name == :define && receiver.name == :Data)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# A constant only a lazy region defines does not exist until the region
|
|
105
|
+
# runs, so no alias for it could execute at the end of the file: it keeps
|
|
106
|
+
# its name, and `Optcarrot.const_get(:SDL2Video)` finds it as written. A
|
|
107
|
+
# constant the flat code defines too is merely reopened by the region
|
|
108
|
+
# and renames as usual.
|
|
109
|
+
def exclude_lazy_definitions(prism_root)
|
|
110
|
+
return if @lazy_regions.empty?
|
|
111
|
+
|
|
112
|
+
flat = Set.new #: Set[Array[Symbol]]
|
|
113
|
+
lazy = Set.new #: Set[Array[Symbol]]
|
|
114
|
+
each_constant_event(prism_root) do |kind, node, cpath, _singleton, _in_def|
|
|
115
|
+
next unless cpath && (kind == :class_def || kind == :write)
|
|
116
|
+
|
|
117
|
+
(LazyRegions.contains?(@lazy_regions, node) ? lazy : flat) << cpath
|
|
118
|
+
end
|
|
119
|
+
(lazy - flat).each { |cpath| @constant_mapping.exclude_path(cpath) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# `expr::NAME` reads a constant on a scope only the running program
|
|
123
|
+
# knows — `self.class::OPTIONS` in an included helper lands on whichever
|
|
124
|
+
# class included it — so no static path stands in for the reference and
|
|
125
|
+
# the name in the text is the name looked up. Every user constant called
|
|
126
|
+
# NAME keeps its name, so any of them can be the one meant.
|
|
127
|
+
def exclude_dynamic_root_reads(prism_root)
|
|
128
|
+
names = Set.new #: Set[Symbol]
|
|
129
|
+
each_constant_event(prism_root) do |kind, node, _cpath, _singleton, _in_def|
|
|
130
|
+
next unless kind == :read && node.is_a?(Prism::ConstantPathNode) && dynamic_root?(node)
|
|
131
|
+
|
|
132
|
+
name = node.name
|
|
133
|
+
names << name if name
|
|
134
|
+
end
|
|
135
|
+
return if names.empty?
|
|
136
|
+
|
|
137
|
+
pinned = [] #: Array[Array[Symbol]]
|
|
138
|
+
@constant_mapping.each_user_defined_path { |cpath| pinned << cpath if names.include?(cpath.last) }
|
|
139
|
+
pinned.each { |cpath| @constant_mapping.exclude_path(cpath) }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A path whose outermost parent is neither a constant nor absent (`::X`)
|
|
143
|
+
# — `self::X`, `self.class::X`, `klass::X`.
|
|
144
|
+
def dynamic_root?(node)
|
|
145
|
+
current = node.parent
|
|
146
|
+
current = current.parent while current.is_a?(Prism::ConstantPathNode)
|
|
147
|
+
!(current.nil? || current.is_a?(Prism::ConstantReadNode))
|
|
148
|
+
end
|
|
149
|
+
|
|
104
150
|
def collect_constants(prism_root)
|
|
105
151
|
each_constant_event(prism_root) do |kind, node, cpath, singleton, in_def|
|
|
106
152
|
case kind
|
|
@@ -224,6 +270,9 @@ module Ryac
|
|
|
224
270
|
each_constant_event(prism_root) do |kind, node, _cpath, _singleton, _in_def|
|
|
225
271
|
next unless kind == :read
|
|
226
272
|
next if counted_prefix_ids.include?(node.object_id)
|
|
273
|
+
# A region's reference resolves when the region runs — after its own
|
|
274
|
+
# requires. A preamble alias for it would resolve at boot.
|
|
275
|
+
next if LazyRegions.contains?(@lazy_regions, node)
|
|
227
276
|
|
|
228
277
|
mark_chain_prefixes(node, counted_prefix_ids)
|
|
229
278
|
prefix = external_prefix_for(node)
|
|
@@ -58,8 +58,13 @@ module Ryac
|
|
|
58
58
|
# minifier's own process, which over-approximates: everything the
|
|
59
59
|
# minifier has loaded — including, under self-hosting, the analyzed
|
|
60
60
|
# program itself — then looks like a reopening.
|
|
61
|
-
|
|
61
|
+
#
|
|
62
|
+
# alias_surface: which renames the alias block restores — :full, every
|
|
63
|
+
# one, or :skeleton, classes and modules only. See
|
|
64
|
+
# generate_alias_declarations.
|
|
65
|
+
def initialize(boot_roots: nil, alias_surface: :full)
|
|
62
66
|
@boot_roots = boot_roots
|
|
67
|
+
@alias_surface = alias_surface
|
|
63
68
|
@mappings = {} # Hash<Array<Symbol>, ConstantInfo> - key is static_cpath
|
|
64
69
|
# keyed by the last path segment
|
|
65
70
|
@by_name = {}
|
|
@@ -205,8 +210,17 @@ module Ryac
|
|
|
205
210
|
# Generate backward-compatible alias declarations for renamed constants.
|
|
206
211
|
# Returns array of strings like "OriginalName=ShortName" or
|
|
207
212
|
# "ShortParent::OriginalName=ShortParent::ShortName" for nested constants.
|
|
213
|
+
#
|
|
214
|
+
# With the :full surface every rename stays restorable: nothing says
|
|
215
|
+
# which original names code outside the analyzed world spells. A program
|
|
216
|
+
# that dynamically loads files at runtime had enumerated its external
|
|
217
|
+
# readers, and bundling them as lazy regions brought every one inside;
|
|
218
|
+
# what remains outside is a launcher, and a launcher spells the
|
|
219
|
+
# class/module skeleton (`Optcarrot::NES.new.run`), never a value
|
|
220
|
+
# constant. That is the :skeleton surface.
|
|
208
221
|
def generate_alias_declarations
|
|
209
222
|
renamed = @mappings.values.select(&:short_name).sort_by { |info| [info.full_path.size, info.full_path] }
|
|
223
|
+
renamed.reject! { |info| info.definition_type == :value } if @alias_surface == :skeleton
|
|
210
224
|
renamed.filter_map { |info| build_alias_declaration(info) }
|
|
211
225
|
end
|
|
212
226
|
|
|
@@ -23,16 +23,42 @@ module Ryac
|
|
|
23
23
|
case node
|
|
24
24
|
when Prism::InstanceVariableReadNode, *IVAR_WRITE_NODES
|
|
25
25
|
# @type var node: Prism::InstanceVariableReadNode | ivar_write_node
|
|
26
|
-
next if attr_backed
|
|
26
|
+
next if attr_backed_ivar?(attr_backed, cpath, node.name)
|
|
27
27
|
@ivar_rename_mapping.add_site(cpath, node.name, node)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# An attr declared anywhere up the chain backs the ivar in every
|
|
33
|
+
# descendant: a subclass's `@palette = ...` fills the slot the
|
|
34
|
+
# ancestor's `attr_reader :palette` reads. Those sites belong to the
|
|
35
|
+
# attr coordination (which follows the declaring ancestor), not to plain
|
|
36
|
+
# ivar renaming — which would give the subclass its own name for the
|
|
37
|
+
# slot and leave the reader looking at nothing.
|
|
38
|
+
def attr_backed_ivar?(attr_backed, cpath, name)
|
|
39
|
+
backed = false
|
|
40
|
+
@oracle.each_ancestor_cpath(cpath, false) do |ancestor_cpath|
|
|
41
|
+
backed ||= attr_backed.fetch(ancestor_cpath, nil)&.include?(name) || false
|
|
42
|
+
end
|
|
43
|
+
backed || (attr_backed.fetch(cpath, nil)&.include?(name) || false)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The attr-backed ivar names per class, closed upward through the
|
|
47
|
+
# program's own classes: an attr declared in D backs the slot in every
|
|
48
|
+
# ancestor of D the program defines, since a write there (`@label_text =
|
|
49
|
+
# ...` in a base class) fills what D's reader reads. Object and the rest
|
|
50
|
+
# of the built-in chain write nothing and are not closed over — they
|
|
51
|
+
# would make every attr back every class's ivar of that name.
|
|
32
52
|
def collect_attr_backed_ivars(prism_root)
|
|
53
|
+
defined = Set.new #: Set[Array[Symbol]]
|
|
54
|
+
each_constant_event(prism_root) { |kind, _node, cpath, _singleton, _in_def| defined << cpath if kind == :class_def && cpath }
|
|
55
|
+
|
|
33
56
|
result = Hash.new { |h, k| h[k] = Set.new } #: Hash[Array[Symbol], Set[Symbol]]
|
|
34
57
|
each_attr_declaration(prism_root, ATTR_DECLARATION_METHODS, require_class_body: false) do |_node, cpath, _singleton, sym|
|
|
35
58
|
result[cpath] << :"@#{sym}"
|
|
59
|
+
@oracle.each_ancestor_cpath(cpath, false) do |ancestor_cpath|
|
|
60
|
+
result[ancestor_cpath] << :"@#{sym}" if defined.include?(ancestor_cpath)
|
|
61
|
+
end
|
|
36
62
|
end
|
|
37
63
|
result
|
|
38
64
|
end
|
|
@@ -151,7 +177,7 @@ module Ryac
|
|
|
151
177
|
used_ivar_names = @ivar_rename_mapping.node_mapping.values.to_set
|
|
152
178
|
used_ivar_names.merge(path_a_mapping.values)
|
|
153
179
|
used_method_names = rename_map.values.to_set
|
|
154
|
-
scope_vars =
|
|
180
|
+
scope_vars = @scope_visible_names
|
|
155
181
|
generator = NameGenerator.new(prefix: "@")
|
|
156
182
|
|
|
157
183
|
path_b_info
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
# A file the program loads at runtime through a dynamic require
|
|
5
|
+
# (optcarrot's `require_relative "driver/#{name}_#{type}"`) is bundled by
|
|
6
|
+
# the Concatenator as a registration:
|
|
7
|
+
#
|
|
8
|
+
# RYAC_LAZY["optcarrot/driver/sdl2_video"] = -> { <the file> }
|
|
9
|
+
#
|
|
10
|
+
# The lambda is the file's schedule and nothing more: its body runs when
|
|
11
|
+
# the original require would have, so a `require "ffi"` or `ffi_lib
|
|
12
|
+
# "SDL2"` at the file's top level keeps its optional-dependency timing.
|
|
13
|
+
# For analysis the body is ordinary code in the closed world — one rename
|
|
14
|
+
# table across the core and the files that subclass it.
|
|
15
|
+
#
|
|
16
|
+
# Two things about a region are special. TypeProf does not register a
|
|
17
|
+
# class defined inside a block, so it is handed a view of the source with
|
|
18
|
+
# the wrapper bytes blanked (typeprof_view): every byte of the body keeps
|
|
19
|
+
# its position, and the body reads as the top-level code it becomes when
|
|
20
|
+
# it runs. And a region does not exist at boot: a constant it alone
|
|
21
|
+
# defines cannot be aliased at the end of the file, and an external
|
|
22
|
+
# constant it references cannot be hoisted into the preamble.
|
|
23
|
+
#
|
|
24
|
+
# Regions are recognized by shape — a top-level `Const["key"] = -> { ... }`
|
|
25
|
+
# statement with a parameterless lambda — not by the registry's name,
|
|
26
|
+
# which the aliaser renames like any other constant. Reading a program's
|
|
27
|
+
# own statement of that shape as a region is safe: every consequence keeps
|
|
28
|
+
# more names, never fewer.
|
|
29
|
+
module LazyRegions
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# The lambdas of every registration, in source order.
|
|
33
|
+
def collect(prism_root)
|
|
34
|
+
lambdas = [] #: Array[Prism::LambdaNode]
|
|
35
|
+
each_registration(prism_root) { |_statement, lambda_node| lambdas << lambda_node }
|
|
36
|
+
lambdas
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def contains?(lambdas, node)
|
|
40
|
+
offset = node.location.start_offset
|
|
41
|
+
lambdas.any? { |lam| offset > lam.opening_loc.start_offset && offset < lam.closing_loc.start_offset }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The source as TypeProf must read it: each registration's wrapper —
|
|
45
|
+
# `["key"] = -> {` after the registry constant, and the closing `}` —
|
|
46
|
+
# replaced by spaces, newlines kept, so the body sits at top level at
|
|
47
|
+
# its original byte positions. The registry constant stays, as its own
|
|
48
|
+
# statement: it is a reference the aliaser renames like any other, and
|
|
49
|
+
# it has to be renamed at the registration too.
|
|
50
|
+
def typeprof_view(content, prism_root)
|
|
51
|
+
bytes = content.b
|
|
52
|
+
each_registration(prism_root) do |statement, lambda_node|
|
|
53
|
+
# statement.receiver is the registry constant (registration_lambda)
|
|
54
|
+
from = statement.receiver.location.end_offset # steep:ignore NoMethod
|
|
55
|
+
blank(bytes, from, lambda_node.opening_loc.end_offset)
|
|
56
|
+
bytes.setbyte(from, 0x3b)
|
|
57
|
+
blank(bytes, lambda_node.closing_loc.start_offset, lambda_node.closing_loc.end_offset)
|
|
58
|
+
end
|
|
59
|
+
bytes.force_encoding(content.encoding)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def each_registration(prism_root)
|
|
63
|
+
prism_root.statements.body.each do |statement|
|
|
64
|
+
lambda_node = registration_lambda(statement)
|
|
65
|
+
yield statement, lambda_node if lambda_node
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def registration_lambda(statement)
|
|
70
|
+
return nil unless statement.is_a?(Prism::CallNode) && statement.name == :[]=
|
|
71
|
+
return nil unless statement.receiver.is_a?(Prism::ConstantReadNode)
|
|
72
|
+
|
|
73
|
+
args = statement.arguments&.arguments
|
|
74
|
+
return nil unless args && args.size == 2
|
|
75
|
+
|
|
76
|
+
key, value = args
|
|
77
|
+
return nil unless key.is_a?(Prism::StringNode) && value.is_a?(Prism::LambdaNode)
|
|
78
|
+
return nil if value.parameters
|
|
79
|
+
|
|
80
|
+
value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def blank(bytes, from, to)
|
|
84
|
+
(from...to).each { |i| bytes.setbyte(i, 0x20) unless bytes.getbyte(i) == 0x0a }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|