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.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +145 -0
  4. data/Rakefile +45 -0
  5. data/Steepfile +9 -0
  6. data/bin/console +11 -0
  7. data/bin/ryac +134 -0
  8. data/bin/setup +8 -0
  9. data/lib/ryac/analysis/constant/collection.rb +332 -0
  10. data/lib/ryac/analysis/constant/rename_mapping.rb +346 -0
  11. data/lib/ryac/analysis/cvar/collection.rb +37 -0
  12. data/lib/ryac/analysis/gvar/collection.rb +36 -0
  13. data/lib/ryac/analysis/gvar/rename_mapping.rb +82 -0
  14. data/lib/ryac/analysis/ivar/collection.rb +281 -0
  15. data/lib/ryac/analysis/keyword/collection.rb +213 -0
  16. data/lib/ryac/analysis/keyword/rename_mapping.rb +167 -0
  17. data/lib/ryac/analysis/local_scopes.rb +582 -0
  18. data/lib/ryac/analysis/method/collection.rb +298 -0
  19. data/lib/ryac/analysis/method/rename_mapping.rb +337 -0
  20. data/lib/ryac/analysis/method_aliases.rb +83 -0
  21. data/lib/ryac/analysis/method_aliasing.rb +65 -0
  22. data/lib/ryac/analysis/nesting.rb +123 -0
  23. data/lib/ryac/analysis/scope_management.rb +25 -0
  24. data/lib/ryac/analysis/site_bucket_mapping.rb +127 -0
  25. data/lib/ryac/analysis/type_oracle.rb +386 -0
  26. data/lib/ryac/ast_utils.rb +321 -0
  27. data/lib/ryac/errors.rb +32 -0
  28. data/lib/ryac/gem_resolver.rb +55 -0
  29. data/lib/ryac/minifier.rb +169 -0
  30. data/lib/ryac/name_generator.rb +68 -0
  31. data/lib/ryac/pipeline/analyzer.rb +264 -0
  32. data/lib/ryac/pipeline/attr_decl_shorten.rb +61 -0
  33. data/lib/ryac/pipeline/boolean_shorten.rb +26 -0
  34. data/lib/ryac/pipeline/char_shorten.rb +20 -0
  35. data/lib/ryac/pipeline/compactor.rb +1202 -0
  36. data/lib/ryac/pipeline/concatenator.rb +248 -0
  37. data/lib/ryac/pipeline/constant_aliaser.rb +240 -0
  38. data/lib/ryac/pipeline/constant_fold.rb +93 -0
  39. data/lib/ryac/pipeline/control_flow_simplify.rb +352 -0
  40. data/lib/ryac/pipeline/data_types.rb +179 -0
  41. data/lib/ryac/pipeline/endless_method.rb +60 -0
  42. data/lib/ryac/pipeline/errors.rb +94 -0
  43. data/lib/ryac/pipeline/file_collector.rb +360 -0
  44. data/lib/ryac/pipeline/method_renamer.rb +151 -0
  45. data/lib/ryac/pipeline/paren_optimizer.rb +161 -0
  46. data/lib/ryac/pipeline/rename_patcher.rb +274 -0
  47. data/lib/ryac/pipeline/source_patcher.rb +54 -0
  48. data/lib/ryac/pipeline/spelling_shorten.rb +60 -0
  49. data/lib/ryac/pipeline/stage.rb +72 -0
  50. data/lib/ryac/pipeline/stage_runner.rb +84 -0
  51. data/lib/ryac/pipeline/variable_renamer.rb +125 -0
  52. data/lib/ryac/union_find.rb +42 -0
  53. data/lib/ryac/version.rb +5 -0
  54. data/lib/ryac.rb +29 -0
  55. data/rbs_collection.lock.yaml +224 -0
  56. data/rbs_collection.yaml +25 -0
  57. data/sig/ryac/analysis/constant/collection.rbs +30 -0
  58. data/sig/ryac/analysis/constant/rename_mapping.rbs +60 -0
  59. data/sig/ryac/analysis/cvar/collection.rbs +11 -0
  60. data/sig/ryac/analysis/gvar/collection.rbs +9 -0
  61. data/sig/ryac/analysis/gvar/rename_mapping.rbs +23 -0
  62. data/sig/ryac/analysis/ivar/collection.rbs +26 -0
  63. data/sig/ryac/analysis/keyword/collection.rbs +27 -0
  64. data/sig/ryac/analysis/keyword/rename_mapping.rbs +31 -0
  65. data/sig/ryac/analysis/local_scopes.rbs +77 -0
  66. data/sig/ryac/analysis/method/collection.rbs +25 -0
  67. data/sig/ryac/analysis/method/rename_mapping.rbs +82 -0
  68. data/sig/ryac/analysis/method_aliases.rbs +10 -0
  69. data/sig/ryac/analysis/method_aliasing.rbs +9 -0
  70. data/sig/ryac/analysis/nesting.rbs +9 -0
  71. data/sig/ryac/analysis/scope_management.rbs +5 -0
  72. data/sig/ryac/analysis/site_bucket_mapping.rbs +26 -0
  73. data/sig/ryac/analysis/type_oracle.rbs +61 -0
  74. data/sig/ryac/ast_utils.rbs +34 -0
  75. data/sig/ryac/errors.rbs +21 -0
  76. data/sig/ryac/gem_resolver.rbs +28 -0
  77. data/sig/ryac/minifier.rbs +33 -0
  78. data/sig/ryac/name_generator.rbs +14 -0
  79. data/sig/ryac/pipeline/analyzer.rbs +34 -0
  80. data/sig/ryac/pipeline/attr_decl_shorten.rbs +17 -0
  81. data/sig/ryac/pipeline/boolean_shorten.rbs +11 -0
  82. data/sig/ryac/pipeline/char_shorten.rbs +11 -0
  83. data/sig/ryac/pipeline/compactor.rbs +125 -0
  84. data/sig/ryac/pipeline/concatenator.rbs +28 -0
  85. data/sig/ryac/pipeline/constant_aliaser.rbs +35 -0
  86. data/sig/ryac/pipeline/constant_fold.rbs +20 -0
  87. data/sig/ryac/pipeline/control_flow_simplify.rbs +34 -0
  88. data/sig/ryac/pipeline/data_types.rbs +151 -0
  89. data/sig/ryac/pipeline/endless_method.rbs +14 -0
  90. data/sig/ryac/pipeline/errors.rbs +46 -0
  91. data/sig/ryac/pipeline/file_collector.rbs +34 -0
  92. data/sig/ryac/pipeline/method_renamer.rbs +25 -0
  93. data/sig/ryac/pipeline/paren_optimizer.rbs +16 -0
  94. data/sig/ryac/pipeline/rename_patcher.rbs +38 -0
  95. data/sig/ryac/pipeline/source_patcher.rbs +12 -0
  96. data/sig/ryac/pipeline/spelling_shorten.rbs +14 -0
  97. data/sig/ryac/pipeline/stage.rbs +28 -0
  98. data/sig/ryac/pipeline/stage_runner.rbs +18 -0
  99. data/sig/ryac/pipeline/variable_renamer.rbs +21 -0
  100. data/sig/ryac/union_find.rbs +17 -0
  101. data/sig/ryac.rbs +13 -0
  102. data/sig/shims/env.rbs +19 -0
  103. data/sig/shims/typeprof/core/ast.rbs +299 -0
  104. data/sig/shims/typeprof/core/type.rbs +22 -0
  105. data/sig/shims/typeprof/core.rbs +73 -0
  106. data/tools/regen_pins.rb +45 -0
  107. 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