orbacle 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +119 -0
- data/LICENSE +22 -0
- data/Makefile +57 -0
- data/README.md +53 -0
- data/circle.yml +82 -0
- data/exe/orbaclerun +6 -0
- data/index.html +106 -0
- data/lib/orbacle.rb +96 -0
- data/lib/orbacle/ast_utils.rb +35 -0
- data/lib/orbacle/bottom_type.rb +23 -0
- data/lib/orbacle/builder.rb +1414 -0
- data/lib/orbacle/builder/context.rb +71 -0
- data/lib/orbacle/builder/operator_assignment_processors.rb +80 -0
- data/lib/orbacle/class_type.rb +32 -0
- data/lib/orbacle/command_line_interface.rb +107 -0
- data/lib/orbacle/const_name.rb +33 -0
- data/lib/orbacle/const_ref.rb +53 -0
- data/lib/orbacle/constants_tree.rb +73 -0
- data/lib/orbacle/define_builtins.rb +139 -0
- data/lib/orbacle/engine.rb +74 -0
- data/lib/orbacle/find_definition_under_position.rb +76 -0
- data/lib/orbacle/generic_type.rb +35 -0
- data/lib/orbacle/global_tree.rb +280 -0
- data/lib/orbacle/graph.rb +126 -0
- data/lib/orbacle/indexer.rb +151 -0
- data/lib/orbacle/integer_id_generator.rb +13 -0
- data/lib/orbacle/lambda_type.rb +37 -0
- data/lib/orbacle/lang_server.rb +64 -0
- data/lib/orbacle/main_type.rb +23 -0
- data/lib/orbacle/nesting.rb +78 -0
- data/lib/orbacle/node.rb +23 -0
- data/lib/orbacle/nominal_type.rb +32 -0
- data/lib/orbacle/ruby_parser.rb +19 -0
- data/lib/orbacle/scope.rb +63 -0
- data/lib/orbacle/selfie.rb +41 -0
- data/lib/orbacle/type_pretty_printer.rb +24 -0
- data/lib/orbacle/typing_service.rb +816 -0
- data/lib/orbacle/union_type.rb +40 -0
- data/lib/orbacle/uuid_id_generator.rb +11 -0
- data/lib/orbacle/worklist.rb +51 -0
- data/orbacle.gemspec +33 -0
- metadata +258 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rgl/adjacency'
|
4
|
+
|
5
|
+
module Orbacle
|
6
|
+
class Graph
|
7
|
+
ZSuper = Struct.new(:send_result, :block)
|
8
|
+
Yield = Struct.new(:send_args, :send_result)
|
9
|
+
Metod = Struct.new(:args, :result, :yields, :zsupers)
|
10
|
+
Lambda = Struct.new(:args, :result)
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@original = RGL::DirectedAdjacencyGraph.new
|
14
|
+
@reversed = RGL::DirectedAdjacencyGraph.new
|
15
|
+
|
16
|
+
@global_variables = {}
|
17
|
+
@constants = {}
|
18
|
+
@main_ivariables = {}
|
19
|
+
@instance_ivariables = {}
|
20
|
+
@class_ivariables = {}
|
21
|
+
@cvariables = {}
|
22
|
+
@metods = {}
|
23
|
+
@lambdas = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :constants
|
27
|
+
|
28
|
+
def add_vertex(node)
|
29
|
+
raise if node.nil?
|
30
|
+
@original.add_vertex(node)
|
31
|
+
@reversed.add_vertex(node)
|
32
|
+
node
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_edges(nodes_source, nodes_target)
|
36
|
+
Array(nodes_source).each do |source|
|
37
|
+
Array(nodes_target).each do |target|
|
38
|
+
add_edge(source, target)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_edge(x, y)
|
44
|
+
raise if x.nil? || y.nil?
|
45
|
+
@original.add_edge(x, y)
|
46
|
+
@reversed.add_edge(y, x)
|
47
|
+
end
|
48
|
+
|
49
|
+
def edges
|
50
|
+
@original.edges
|
51
|
+
end
|
52
|
+
|
53
|
+
def vertices
|
54
|
+
@original.vertices
|
55
|
+
end
|
56
|
+
|
57
|
+
def adjacent_vertices(v)
|
58
|
+
@original.adjacent_vertices(v)
|
59
|
+
end
|
60
|
+
|
61
|
+
def parent_vertices(v)
|
62
|
+
@reversed.adjacent_vertices(v)
|
63
|
+
end
|
64
|
+
|
65
|
+
def has_edge?(x, y)
|
66
|
+
@original.has_edge?(x, y)
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_gvar_definition_node(gvar_name)
|
70
|
+
global_variables[gvar_name] ||= add_vertex(Node.new(:gvar_definition, {}))
|
71
|
+
return global_variables[gvar_name]
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_main_ivar_definition_node(ivar_name)
|
75
|
+
main_ivariables[ivar_name] ||= add_vertex(Node.new(:ivar_definition, {}))
|
76
|
+
return main_ivariables[ivar_name]
|
77
|
+
end
|
78
|
+
|
79
|
+
def get_constant_definition_node(const_name)
|
80
|
+
constants[const_name] ||= add_vertex(Node.new(:const_definition, {}))
|
81
|
+
return constants[const_name]
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_ivar_definition_node(scope, ivar_name)
|
85
|
+
instance_ivariables[scope.absolute_str] ||= {}
|
86
|
+
instance_ivariables[scope.absolute_str][ivar_name] ||= add_vertex(Node.new(:ivar_definition, {}))
|
87
|
+
return instance_ivariables[scope.absolute_str][ivar_name]
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_class_level_ivar_definition_node(scope, ivar_name)
|
91
|
+
class_ivariables[scope.absolute_str] ||= {}
|
92
|
+
class_ivariables[scope.absolute_str][ivar_name] ||= add_vertex(Node.new(:clivar_definition, {}))
|
93
|
+
return class_ivariables[scope.absolute_str][ivar_name]
|
94
|
+
end
|
95
|
+
|
96
|
+
def get_cvar_definition_node(scope, ivar_name)
|
97
|
+
cvariables[scope.absolute_str] ||= {}
|
98
|
+
cvariables[scope.absolute_str][ivar_name] ||= add_vertex(Node.new(:cvar_definition, {}))
|
99
|
+
return cvariables[scope.absolute_str][ivar_name]
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_metod_nodes(metod_id)
|
103
|
+
return metods[metod_id]
|
104
|
+
end
|
105
|
+
|
106
|
+
def store_metod_nodes(metod_id, arguments_nodes)
|
107
|
+
raise if !arguments_nodes.is_a?(Hash)
|
108
|
+
metods[metod_id] ||= Metod.new(
|
109
|
+
arguments_nodes,
|
110
|
+
add_vertex(Node.new(:method_result, {})),
|
111
|
+
[],
|
112
|
+
[])
|
113
|
+
end
|
114
|
+
|
115
|
+
def get_lambda_nodes(lambda_id)
|
116
|
+
return lambdas[lambda_id]
|
117
|
+
end
|
118
|
+
|
119
|
+
def store_lambda_nodes(lambda_id, arguments_nodes, result_node)
|
120
|
+
lambdas[lambda_id] ||= Lambda.new(arguments_nodes, result_node)
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
attr_reader :global_variables, :main_ivariables, :instance_ivariables, :class_ivariables, :cvariables, :metods, :lambdas
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'thread'
|
5
|
+
require 'benchmark'
|
6
|
+
|
7
|
+
module Orbacle
|
8
|
+
class Indexer
|
9
|
+
QueueElement = Struct.new(:ast, :file_path)
|
10
|
+
class StatsRecorder
|
11
|
+
def initialize
|
12
|
+
@timers = Hash.new(0.0)
|
13
|
+
@counters = Hash.new(0)
|
14
|
+
@values = Hash.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def measure(timer)
|
18
|
+
started_at = Time.now.to_f
|
19
|
+
process_result = yield
|
20
|
+
finished_at = Time.now.to_f
|
21
|
+
process_result
|
22
|
+
rescue Exception => e
|
23
|
+
finished_at ||= Time.now.to_f
|
24
|
+
raise
|
25
|
+
ensure
|
26
|
+
@timers[timer] += finished_at - started_at
|
27
|
+
end
|
28
|
+
|
29
|
+
def all_stats
|
30
|
+
@timers.merge(@counters).merge(@values)
|
31
|
+
end
|
32
|
+
|
33
|
+
def inc(counter_key, by = 1)
|
34
|
+
@counters[counter_key] += by
|
35
|
+
end
|
36
|
+
|
37
|
+
def counter(counter_key)
|
38
|
+
@counters[counter_key]
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_value(key, value)
|
42
|
+
@values[key] = value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class ReadingProcess
|
47
|
+
def initialize(logger, queue, files)
|
48
|
+
@logger = logger
|
49
|
+
@queue = queue
|
50
|
+
@files = files
|
51
|
+
end
|
52
|
+
|
53
|
+
def call
|
54
|
+
@files.each do |file_path|
|
55
|
+
file_content = File.read(file_path)
|
56
|
+
@queue.push(QueueElement.new(file_content, file_path))
|
57
|
+
end
|
58
|
+
@queue.close
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
attr_reader :logger
|
63
|
+
end
|
64
|
+
|
65
|
+
class ParsingProcess
|
66
|
+
def initialize(logger, queue_contents, queue_asts)
|
67
|
+
@logger = logger
|
68
|
+
@queue_contents = queue_contents
|
69
|
+
@queue_asts = queue_asts
|
70
|
+
end
|
71
|
+
|
72
|
+
def call
|
73
|
+
parser = RubyParser.new
|
74
|
+
while !@queue_contents.closed? || !@queue_contents.empty?
|
75
|
+
element = @queue_contents.shift
|
76
|
+
begin
|
77
|
+
ast = parser.parse(element.ast)
|
78
|
+
@queue_asts.push(QueueElement.new(ast, element.file_path))
|
79
|
+
rescue RubyParser::Error => e
|
80
|
+
logger.warn "Warning: Skipped #{element.file_path} because of #{e}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
@queue_asts.close
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
attr_reader :logger
|
88
|
+
end
|
89
|
+
|
90
|
+
class BuildingProcess
|
91
|
+
def initialize(queue, builder)
|
92
|
+
@queue = queue
|
93
|
+
@builder = builder
|
94
|
+
end
|
95
|
+
|
96
|
+
def call
|
97
|
+
while !@queue.closed? || !@queue.empty?
|
98
|
+
element = @queue.shift
|
99
|
+
@builder.process_file(element.ast, element.file_path)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def initialize(logger, stats)
|
105
|
+
@logger = logger
|
106
|
+
@stats = stats
|
107
|
+
end
|
108
|
+
|
109
|
+
def call(project_root:)
|
110
|
+
project_root_path = Pathname.new(project_root)
|
111
|
+
|
112
|
+
files = Dir.glob("#{project_root_path}/**/*.rb")
|
113
|
+
id_generator = IntegerIdGenerator.new
|
114
|
+
worklist = Worklist.new
|
115
|
+
state = GlobalTree.new(id_generator)
|
116
|
+
graph = Graph.new
|
117
|
+
DefineBuiltins.new(graph, state, id_generator).()
|
118
|
+
@parser = Builder.new(graph, worklist, state, id_generator)
|
119
|
+
|
120
|
+
queue_contents = Queue.new
|
121
|
+
queue_asts = Queue.new
|
122
|
+
|
123
|
+
logger.info "Reading..."
|
124
|
+
reading_process = ReadingProcess.new(logger, queue_contents, files)
|
125
|
+
@stats.measure(:reading) { reading_process.call() }
|
126
|
+
|
127
|
+
logger.info "Parsing..."
|
128
|
+
parsing_process = ParsingProcess.new(logger, queue_contents, queue_asts)
|
129
|
+
@stats.measure(:parsing) { parsing_process.call() }
|
130
|
+
|
131
|
+
logger.info "Building graph..."
|
132
|
+
building_process = BuildingProcess.new(queue_asts, @parser)
|
133
|
+
@stats.measure(:building) { building_process.call() }
|
134
|
+
|
135
|
+
logger.info "Typing..."
|
136
|
+
typing_service = TypingService.new(logger, @stats)
|
137
|
+
@stats.measure(:typing) { typing_service.(graph, worklist, state) }
|
138
|
+
|
139
|
+
type_mapping = state.instance_variable_get(:@type_mapping)
|
140
|
+
@stats.set_value(:typed_nodes_all, type_mapping.size)
|
141
|
+
@stats.set_value(:typed_nodes_not_bottom, type_mapping.count {|k,v| !v.bottom? })
|
142
|
+
@stats.set_value(:typed_nodes_call_result, type_mapping.count {|k,v| k.type == :call_result })
|
143
|
+
@stats.set_value(:typed_nodes_call_result_not_bottom, type_mapping.count {|k,v| k.type == :call_result && !v.bottom? })
|
144
|
+
|
145
|
+
return state, graph, worklist
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
attr_reader :logger
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Orbacle
|
4
|
+
class ProcType
|
5
|
+
def initialize(lambda_id)
|
6
|
+
@lambda_id = lambda_id
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :lambda_id
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
self.class == other.class &&
|
13
|
+
self.lambda_id == other.lambda_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def hash
|
17
|
+
[
|
18
|
+
self.class,
|
19
|
+
self.lambda_id,
|
20
|
+
].hash ^ BIG_VALUE
|
21
|
+
end
|
22
|
+
alias eql? ==
|
23
|
+
|
24
|
+
def each_possible_type
|
25
|
+
yield self
|
26
|
+
end
|
27
|
+
|
28
|
+
def bottom?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def name
|
33
|
+
"Proc"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lsp'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Orbacle
|
7
|
+
class LangServer
|
8
|
+
include Lsp::LanguageServer
|
9
|
+
|
10
|
+
def initialize(logger, engine)
|
11
|
+
@logger = logger
|
12
|
+
@engine = engine
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :logger, :engine
|
16
|
+
|
17
|
+
def handle_initialize(request)
|
18
|
+
root_path = request.root_uri.path
|
19
|
+
logger.info("Initializing at #{root_path.inspect}")
|
20
|
+
engine.index(root_path)
|
21
|
+
Lsp::ResponseMessage.successful(nil)
|
22
|
+
end
|
23
|
+
|
24
|
+
def handle_text_document_hover(request)
|
25
|
+
log_errors do
|
26
|
+
filepath = request.text_document.uri.path
|
27
|
+
pretty_type = engine.get_type_information(filepath, Position.new(request.position.line, request.position.character))
|
28
|
+
Lsp::ResponseMessage.successful(
|
29
|
+
Lsp::TextDocumentHoverResult.new(
|
30
|
+
"Type of that expression: #{pretty_type}"))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def handle_text_document_definition(request)
|
35
|
+
log_errors do
|
36
|
+
file_path = request.text_document.uri.path
|
37
|
+
file_content = File.read(file_path)
|
38
|
+
locations = engine.locations_for_definition_under_position(file_path, file_content, Position.new(request.position.line, request.position.character))
|
39
|
+
if locations
|
40
|
+
Lsp::ResponseMessage.successful(locations.map(&method(:location_to_lsp_location)))
|
41
|
+
else
|
42
|
+
Lsp::ResponseMessage.successful(nil)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def log_errors
|
48
|
+
begin
|
49
|
+
yield
|
50
|
+
rescue => e
|
51
|
+
logger.error(e)
|
52
|
+
raise
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def location_to_lsp_location(location)
|
57
|
+
Lsp::Location.new(
|
58
|
+
URI("file://#{location.uri}"),
|
59
|
+
Lsp::Range.new(
|
60
|
+
Lsp::Position.new(location.start.line, location.start.character),
|
61
|
+
Lsp::Position.new(location.end.line, location.end.character)))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Orbacle
|
4
|
+
class MainType
|
5
|
+
def each_possible_type
|
6
|
+
end
|
7
|
+
|
8
|
+
def ==(other)
|
9
|
+
self.class == other.class
|
10
|
+
end
|
11
|
+
|
12
|
+
def hash
|
13
|
+
[
|
14
|
+
self.class,
|
15
|
+
].hash ^ BIG_VALUE
|
16
|
+
end
|
17
|
+
alias eql? ==
|
18
|
+
|
19
|
+
def bottom?
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Orbacle
|
4
|
+
class Nesting
|
5
|
+
class ConstLevel < Struct.new(:const_ref)
|
6
|
+
def full_name
|
7
|
+
const_ref.full_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def absolute?
|
11
|
+
const_ref.absolute?
|
12
|
+
end
|
13
|
+
|
14
|
+
def eigenclass?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ClassConstLevel < Struct.new(:scope)
|
20
|
+
def full_name
|
21
|
+
scope.absolute_str
|
22
|
+
end
|
23
|
+
|
24
|
+
def absolute?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def eigenclass?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.empty
|
34
|
+
new([])
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(levels)
|
38
|
+
@levels = levels
|
39
|
+
end
|
40
|
+
|
41
|
+
def ==(other)
|
42
|
+
levels.size == other.levels.size &&
|
43
|
+
levels.zip(other.levels).all? {|l1, l2| l1 == l2 }
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :levels
|
47
|
+
|
48
|
+
def to_primitive
|
49
|
+
levels.map {|level| level.full_name }
|
50
|
+
end
|
51
|
+
|
52
|
+
def increase_nesting_const(const_ref)
|
53
|
+
Nesting.new(levels + [ConstLevel.new(const_ref)])
|
54
|
+
end
|
55
|
+
|
56
|
+
def increase_nesting_self
|
57
|
+
Nesting.new(levels + [ClassConstLevel.new(to_scope)])
|
58
|
+
end
|
59
|
+
|
60
|
+
def decrease_nesting
|
61
|
+
Nesting.new(levels[0..-2])
|
62
|
+
end
|
63
|
+
|
64
|
+
def empty?
|
65
|
+
levels.empty?
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_scope
|
69
|
+
levels.inject(Scope.empty) do |scope, nesting_level|
|
70
|
+
if nesting_level.eigenclass?
|
71
|
+
Scope.new(nesting_level.full_name.split("::").reject(&:empty?), nesting_level.eigenclass?)
|
72
|
+
else
|
73
|
+
scope.increase_by_ref(nesting_level.const_ref)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|