typeprof 0.15.3 → 0.20.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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +1 -1
- data/Gemfile.lock +4 -4
- data/exe/typeprof +5 -1
- data/lib/typeprof/analyzer.rb +228 -54
- data/lib/typeprof/arguments.rb +1 -0
- data/lib/typeprof/builtin.rb +23 -23
- data/lib/typeprof/cli.rb +22 -2
- data/lib/typeprof/code-range.rb +177 -0
- data/lib/typeprof/config.rb +43 -18
- data/lib/typeprof/container-type.rb +3 -0
- data/lib/typeprof/export.rb +191 -15
- data/lib/typeprof/import.rb +25 -4
- data/lib/typeprof/iseq.rb +218 -16
- data/lib/typeprof/lsp.rb +865 -0
- data/lib/typeprof/method.rb +15 -11
- data/lib/typeprof/type.rb +46 -38
- data/lib/typeprof/utils.rb +18 -1
- data/lib/typeprof/version.rb +1 -1
- data/lib/typeprof.rb +3 -0
- data/typeprof-lsp +3 -0
- data/typeprof.gemspec +1 -1
- data/vscode/.gitignore +5 -0
- data/vscode/.vscode/launch.json +16 -0
- data/vscode/.vscodeignore +7 -0
- data/vscode/README.md +22 -0
- data/vscode/development.md +31 -0
- data/vscode/package-lock.json +2211 -0
- data/vscode/package.json +71 -0
- data/vscode/sandbox/test.rb +24 -0
- data/vscode/src/extension.ts +285 -0
- data/vscode/tsconfig.json +15 -0
- metadata +18 -5
data/lib/typeprof/iseq.rb
CHANGED
@@ -2,6 +2,17 @@ module TypeProf
|
|
2
2
|
class ISeq
|
3
3
|
# https://github.com/ruby/ruby/pull/4468
|
4
4
|
CASE_WHEN_CHECKMATCH = RubyVM::InstructionSequence.compile("case 1; when Integer; end").to_a.last.any? {|insn,| insn == :checkmatch }
|
5
|
+
# https://github.com/ruby/ruby/blob/v3_0_2/vm_core.h#L1206
|
6
|
+
VM_ENV_DATA_SIZE = 3
|
7
|
+
# Check if Ruby 3.1 or later
|
8
|
+
RICH_AST = begin RubyVM::AbstractSyntaxTree.parse("1", keep_script_lines: true).node_id; true; rescue; false; end
|
9
|
+
|
10
|
+
FileInfo = Struct.new(
|
11
|
+
:node_id2node,
|
12
|
+
:definition_table,
|
13
|
+
:caller_table,
|
14
|
+
:created_iseqs,
|
15
|
+
)
|
5
16
|
|
6
17
|
class << self
|
7
18
|
def compile(file)
|
@@ -20,17 +31,62 @@ module TypeProf
|
|
20
31
|
opt[:operands_unification] = false
|
21
32
|
opt[:coverage_enabled] = false
|
22
33
|
|
34
|
+
parse_opts = {}
|
35
|
+
parse_opts[:keep_script_lines] = true if RICH_AST
|
36
|
+
|
23
37
|
if str
|
38
|
+
node = RubyVM::AbstractSyntaxTree.parse(str, **parse_opts)
|
24
39
|
iseq = RubyVM::InstructionSequence.compile(str, path, **opt)
|
25
40
|
else
|
41
|
+
node = RubyVM::AbstractSyntaxTree.parse_file(path, **parse_opts)
|
26
42
|
iseq = RubyVM::InstructionSequence.compile_file(path, **opt)
|
27
43
|
end
|
28
44
|
|
29
|
-
|
45
|
+
node_id2node = {}
|
46
|
+
build_ast_node_id_table(node, node_id2node) if RICH_AST
|
47
|
+
|
48
|
+
file_info = FileInfo.new(node_id2node, CodeRangeTable.new, CodeRangeTable.new, [])
|
49
|
+
iseq_rb = new(iseq.to_a, file_info)
|
50
|
+
iseq_rb.collect_local_variable_info(file_info) if RICH_AST
|
51
|
+
file_info.created_iseqs.each do |iseq|
|
52
|
+
iseq.unify_instructions
|
53
|
+
end
|
54
|
+
|
55
|
+
return iseq_rb, file_info.definition_table, file_info.caller_table
|
56
|
+
end
|
57
|
+
|
58
|
+
private def build_ast_node_id_table(node, tbl = {})
|
59
|
+
tbl[node.node_id] = node
|
60
|
+
node.children.each do |child|
|
61
|
+
build_ast_node_id_table(child, tbl) if child.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
62
|
+
end
|
63
|
+
tbl
|
64
|
+
end
|
65
|
+
|
66
|
+
def code_range_from_node(node)
|
67
|
+
CodeRange.new(
|
68
|
+
CodeLocation.new(node.first_lineno, node.first_column),
|
69
|
+
CodeLocation.new(node.last_lineno, node.last_column),
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_node_by_id(node, id)
|
74
|
+
node = RubyVM::AbstractSyntaxTree.parse(node) if node.is_a?(String)
|
75
|
+
|
76
|
+
return node if id == node.node_id
|
77
|
+
|
78
|
+
node.children.each do |child|
|
79
|
+
if child.is_a?(RubyVM::AbstractSyntaxTree::Node)
|
80
|
+
ret = find_node_by_id(child, id)
|
81
|
+
return ret if ret
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
nil
|
30
86
|
end
|
31
87
|
end
|
32
88
|
|
33
|
-
Insn = Struct.new(:insn, :operands, :lineno)
|
89
|
+
Insn = Struct.new(:insn, :operands, :lineno, :code_range, :definitions)
|
34
90
|
class Insn
|
35
91
|
def check?(insn_cmp, operands_cmp = nil)
|
36
92
|
return insn == insn_cmp && (!operands_cmp || operands == operands_cmp)
|
@@ -39,14 +95,19 @@ module TypeProf
|
|
39
95
|
|
40
96
|
ISEQ_FRESH_ID = [0]
|
41
97
|
|
42
|
-
def initialize(iseq)
|
98
|
+
def initialize(iseq, file_info)
|
99
|
+
file_info.created_iseqs << self
|
100
|
+
|
43
101
|
@id = (ISEQ_FRESH_ID[0] += 1)
|
44
102
|
|
45
|
-
_magic, _major_version, _minor_version, _format_type,
|
103
|
+
_magic, _major_version, _minor_version, _format_type, misc,
|
46
104
|
@name, @path, @absolute_path, @start_lineno, @type,
|
47
105
|
@locals, @fargs_format, catch_table, insns = *iseq
|
48
106
|
|
49
|
-
|
107
|
+
fl, fc, ll, lc = misc[:code_location]
|
108
|
+
@iseq_code_range = CodeRange.new(CodeLocation.new(fl, fc), CodeLocation.new(ll, lc))
|
109
|
+
|
110
|
+
convert_insns(insns, misc[:node_ids] || [], file_info)
|
50
111
|
|
51
112
|
add_body_start_marker(insns)
|
52
113
|
|
@@ -54,13 +115,13 @@ module TypeProf
|
|
54
115
|
|
55
116
|
labels = create_label_table(insns)
|
56
117
|
|
57
|
-
@insns = setup_insns(insns, labels)
|
118
|
+
@insns = setup_insns(insns, labels, file_info)
|
58
119
|
|
59
120
|
@fargs_format[:opt] = @fargs_format[:opt].map {|l| labels[l] } if @fargs_format[:opt]
|
60
121
|
|
61
122
|
@catch_table = []
|
62
123
|
catch_table.map do |type, iseq, first, last, cont, stack_depth|
|
63
|
-
iseq = iseq ? ISeq.new(iseq) : nil
|
124
|
+
iseq = iseq ? ISeq.new(iseq, file_info) : nil
|
64
125
|
target = labels[cont]
|
65
126
|
entry = [type, iseq, target, stack_depth]
|
66
127
|
labels[first].upto(labels[last]) do |i|
|
@@ -69,17 +130,78 @@ module TypeProf
|
|
69
130
|
end
|
70
131
|
end
|
71
132
|
|
133
|
+
def_node_id = misc[:def_node_id]
|
134
|
+
if def_node_id && file_info.node_id2node[def_node_id] && (@type == :method || @type == :block)
|
135
|
+
def_node = file_info.node_id2node[def_node_id]
|
136
|
+
method_name_token_range = extract_method_name_token_range(def_node)
|
137
|
+
if method_name_token_range
|
138
|
+
@callers = Utils::MutableSet.new
|
139
|
+
file_info.caller_table[method_name_token_range] = @callers
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
72
143
|
rename_insn_types
|
144
|
+
end
|
73
145
|
|
74
|
-
|
146
|
+
def extract_method_name_token_range(node)
|
147
|
+
case @type
|
148
|
+
when :method
|
149
|
+
regex = if node.type == :DEFS
|
150
|
+
/^def\s+(?:\w+)\s*\.\s*(\w+)/
|
151
|
+
else
|
152
|
+
/^def\s+(\w+)/
|
153
|
+
end
|
154
|
+
return nil unless node.source =~ regex
|
155
|
+
zero_loc = CodeLocation.new(1, 0)
|
156
|
+
name_start = $~.begin(1)
|
157
|
+
name_length = $~.end(1) - name_start
|
158
|
+
name_head_loc = zero_loc.advance_cursor(name_start, node.source)
|
159
|
+
name_tail_loc = name_head_loc.advance_cursor(name_length, node.source)
|
160
|
+
return CodeRange.new(
|
161
|
+
CodeLocation.new(
|
162
|
+
node.first_lineno + (name_head_loc.lineno - 1),
|
163
|
+
name_head_loc.lineno == 1 ? node.first_column + name_head_loc.column : name_head_loc.column
|
164
|
+
),
|
165
|
+
CodeLocation.new(
|
166
|
+
node.first_lineno + (name_tail_loc.lineno - 1),
|
167
|
+
name_tail_loc.lineno == 1 ? node.first_column + name_tail_loc.column : name_tail_loc.column
|
168
|
+
),
|
169
|
+
)
|
170
|
+
when :block
|
171
|
+
return ISeq.code_range_from_node(node)
|
172
|
+
end
|
75
173
|
end
|
76
174
|
|
77
175
|
def source_location(pc)
|
78
176
|
"#{ @path }:#{ @insns[pc].lineno }"
|
79
177
|
end
|
80
178
|
|
179
|
+
def detailed_source_location(pc)
|
180
|
+
code_range = @insns[pc].code_range
|
181
|
+
if code_range
|
182
|
+
[@path, code_range]
|
183
|
+
else
|
184
|
+
[@path]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def add_called_iseq(pc, callee_iseq)
|
189
|
+
if callee_iseq && @insns[pc].definitions
|
190
|
+
@insns[pc].definitions << [callee_iseq.path, callee_iseq.iseq_code_range]
|
191
|
+
end
|
192
|
+
if callee_iseq.callers
|
193
|
+
callee_iseq.callers << [@path, @insns[pc].code_range]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def add_def_loc(pc, detailed_loc)
|
198
|
+
if detailed_loc && @insns[pc].definitions
|
199
|
+
@insns[pc].definitions << detailed_loc
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
81
203
|
attr_reader :name, :path, :absolute_path, :start_lineno, :type, :locals, :fargs_format, :catch_table, :insns
|
82
|
-
attr_reader :id
|
204
|
+
attr_reader :id, :iseq_code_range, :callers
|
83
205
|
|
84
206
|
def pretty_print(q)
|
85
207
|
q.text "ISeq["
|
@@ -118,7 +240,7 @@ module TypeProf
|
|
118
240
|
end
|
119
241
|
|
120
242
|
# Remove lineno entry and convert instructions to Insn instances
|
121
|
-
def convert_insns(insns)
|
243
|
+
def convert_insns(insns, node_ids, file_info)
|
122
244
|
ninsns = []
|
123
245
|
lineno = 0
|
124
246
|
insns.each do |e|
|
@@ -129,7 +251,25 @@ module TypeProf
|
|
129
251
|
ninsns << e
|
130
252
|
when Array
|
131
253
|
insn, *operands = e
|
132
|
-
|
254
|
+
node_id = node_ids.shift
|
255
|
+
node = file_info.node_id2node[node_id]
|
256
|
+
if node
|
257
|
+
code_range = ISeq.code_range_from_node(node)
|
258
|
+
case insn
|
259
|
+
when :send, :invokesuper
|
260
|
+
opt, blk_iseq = operands
|
261
|
+
opt[:node_id] = node_id
|
262
|
+
if blk_iseq
|
263
|
+
misc = blk_iseq[4] # iseq's "misc" field
|
264
|
+
misc[:def_node_id] = node_id
|
265
|
+
end
|
266
|
+
when :definemethod, :definesmethod
|
267
|
+
iseq = operands[1]
|
268
|
+
misc = iseq[4] # iseq's "misc" field
|
269
|
+
misc[:def_node_id] = node_id
|
270
|
+
end
|
271
|
+
end
|
272
|
+
ninsns << Insn.new(insn, operands, lineno, code_range, nil)
|
133
273
|
else
|
134
274
|
raise "unknown iseq entry: #{ e }"
|
135
275
|
end
|
@@ -156,7 +296,7 @@ module TypeProf
|
|
156
296
|
i = insns.index(label) + 1
|
157
297
|
end
|
158
298
|
|
159
|
-
insns.insert(i, Insn.new(:_iseq_body_start, [], @start_lineno))
|
299
|
+
insns.insert(i, Insn.new(:_iseq_body_start, [], @start_lineno, nil, nil))
|
160
300
|
end
|
161
301
|
end
|
162
302
|
|
@@ -198,7 +338,7 @@ module TypeProf
|
|
198
338
|
labels
|
199
339
|
end
|
200
340
|
|
201
|
-
def setup_insns(insns, labels)
|
341
|
+
def setup_insns(insns, labels, file_info)
|
202
342
|
ninsns = []
|
203
343
|
insns.each do |e|
|
204
344
|
case e
|
@@ -208,7 +348,7 @@ module TypeProf
|
|
208
348
|
operands = (INSN_TABLE[e.insn] || []).zip(e.operands).map do |type, operand|
|
209
349
|
case type
|
210
350
|
when "ISEQ"
|
211
|
-
operand && ISeq.new(operand)
|
351
|
+
operand && ISeq.new(operand, file_info)
|
212
352
|
when "lindex_t", "rb_num_t", "VALUE", "ID", "GENTRY", "CALL_DATA"
|
213
353
|
operand
|
214
354
|
when "OFFSET"
|
@@ -221,7 +361,12 @@ module TypeProf
|
|
221
361
|
end
|
222
362
|
end
|
223
363
|
|
224
|
-
|
364
|
+
if e.code_range && should_collect_defs(e.insn)
|
365
|
+
definition = Utils::MutableSet.new
|
366
|
+
file_info.definition_table[e.code_range] = definition
|
367
|
+
end
|
368
|
+
|
369
|
+
ninsns << Insn.new(e.insn, operands, e.lineno, e.code_range, definition)
|
225
370
|
else
|
226
371
|
raise "unknown iseq entry: #{ e }"
|
227
372
|
end
|
@@ -229,6 +374,63 @@ module TypeProf
|
|
229
374
|
ninsns
|
230
375
|
end
|
231
376
|
|
377
|
+
def should_collect_defs(insn_kind)
|
378
|
+
case insn_kind
|
379
|
+
when :send, :getinstancevariable, :getconstant
|
380
|
+
return true
|
381
|
+
else
|
382
|
+
return false
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
# Collect local variable use and definition info recursively
|
387
|
+
def collect_local_variable_info(file_info, absolute_level = 0, parent_variable_tables = {})
|
388
|
+
# e.g.
|
389
|
+
# variable_tables[abs_level][idx] = [[path, code_range]]
|
390
|
+
current_variables = []
|
391
|
+
variable_tables = parent_variable_tables.merge({
|
392
|
+
absolute_level => current_variables
|
393
|
+
})
|
394
|
+
|
395
|
+
dummy_def_range = CodeRange.new(
|
396
|
+
CodeLocation.new(@start_lineno, 0),
|
397
|
+
CodeLocation.new(@start_lineno, 1),
|
398
|
+
)
|
399
|
+
# Fill tail elements with parameters
|
400
|
+
(@fargs_format[:lead_num] || 0).times do |offset|
|
401
|
+
current_variables[VM_ENV_DATA_SIZE + @locals.length - offset - 1] ||= Utils::MutableSet.new
|
402
|
+
current_variables[VM_ENV_DATA_SIZE + @locals.length - offset - 1] << [@path, dummy_def_range]
|
403
|
+
end
|
404
|
+
|
405
|
+
@insns.each do |insn|
|
406
|
+
next unless insn.insn == :getlocal || insn.insn == :setlocal
|
407
|
+
|
408
|
+
idx = insn.operands[0]
|
409
|
+
# note: level is relative value to the current level
|
410
|
+
level = insn.operands[1]
|
411
|
+
target_abs_level = absolute_level - level
|
412
|
+
variable_tables[target_abs_level] ||= {}
|
413
|
+
variable_tables[target_abs_level][idx] ||= Utils::MutableSet.new
|
414
|
+
|
415
|
+
case insn.insn
|
416
|
+
when :setlocal
|
417
|
+
variable_tables[target_abs_level][idx] << [path, insn.code_range]
|
418
|
+
when :getlocal
|
419
|
+
file_info.definition_table[insn.code_range] = variable_tables[target_abs_level][idx]
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
@insns.each do |insn|
|
424
|
+
insn.operands.each do |operand|
|
425
|
+
next unless operand.is_a?(ISeq)
|
426
|
+
operand.collect_local_variable_info(
|
427
|
+
file_info, absolute_level + 1,
|
428
|
+
variable_tables
|
429
|
+
)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
232
434
|
def rename_insn_types
|
233
435
|
@insns.each do |insn|
|
234
436
|
case insn.insn
|
@@ -331,7 +533,7 @@ module TypeProf
|
|
331
533
|
break unless @insns[j + 1].check?(:putobject, [true])
|
332
534
|
break unless @insns[j + 2].check?(:getconstant) # TODO: support A::B::C
|
333
535
|
break unless @insns[j + 3].check?(:topn, [1])
|
334
|
-
break unless @insns[j + 4].check?(:send,
|
536
|
+
break unless @insns[j + 4].check?(:send) && @insns[j + 4].operands[0].slice(:mid, :flag, :orig_argc) == {:mid=>:===, :flag=>20, :orig_argc=>1}
|
335
537
|
break unless @insns[j + 5].check?(:branch)
|
336
538
|
target_pc = @insns[j + 5].operands[1]
|
337
539
|
break unless @insns[target_pc].check?(:pop, [])
|