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,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
# Method alias mappings: longer method name => shorter equivalent
|
|
5
|
+
# TypeProf verifies at analysis time that the shorter method
|
|
6
|
+
# is available on the receiver type via inheritance chain.
|
|
7
|
+
#
|
|
8
|
+
# SELF-HOSTING NOTE: these rewrites fire only where inference proves
|
|
9
|
+
# the receiver, and inference over lib/'s own source can shift between
|
|
10
|
+
# self-hosting passes; the integration suite's fixed-point check is what
|
|
11
|
+
# catches a rewrite firing on one pass only.
|
|
12
|
+
# Object/Kernel aliases where both spellings name the same method on
|
|
13
|
+
# EVERY receiver — the only rows safe on a receiverless call, which
|
|
14
|
+
# dispatches on whatever self happens to be.
|
|
15
|
+
KERNEL_ALIASES = {
|
|
16
|
+
kind_of?: :is_a?,
|
|
17
|
+
yield_self: :then,
|
|
18
|
+
object_id: :__id__,
|
|
19
|
+
raise: :fail,
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Receiver-specific synonyms (Enumerable/Hash/String/...): a receiverless
|
|
23
|
+
# spelling can name something else entirely (`include?` in a class body is
|
|
24
|
+
# Module#include?), so these fire only when inference proves the receiver
|
|
25
|
+
# responds to the short spelling.
|
|
26
|
+
RECEIVER_ALIASES = {
|
|
27
|
+
collect: :map,
|
|
28
|
+
collect!: :map!,
|
|
29
|
+
detect: :find,
|
|
30
|
+
find_all: :select,
|
|
31
|
+
collect_concat: :flat_map,
|
|
32
|
+
each_pair: :each,
|
|
33
|
+
has_key?: :key?,
|
|
34
|
+
has_value?: :value?,
|
|
35
|
+
find_index: :index,
|
|
36
|
+
magnitude: :abs,
|
|
37
|
+
id2name: :to_s,
|
|
38
|
+
length: :size,
|
|
39
|
+
entries: :to_a,
|
|
40
|
+
append: :push,
|
|
41
|
+
include?: :key?,
|
|
42
|
+
member?: :key?,
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
METHOD_ALIASES = KERNEL_ALIASES.merge(RECEIVER_ALIASES).freeze
|
|
46
|
+
|
|
47
|
+
# The literal an emptiness rewrite compares against, per receiver type —
|
|
48
|
+
# the single fact both transform tables below build on.
|
|
49
|
+
EMPTY_LITERALS = { Array: '[]', String: '""', Hash: '{}' }.freeze
|
|
50
|
+
|
|
51
|
+
# Structural method transforms: method call → different syntax
|
|
52
|
+
# Applied only when TypeProf verifies receiver type compatibility.
|
|
53
|
+
# Key: [method_name, :ClassName], Value: replacement string
|
|
54
|
+
METHOD_TRANSFORMS = {
|
|
55
|
+
[:first, :Array] => '[0]',
|
|
56
|
+
[:zero?, :Numeric] => '==0',
|
|
57
|
+
**EMPTY_LITERALS.to_h { |type, lit| [[:empty?, type], "==#{lit}"] },
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
# The comparison operators the size-comparison transforms recognize, and
|
|
61
|
+
# the spelling each takes in the rewrite: `> 0` means non-empty for a
|
|
62
|
+
# size, so it shares the != spelling.
|
|
63
|
+
SIZE_COMPARISON_OPS = { :== => '==', :!= => '!=', :> => '!=' }.freeze
|
|
64
|
+
|
|
65
|
+
# Whole-comparison transforms: `.size==0` and friends collapse to a
|
|
66
|
+
# literal comparison under the same receiver-type gate as the empty?
|
|
67
|
+
# rows, one call deeper. Key: [comparison_op, :ClassName], value: the
|
|
68
|
+
# replacement for `.mid op 0`.
|
|
69
|
+
SIZE_COMPARISON_TRANSFORMS = SIZE_COMPARISON_OPS.each_with_object(
|
|
70
|
+
{} #: Hash[[Symbol, Symbol], String]
|
|
71
|
+
) { |(op, spelled), table|
|
|
72
|
+
EMPTY_LITERALS.each { |type, lit| table[[op, type]] = "#{spelled}#{lit}" }
|
|
73
|
+
}.freeze
|
|
74
|
+
|
|
75
|
+
# The inner calls those comparisons recognize, with the types where the
|
|
76
|
+
# no-argument form means size. String#count requires an argument, so a
|
|
77
|
+
# bare .count is never a String size query.
|
|
78
|
+
SIZE_QUERY_MIDS = {
|
|
79
|
+
size: %i[Array String Hash],
|
|
80
|
+
length: %i[Array String Hash],
|
|
81
|
+
count: %i[Array Hash],
|
|
82
|
+
}.freeze
|
|
83
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
module AnalysisPhases
|
|
5
|
+
def resolve_method_aliases_and_transforms(prism_root)
|
|
6
|
+
alias_map = {} #: Hash[location_key, Symbol]
|
|
7
|
+
transform_map = {} #: Hash[location_key, String]
|
|
8
|
+
suppressed_alias_keys = [] #: Array[location_key]
|
|
9
|
+
AstUtils.each_node(prism_root) do |node|
|
|
10
|
+
next unless node.is_a?(Prism::CallNode)
|
|
11
|
+
|
|
12
|
+
if node.receiver
|
|
13
|
+
shorter = METHOD_ALIASES[node.name]
|
|
14
|
+
alias_map[AstUtils.location_key(node)] = shorter if shorter && @oracle.receiver_responds_to?(node, shorter)
|
|
15
|
+
else
|
|
16
|
+
shorter = KERNEL_ALIASES[node.name]
|
|
17
|
+
alias_map[AstUtils.location_key(node)] = shorter if shorter
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if node.receiver && (node.arguments.nil? || node.arguments.arguments.empty?)
|
|
21
|
+
METHOD_TRANSFORMS.each do |(mid, type_name), replacement|
|
|
22
|
+
next unless mid == node.name
|
|
23
|
+
if @oracle.receiver_within_type?(node, type_name)
|
|
24
|
+
transform_map[AstUtils.location_key(node)] = replacement
|
|
25
|
+
break
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
register_size_comparison(node, transform_map, suppressed_alias_keys)
|
|
31
|
+
end
|
|
32
|
+
# A comparison transform swallows its inner size call whole — an
|
|
33
|
+
# alias patch on that call would overlap it.
|
|
34
|
+
suppressed_alias_keys.each { |k| alias_map.delete(k) }
|
|
35
|
+
[alias_map, transform_map]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# `.size==0` (and !=0 / >0) against a proven receiver collapses to the
|
|
41
|
+
# literal comparison. Registered on the comparison node; the renamer
|
|
42
|
+
# patches from the data receiver through the comparison's end.
|
|
43
|
+
def register_size_comparison(node, transform_map, suppressed_alias_keys)
|
|
44
|
+
return unless SIZE_COMPARISON_OPS.key?(node.name)
|
|
45
|
+
|
|
46
|
+
inner = node.receiver
|
|
47
|
+
return unless inner.is_a?(Prism::CallNode)
|
|
48
|
+
return unless inner.receiver && inner.call_operator_loc && !inner.safe_navigation?
|
|
49
|
+
return unless inner.arguments.nil? && inner.block.nil?
|
|
50
|
+
|
|
51
|
+
types = SIZE_QUERY_MIDS[inner.name]
|
|
52
|
+
return unless types
|
|
53
|
+
|
|
54
|
+
args = node.arguments&.arguments
|
|
55
|
+
return unless args && args.size == 1 && args[0].is_a?(Prism::IntegerNode) && args[0].value == 0
|
|
56
|
+
|
|
57
|
+
types.each do |type_name|
|
|
58
|
+
next unless @oracle.receiver_within_type?(inner, type_name)
|
|
59
|
+
transform_map[AstUtils.location_key(node)] = SIZE_COMPARISON_TRANSFORMS.fetch([node.name, type_name])
|
|
60
|
+
suppressed_alias_keys << AstUtils.location_key(inner)
|
|
61
|
+
break
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
# Closed-world walk of the Prism tree with the class/module nesting tracked.
|
|
5
|
+
#
|
|
6
|
+
# The nesting is a lexical fact: `class B::C` under [:M, :A] defines
|
|
7
|
+
# [:M, :A, :B, :C] — verified against TypeProf, which concatenates the
|
|
8
|
+
# written path onto the enclosing nesting exactly like this rather than
|
|
9
|
+
# resolving `B` first. Defs and `class << self` do not change it. The one
|
|
10
|
+
# thing that cannot be computed here is a dynamic path (`class self::X`,
|
|
11
|
+
# `class << other`); those bodies are not descended into, which errs on the
|
|
12
|
+
# side of leaving their contents unrenamed.
|
|
13
|
+
module Nesting
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Yields every node together with the enclosing [cpath, singleton, in_def]
|
|
17
|
+
# — singleton is true inside `class << self`, in_def inside a method body.
|
|
18
|
+
def each(root, &block)
|
|
19
|
+
walk(root, [], false, false, &block)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Yields meta-style declarations (`attr_reader :a` / `include M`) under
|
|
23
|
+
# the conditions type analysis recognizes them: a bare call in statement
|
|
24
|
+
# position directly in a class or module body (or `class << self`),
|
|
25
|
+
# outside any def. Passing loose: true drops everything but the bare-call
|
|
26
|
+
# requirement — for consumers where over-matching is the safe direction.
|
|
27
|
+
def each_meta_call(root, names, loose: false)
|
|
28
|
+
if loose
|
|
29
|
+
each(root) do |node, cpath, singleton, _in_def|
|
|
30
|
+
next unless node.is_a?(Prism::CallNode) && node.receiver.nil? && names.include?(node.name)
|
|
31
|
+
|
|
32
|
+
yield node, cpath, singleton
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
each(root) do |node, cpath, singleton, in_def|
|
|
36
|
+
next unless node.is_a?(Prism::StatementsNode)
|
|
37
|
+
next if in_def
|
|
38
|
+
# any? rather than empty?: the type-dependent empty? transform does
|
|
39
|
+
# not reach a fixed point on this code under self-hosting.
|
|
40
|
+
next unless cpath.any? || singleton
|
|
41
|
+
|
|
42
|
+
node.body.each do |child|
|
|
43
|
+
next unless child.is_a?(Prism::CallNode) && child.receiver.nil? && names.include?(child.name)
|
|
44
|
+
|
|
45
|
+
yield child, cpath, singleton
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Yields every method definition with its [cpath, singleton, name] key:
|
|
52
|
+
# the receiver form folded in (`def self.x` and defs inside `class <<
|
|
53
|
+
# self` are singleton; `def Foo.x` defines on Foo wherever it lexically
|
|
54
|
+
# sits), so every collection computes method identity the same way.
|
|
55
|
+
def each_method_definition(root)
|
|
56
|
+
each(root) do |node, cpath, sclass_singleton, _in_def|
|
|
57
|
+
next unless node.is_a?(Prism::DefNode)
|
|
58
|
+
|
|
59
|
+
singleton = sclass_singleton || !node.receiver.nil?
|
|
60
|
+
key_cpath = cpath
|
|
61
|
+
receiver = node.receiver
|
|
62
|
+
if receiver && !receiver.is_a?(Prism::SelfNode)
|
|
63
|
+
segments, _absolute = path_segments(receiver)
|
|
64
|
+
key_cpath = segments if segments
|
|
65
|
+
end
|
|
66
|
+
yield node, [key_cpath, singleton, node.name].freeze # steep:ignore ArgumentTypeMismatch
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The written segments of a qualified constant — a class/module name, an
|
|
71
|
+
# assignment target, a def receiver. Returns [segments, absolute] —
|
|
72
|
+
# absolute when written `::X` — or nil when the path is not statically
|
|
73
|
+
# known (`self::X`, `expr::X`).
|
|
74
|
+
def path_segments(constant_path)
|
|
75
|
+
segments = [] #: Array[Symbol]
|
|
76
|
+
current = constant_path
|
|
77
|
+
while current.is_a?(Prism::ConstantPathNode) || current.is_a?(Prism::ConstantPathTargetNode)
|
|
78
|
+
# Prism types #name as Symbol? only for parse-degenerate paths.
|
|
79
|
+
segments.unshift(current.name) # steep:ignore ArgumentTypeMismatch
|
|
80
|
+
current = current.parent
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
case current
|
|
84
|
+
when Prism::ConstantReadNode
|
|
85
|
+
[segments.unshift(current.name), false]
|
|
86
|
+
when nil
|
|
87
|
+
[segments, true]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def walk(node, cpath, singleton, in_def, &block)
|
|
92
|
+
return unless node.is_a?(Prism::Node)
|
|
93
|
+
|
|
94
|
+
yield node, cpath, singleton, in_def
|
|
95
|
+
|
|
96
|
+
case node
|
|
97
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
98
|
+
# The name path is walked too: its segments are constant references
|
|
99
|
+
# in their own right (counted and aliased like any other).
|
|
100
|
+
walk(node.constant_path, cpath, singleton, in_def, &block)
|
|
101
|
+
walk(node.superclass, cpath, singleton, in_def, &block) if node.is_a?(Prism::ClassNode) && node.superclass
|
|
102
|
+
segments, absolute = path_segments(node.constant_path)
|
|
103
|
+
return unless segments
|
|
104
|
+
|
|
105
|
+
inner = absolute ? segments : cpath + segments
|
|
106
|
+
walk(node.body, inner, false, false, &block)
|
|
107
|
+
when Prism::SingletonClassNode
|
|
108
|
+
walk(node.expression, cpath, singleton, in_def, &block)
|
|
109
|
+
return unless node.expression.is_a?(Prism::SelfNode)
|
|
110
|
+
|
|
111
|
+
walk(node.body, cpath, true, in_def, &block)
|
|
112
|
+
when Prism::DefNode
|
|
113
|
+
# The receiver of `def Foo.bar` is deliberately not walked: type
|
|
114
|
+
# analysis has no nodes there, and every renamer that cares reads
|
|
115
|
+
# node.receiver off the def itself.
|
|
116
|
+
walk(node.parameters, cpath, singleton, true, &block)
|
|
117
|
+
walk(node.body, cpath, singleton, true, &block)
|
|
118
|
+
else
|
|
119
|
+
node.compact_child_nodes.each { |child| walk(child, cpath, singleton, in_def, &block) }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
# Two different variables of one scope collapsing into one name silently
|
|
5
|
+
# changes what the code computes, and the result still parses — the
|
|
6
|
+
# self-hosted minifier once emitted `def c(a) = c(a) || d(a)` this way. Every
|
|
7
|
+
# scope mapping passes through here before it is used, so a collision fails
|
|
8
|
+
# the run instead of shipping.
|
|
9
|
+
#
|
|
10
|
+
# Its own module so LocalScopes can include just this: including
|
|
11
|
+
# AnalysisPhases would graft dozens of phase methods onto it as
|
|
12
|
+
# ancestors, which under self-hosting entangles its rename groups with
|
|
13
|
+
# the mixin's.
|
|
14
|
+
module RenameInvariants
|
|
15
|
+
def verify_injective!(mapping, scope_label)
|
|
16
|
+
return if mapping.size < 2
|
|
17
|
+
|
|
18
|
+
collisions = mapping.group_by { |_, short| short }
|
|
19
|
+
.filter_map { |short, pairs| [short, pairs.map(&:first)] if pairs.size > 1 } #: Array[[String, Array[Symbol]]]
|
|
20
|
+
return if collisions.empty?
|
|
21
|
+
|
|
22
|
+
raise Pipeline::RenameCollisionError.new(scope_label, collisions)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ryac
|
|
4
|
+
# One (cpath, name) → sites table for the sigil families: instance and
|
|
5
|
+
# class variables differ only in their sigil, and a name of sigil + one
|
|
6
|
+
# character is already minimal.
|
|
7
|
+
#
|
|
8
|
+
# Buckets merge along inheritance, and the merge records an alias so a
|
|
9
|
+
# site added afterwards lands in the merged bucket instead of silently
|
|
10
|
+
# resurrecting the child's. After assign_short_names the table is sealed:
|
|
11
|
+
# anything arriving later would miss renaming, so the phase that got its
|
|
12
|
+
# order wrong fails loudly instead.
|
|
13
|
+
class SiteBucketMapping
|
|
14
|
+
def initialize(prefix:)
|
|
15
|
+
@prefix = prefix
|
|
16
|
+
@kept_name_max = prefix.size + 1
|
|
17
|
+
@buckets = {}
|
|
18
|
+
@cpath_to_canonical = {}
|
|
19
|
+
@excluded_cpaths = Set.new
|
|
20
|
+
@reserved_names = {}
|
|
21
|
+
@node_short_names = {}
|
|
22
|
+
@frozen = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_site(cpath, name, node)
|
|
26
|
+
check_open!('site added')
|
|
27
|
+
canonical = resolve_canonical(cpath)
|
|
28
|
+
@buckets[canonical] ||= {}
|
|
29
|
+
(@buckets[canonical][name] ||= []) << node
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def exclude_cpath(cpath)
|
|
33
|
+
check_open!('cpath excluded')
|
|
34
|
+
@excluded_cpaths << resolve_canonical(cpath)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reserve_name(cpath, short_name)
|
|
38
|
+
check_open!('name reserved')
|
|
39
|
+
canonical = resolve_canonical(cpath)
|
|
40
|
+
@reserved_names[canonical] ||= Set.new
|
|
41
|
+
@reserved_names[canonical] << short_name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def each_canonical_cpath(&block)
|
|
45
|
+
@buckets.each_key(&block)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def merge_with_ancestor(child_cpath, ancestor_cpath)
|
|
49
|
+
check_open!('merge')
|
|
50
|
+
child_canonical = resolve_canonical(child_cpath)
|
|
51
|
+
ancestor_canonical = resolve_canonical(ancestor_cpath)
|
|
52
|
+
return if child_canonical == ancestor_canonical
|
|
53
|
+
return unless @buckets.key?(ancestor_canonical)
|
|
54
|
+
|
|
55
|
+
child_entry = @buckets[child_canonical]
|
|
56
|
+
return unless child_entry
|
|
57
|
+
|
|
58
|
+
# Recorded before the sites move: from here on, a site added for the
|
|
59
|
+
# child cpath belongs to the merged bucket.
|
|
60
|
+
@cpath_to_canonical[child_canonical] = ancestor_canonical
|
|
61
|
+
|
|
62
|
+
ancestor_entry = @buckets[ancestor_canonical]
|
|
63
|
+
child_entry.keys.each do |name|
|
|
64
|
+
child_nodes = child_entry.delete(name)
|
|
65
|
+
next unless child_nodes
|
|
66
|
+
if ancestor_entry.key?(name)
|
|
67
|
+
ancestor_entry[name].concat(child_nodes)
|
|
68
|
+
else
|
|
69
|
+
ancestor_entry[name] = child_nodes
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@buckets.delete(child_canonical) if child_entry.none?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def assign_short_names
|
|
77
|
+
@buckets.each do |cpath, names|
|
|
78
|
+
next if @excluded_cpaths.include?(cpath)
|
|
79
|
+
|
|
80
|
+
existing_names = Set.new
|
|
81
|
+
names.each_key do |name|
|
|
82
|
+
existing_names << name.to_s if name.to_s.size <= @kept_name_max
|
|
83
|
+
end
|
|
84
|
+
reserved = @reserved_names[cpath]
|
|
85
|
+
existing_names.merge(reserved) if reserved
|
|
86
|
+
|
|
87
|
+
generator = NameGenerator.new(existing_names, prefix: @prefix)
|
|
88
|
+
sorted = names.sort_by do |name, nodes|
|
|
89
|
+
-(name.to_s.size * nodes.size)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
sorted.each do |name, nodes|
|
|
93
|
+
next if name.to_s.size <= @kept_name_max
|
|
94
|
+
|
|
95
|
+
short_name = generator.next_name
|
|
96
|
+
savings = (name.to_s.size - short_name.size) * nodes.size
|
|
97
|
+
next unless savings > 0
|
|
98
|
+
|
|
99
|
+
nodes.each { |n| @node_short_names[AstUtils.location_key(n)] = short_name }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
@frozen = true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def node_mapping
|
|
106
|
+
@node_short_names.dup
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def check_open!(action)
|
|
112
|
+
raise InternalError, "#{@prefix}-mapping: #{action} after assign_short_names" if @frozen
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def resolve_canonical(cpath)
|
|
116
|
+
return cpath unless @cpath_to_canonical.key?(cpath)
|
|
117
|
+
|
|
118
|
+
current = cpath
|
|
119
|
+
seen = Set.new
|
|
120
|
+
while @cpath_to_canonical.key?(current) && !seen.include?(current)
|
|
121
|
+
seen << current
|
|
122
|
+
current = @cpath_to_canonical[current]
|
|
123
|
+
end
|
|
124
|
+
current
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|