Skip to content

Commit

Permalink
Add TypeCompletor (YARP+RBS completion)
Browse files Browse the repository at this point in the history
  • Loading branch information
tompng committed Sep 8, 2023
1 parent bb385c8 commit 8168310
Show file tree
Hide file tree
Showing 5 changed files with 2,022 additions and 3 deletions.
23 changes: 20 additions & 3 deletions lib/irb/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,27 @@ def self.retrieve_files_to_require_relative_from_current_dir
end

singleton_class.attr_accessor :completion_analyze_result
singleton_class.attr_reader :completion_method, :completor

def self.completor
require_relative 'completion/regexp_completor'
RegexpCompletor
def self.completion_method=(completion_method)
case completion_method
when :regexp
require_relative 'completion/regexp_completor'
@completor = RegexpCompletor
when :type
require_relative 'completion/type_completor'
@completor = TypeCompletor
else
raise ArgumentError
end
@completion_method = completion_method
end

# TODO: make it configurable
begin
self.completion_method = :type
rescue StandardError, LoadError
self.completion_method = :regexp
end

CompletionProc = lambda { |target, preposing = nil, postposing = nil|
Expand Down
318 changes: 318 additions & 0 deletions lib/irb/completion/scope.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
require 'set'
require_relative 'types'

# TODO: rename module
module KatakataIrb
class BaseScope
BREAK_RESULT = '%break'
NEXT_RESULT = '%next'
RETURN_RESULT = '%return'
PATTERNMATCH_BREAK = '%match'
RAISE_BREAK = '%raise'

attr_reader :module_nesting

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] = KatakataIrb::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) = 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)
result = path.reduce nesting do |mod, name|
return nil unless mod.is_a?(Module) && module_own_constant?(mod, name)
mod.const_get name
end
KatakataIrb::Types.type_from_object result
end

def [](name)
@cache[name] ||= (
fallback = KatakataIrb::Types::NIL
case BaseScope.type_by_name name
when :cvar
BaseScope.type_of(fallback: fallback) { @self_object.class_variable_get name }
when :ivar
BaseScope.type_of(fallback: fallback) { @self_object.instance_variable_get name }
when :lvar
BaseScope.type_of(fallback: fallback) { @binding.local_variable_get(name) }
when :const
BaseScope.type_of(fallback: fallback) { @binding.eval name }
when :gvar
BaseScope.type_of(fallback: fallback) { @binding.eval name if @global_variables.include? name }
end
)
end

def self_type
KatakataIrb::Types.type_from_object @self_object
end

def local_variables() = @local_variables.to_a

def global_variables() = @global_variables.to_a

def self.type_of(fallback: KatakataIrb::Types::OBJECT)
begin
KatakataIrb::Types.type_from_object yield
rescue
fallback
end
end

def self.type_by_name(name)
if name.start_with? '@@'
:cvar
elsif name.start_with? '@'
:ivar
elsif name.start_with? '$'
:gvar
elsif name.start_with? '%'
:internal
elsif name[0].downcase != name[0]
:const
else
:lvar
end
end
end

class Scope < BaseScope
attr_reader :parent, :mergeable_changes, :level, :module_nesting

def self.from_binding(binding, locals) = new(BaseScope.new(binding, binding.eval('self'), locals))

def initialize(parent, table = {}, trace_cvar: true, trace_ivar: true, trace_lvar: true, self_type: nil, nesting: nil)
@parent = parent
@level = parent.level + 1
@trace_cvar = trace_cvar
@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 = BaseScope.type_by_name(name)
type == :cvar ? @trace_cvar : type == :ivar ? @trace_ivar : type == :lvar ? @trace_lvar : true
end

def level_of(name)
variable_level, = @table[name]
variable_level || parent.level_of(name)
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 [](name)
if BaseScope.type_by_name(name) == :const
module_nesting.each do |(nesting, path)|
value = get_const nesting, [*path, name]
return value if value
end
KatakataIrb::Types::NIL
end
level, value = @table[name]
if level
value
elsif trace? name
@parent[name] if trace? name
end
end

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

def []=(name, value)
if BaseScope.type_by_name(name) == :const
parent_module, parent_path = module_nesting.first
set_const parent_module, [*parent_path, name], value
return
end
variable_level = level_of name
@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|
BaseScope.type_by_name(name) == :gvar
end
gvar_keys | @parent.global_variables
end

def local_variables
lvar_keys = @table.keys.select do |name|
BaseScope.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_instance_variables
ivars = @table.keys.select { BaseScope.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(KatakataIrb::Types::SingletonType)
self_singleton_types.flat_map { _1.module_or_class.instance_variables } | table_instance_variables
end

def class_variables
cvars = @table.keys.select { BaseScope.type_by_name(_1) == :cvar }
cvars |= @parent.class_variables if @parent.mutable? && @trace_cvar
unless @trace_cvar
m = module_nesting.first
cvars |= m.class_variables if m.is_a? Module
end
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 || KatakataIrb::Types::NIL
end

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

def run(*args, **option)
scope = Scope.new(self, *args, **option)
yield scope
merge_jumps
update scope
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] = KatakataIrb::Types::UnionType[*values.compact] unless values.empty?
end
end
end
end
Loading

0 comments on commit 8168310

Please sign in to comment.