aboutsummaryrefslogtreecommitdiffstats
path: root/lib/irb/type_completion/scope.rb
blob: 5a58a0ed65c93848ad00357635bd77024be458c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# frozen_string_literal: true

require 'set'
require_relative 'types'

module IRB
  module TypeCompletion

    class RootScope
      attr_reader :module_nesting, :self_object

      def initialize(binding, self_object, local_variables)
        @binding = binding
        @self_object = self_object
        @cache = {}
        modules = [*binding.eval('::Module.nesting'), Object]
        @module_nesting = modules.map { [_1, []] }
        binding_local_variables = binding.local_variables
        uninitialized_locals = local_variables - binding_local_variables
        uninitialized_locals.each { @cache[_1] = Types::NIL }
        @local_variables = (local_variables | binding_local_variables).map(&:to_s).to_set
        @global_variables = Kernel.global_variables.map(&:to_s).to_set
        @owned_constants_cache = {}
      end

      def level() = 0

      def level_of(_name, _var_type) = 0

      def mutable?() = false

      def module_own_constant?(mod, name)
        set = (@owned_constants_cache[mod] ||= Set.new(mod.constants.map(&:to_s)))
        set.include? name
      end

      def get_const(nesting, path, _key = nil)
        return unless nesting

        result = path.reduce nesting do |mod, name|
          return nil unless mod.is_a?(Module) && module_own_constant?(mod, name)
          mod.const_get name
        end
        Types.type_from_object result
      end

      def get_cvar(nesting, path, name, _key = nil)
        return Types::NIL unless nesting

        result = path.reduce nesting do |mod, n|
          return Types::NIL unless mod.is_a?(Module) && module_own_constant?(mod, n)
          mod.const_get n
        end
        value = result.class_variable_get name if result.is_a?(Module) && name.size >= 3 && result.class_variable_defined?(name)
        Types.type_from_object value
      end

      def [](name)
        @cache[name] ||= (
          value = case RootScope.type_by_name name
          when :ivar
            begin
              Methods::OBJECT_INSTANCE_VARIABLE_GET_METHOD.bind_call(@self_object, name)
            rescue NameError
            end
          when :lvar
            begin
              @binding.local_variable_get(name)
            rescue NameError
            end
          when :gvar
            @binding.eval name if @global_variables.include? name
          end
          Types.type_from_object(value)
        )
      end

      def self_type
        Types.type_from_object @self_object
      end

      def local_variables() = @local_variables.to_a

      def global_variables() = @global_variables.to_a

      def self.type_by_name(name)
        if name.start_with? '@@'
          # "@@cvar" or "@@cvar::[module_id]::[module_path]"
          :cvar
        elsif name.start_with? '@'
          :ivar
        elsif name.start_with? '$'
          :gvar
        elsif name.start_with? '%'
          :internal
        elsif name[0].downcase != name[0] || name[0].match?(/\d/)
          # "ConstName" or "[module_id]::[const_path]"
          :const
        else
          :lvar
        end
      end
    end

    class Scope
      BREAK_RESULT = '%break'
      NEXT_RESULT = '%next'
      RETURN_RESULT = '%return'
      PATTERNMATCH_BREAK = '%match'

      attr_reader :parent, :mergeable_changes, :level, :module_nesting

      def self.from_binding(binding, locals) = new(RootScope.new(binding, binding.receiver, locals))

      def initialize(parent, table = {}, trace_ivar: true, trace_lvar: true, self_type: nil, nesting: nil)
        @parent = parent
        @level = parent.level + 1
        @trace_ivar = trace_ivar
        @trace_lvar = trace_lvar
        @module_nesting = nesting ? [nesting, *parent.module_nesting] : parent.module_nesting
        @self_type = self_type
        @terminated = false
        @jump_branches = []
        @mergeable_changes = @table = table.transform_values { [level, _1] }
      end

      def mutable? = true

      def terminated?
        @terminated
      end

      def terminate_with(type, value)
        return if terminated?
        store_jump type, value, @mergeable_changes
        terminate
      end

      def store_jump(type, value, changes)
        return if terminated?
        if has_own?(type)
          changes[type] = [level, value]
          @jump_branches << changes
        elsif @parent.mutable?
          @parent.store_jump(type, value, changes)
        end
      end

      def terminate
        return if terminated?
        @terminated = true
        @table = @mergeable_changes.dup
      end

      def trace?(name)
        return false unless @parent
        type = RootScope.type_by_name(name)
        type == :ivar ? @trace_ivar : type == :lvar ? @trace_lvar : true
      end

      def level_of(name, var_type)
        case var_type
        when :ivar
          return level unless @trace_ivar
        when :gvar
          return 0
        end
        variable_level, = @table[name]
        variable_level || parent.level_of(name, var_type)
      end

      def get_const(nesting, path, key = nil)
        key ||= [nesting.__id__, path].join('::')
        _l, value = @table[key]
        value || @parent.get_const(nesting, path, key)
      end

      def get_cvar(nesting, path, name, key = nil)
        key ||= [name, nesting.__id__, path].join('::')
        _l, value = @table[key]
        value || @parent.get_cvar(nesting, path, name, key)
      end

      def [](name)
        type = RootScope.type_by_name(name)
        if type == :const
          return get_const(nil, nil, name) || Types::NIL if name.include?('::')

          module_nesting.each do |(nesting, path)|
            value = get_const nesting, [*path, name]
            return value if value
          end
          return Types::NIL
        elsif type == :cvar
          return get_cvar(nil, nil, nil, name) if name.include?('::')

          nesting, path = module_nesting.first
          return get_cvar(nesting, path, name)
        end
        level, value = @table[name]
        if level
          value
        elsif trace? name
          @parent[name]
        elsif type == :ivar
          self_instance_variable_get name
        end
      end

      def set_const(nesting, path, value)
        key = [nesting.__id__, path].join('::')
        @table[key] = [0, value]
      end

      def set_cvar(nesting, path, name, value)
        key = [name, nesting.__id__, path].join('::')
        @table[key] = [0, value]
      end

      def []=(name, value)
        type = RootScope.type_by_name(name)
        if type == :const
          if name.include?('::')
            @table[name] = [0, value]
          else
            parent_module, parent_path = module_nesting.first
            set_const parent_module, [*parent_path, name], value
          end
          return
        elsif type == :cvar
          if name.include?('::')
            @table[name] = [0, value]
          else
            parent_module, parent_path = module_nesting.first
            set_cvar parent_module, parent_path, name, value
          end
          return
        end
        variable_level = level_of name, type
        @table[name] = [variable_level, value] if variable_level
      end

      def self_type
        @self_type || @parent.self_type
      end

      def global_variables
        gvar_keys = @table.keys.select do |name|
          RootScope.type_by_name(name) == :gvar
        end
        gvar_keys | @parent.global_variables
      end

      def local_variables
        lvar_keys = @table.keys.select do |name|
          RootScope.type_by_name(name) == :lvar
        end
        lvar_keys |= @parent.local_variables if @trace_lvar
        lvar_keys
      end

      def table_constants
        constants = module_nesting.flat_map do |mod, path|
          prefix = [mod.__id__, *path].join('::') + '::'
          @table.keys.select { _1.start_with? prefix }.map { _1.delete_prefix(prefix).split('::').first }
        end.uniq
        constants |= @parent.table_constants if @parent.mutable?
        constants
      end

      def table_module_constants(mod)
        prefix = "#{mod.__id__}::"
        constants = @table.keys.select { _1.start_with? prefix }.map { _1.delete_prefix(prefix).split('::').first }
        constants |= @parent.table_constants if @parent.mutable?
        constants
      end

      def base_scope
        @parent.mutable? ? @parent.base_scope : @parent
      end

      def table_instance_variables
        ivars = @table.keys.select { RootScope.type_by_name(_1) == :ivar }
        ivars |= @parent.table_instance_variables if @parent.mutable? && @trace_ivar
        ivars
      end

      def instance_variables
        self_singleton_types = self_type.types.grep(Types::SingletonType)
        singleton_classes = self_type.types.grep(Types::InstanceType).map(&:klass).select(&:singleton_class?)
        base_self = base_scope.self_object
        self_instance_variables = singleton_classes.flat_map do |singleton_class|
          if singleton_class.respond_to? :attached_object
            Methods::OBJECT_INSTANCE_VARIABLES_METHOD.bind_call(singleton_class.attached_object).map(&:to_s)
          elsif singleton_class == Methods::OBJECT_SINGLETON_CLASS_METHOD.bind_call(base_self)
            Methods::OBJECT_INSTANCE_VARIABLES_METHOD.bind_call(base_self).map(&:to_s)
          else
            []
          end
        end
        [
          self_singleton_types.flat_map { _1.module_or_class.instance_variables.map(&:to_s) },
          self_instance_variables || [],
          table_instance_variables
        ].inject(:|)
      end

      def self_instance_variable_get(name)
        self_objects = self_type.types.grep(Types::SingletonType).map(&:module_or_class)
        singleton_classes = self_type.types.grep(Types::InstanceType).map(&:klass).select(&:singleton_class?)
        base_self = base_scope.self_object
        singleton_classes.each do |singleton_class|
          if singleton_class.respond_to? :attached_object
            self_objects << singleton_class.attached_object
          elsif singleton_class == base_self.singleton_class
            self_objects << base_self
          end
        end
        types = self_objects.map do |object|
          value = begin
            Methods::OBJECT_INSTANCE_VARIABLE_GET_METHOD.bind_call(object, name)
          rescue NameError
          end
          Types.type_from_object value
        end
        Types::UnionType[*types]
      end

      def table_class_variables
        cvars = @table.keys.filter_map { _1.split('::', 2).first if RootScope.type_by_name(_1) == :cvar }
        cvars |= @parent.table_class_variables if @parent.mutable?
        cvars
      end

      def class_variables
        cvars = table_class_variables
        m, = module_nesting.first
        cvars |= m.class_variables.map(&:to_s) if m.is_a? Module
        cvars
      end

      def constants
        module_nesting.flat_map do |nest,|
          nest.constants
        end.map(&:to_s) | table_constants
      end

      def merge_jumps
        if terminated?
          @terminated = false
          @table = @mergeable_changes
          merge @jump_branches
          @terminated = true
        else
          merge [*@jump_branches, {}]
        end
      end

      def conditional(&block)
        run_branches(block, ->(_s) {}).first || Types::NIL
      end

      def never(&block)
        block.call Scope.new(self, { BREAK_RESULT => nil, NEXT_RESULT => nil, PATTERNMATCH_BREAK => nil, RETURN_RESULT => nil })
      end

      def run_branches(*blocks)
        results = []
        branches = []
        blocks.each do |block|
          scope = Scope.new self
          result = block.call scope
          next if scope.terminated?
          results << result
          branches << scope.mergeable_changes
        end
        terminate if branches.empty?
        merge branches
        results
      end

      def has_own?(name)
        @table.key? name
      end

      def update(child_scope)
        current_level = level
        child_scope.mergeable_changes.each do |name, (level, value)|
          self[name] = value if level <= current_level
        end
      end

      protected

      def merge(branches)
        current_level = level
        merge = {}
        branches.each do |changes|
          changes.each do |name, (level, value)|
            next if current_level < level
            (merge[name] ||= []) << value
          end
        end
        merge.each do |name, values|
          values << self[name] unless values.size == branches.size
          values.compact!
          self[name] = Types::UnionType[*values.compact] unless values.empty?
        end
      end
    end
  end
end