class IRB::TypeCompletion::RootScope

Attributes

module_nesting[R]
self_object[R]

Public Class Methods

new(binding, self_object, local_variables) click to toggle source
# File irb/type_completion/scope.rb, line 12
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
type_by_name(name) click to toggle source
# File irb/type_completion/scope.rb, line 86
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

Public Instance Methods

[](name) click to toggle source
# File irb/type_completion/scope.rb, line 58
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
get_const(nesting, path, _key = nil) click to toggle source
# File irb/type_completion/scope.rb, line 37
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
get_cvar(nesting, path, name, _key = nil) click to toggle source
# File irb/type_completion/scope.rb, line 47
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
global_variables() click to toggle source
# File irb/type_completion/scope.rb, line 84
  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
level() click to toggle source
# File irb/type_completion/scope.rb, line 26
  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
level_of(_name, _var_type) click to toggle source
# File irb/type_completion/scope.rb, line 28
  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
local_variables() click to toggle source
# File irb/type_completion/scope.rb, line 82
    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
module_own_constant?(mod, name) click to toggle source
# File irb/type_completion/scope.rb, line 32
def module_own_constant?(mod, name)
  set = (@owned_constants_cache[mod] ||= Set.new(mod.constants.map(&:to_s)))
  set.include? name
end
mutable?() click to toggle source
# File irb/type_completion/scope.rb, line 30
  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
self_type() click to toggle source
# File irb/type_completion/scope.rb, line 78
def self_type
  Types.type_from_object @self_object
end