typeprof 0.15.0 → 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 +261 -66
- data/lib/typeprof/arguments.rb +1 -0
- data/lib/typeprof/builtin.rb +30 -22
- 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 +10 -1
- data/lib/typeprof/export.rb +199 -20
- data/lib/typeprof/import.rb +30 -4
- data/lib/typeprof/iseq.rb +504 -200
- data/lib/typeprof/lsp.rb +865 -0
- data/lib/typeprof/method.rb +17 -13
- 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/smoke/array15.rb +1 -1
- data/smoke/array6.rb +2 -2
- data/smoke/array8.rb +1 -1
- data/smoke/block-args2.rb +3 -3
- data/smoke/block-args3.rb +4 -4
- data/smoke/break2.rb +1 -1
- data/smoke/gvar2.rb +0 -3
- data/smoke/hash-bot.rb +1 -1
- data/smoke/hash4.rb +1 -1
- data/smoke/identifier_keywords.rb +17 -0
- data/smoke/next2.rb +1 -1
- data/smoke/or_raise.rb +18 -0
- data/smoke/parameterizedd-self.rb +2 -2
- data/smoke/pattern-match1.rb +1 -6
- data/smoke/rbs-vars.rb +0 -3
- data/testbed/ao.rb +1 -1
- 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 +20 -5
data/lib/typeprof/iseq.rb
CHANGED
@@ -2,76 +2,127 @@ 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
|
+
)
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def compile(file)
|
19
|
+
compile_core(nil, file)
|
20
|
+
end
|
5
21
|
|
6
|
-
|
22
|
+
def compile_str(str, path = nil)
|
23
|
+
compile_core(str, path)
|
24
|
+
end
|
7
25
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
new(RubyVM::InstructionSequence.compile_file(file, **opt).to_a)
|
16
|
-
end
|
26
|
+
private def compile_core(str, path)
|
27
|
+
opt = RubyVM::InstructionSequence.compile_option
|
28
|
+
opt[:inline_const_cache] = false
|
29
|
+
opt[:peephole_optimization] = false
|
30
|
+
opt[:specialized_instruction] = false
|
31
|
+
opt[:operands_unification] = false
|
32
|
+
opt[:coverage_enabled] = false
|
17
33
|
|
18
|
-
|
19
|
-
|
20
|
-
opt[:inline_const_cache] = false
|
21
|
-
opt[:peephole_optimization] = false
|
22
|
-
opt[:specialized_instruction] = false
|
23
|
-
opt[:operands_unification] = false
|
24
|
-
opt[:coverage_enabled] = false
|
25
|
-
new(RubyVM::InstructionSequence.compile(str, path, **opt).to_a)
|
26
|
-
end
|
34
|
+
parse_opts = {}
|
35
|
+
parse_opts[:keep_script_lines] = true if RICH_AST
|
27
36
|
|
28
|
-
|
37
|
+
if str
|
38
|
+
node = RubyVM::AbstractSyntaxTree.parse(str, **parse_opts)
|
39
|
+
iseq = RubyVM::InstructionSequence.compile(str, path, **opt)
|
40
|
+
else
|
41
|
+
node = RubyVM::AbstractSyntaxTree.parse_file(path, **parse_opts)
|
42
|
+
iseq = RubyVM::InstructionSequence.compile_file(path, **opt)
|
43
|
+
end
|
29
44
|
|
30
|
-
|
31
|
-
|
32
|
-
FRESH_ID[0] += 1
|
45
|
+
node_id2node = {}
|
46
|
+
build_ast_node_id_table(node, node_id2node) if RICH_AST
|
33
47
|
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
37
54
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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)
|
45
62
|
end
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
51
83
|
end
|
52
|
-
|
84
|
+
|
85
|
+
nil
|
53
86
|
end
|
87
|
+
end
|
54
88
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
catch_table.map do |type, iseq, first, last, cont, stack_depth|
|
60
|
-
special_labels[cont] = true if type == :rescue || type == :ensure
|
89
|
+
Insn = Struct.new(:insn, :operands, :lineno, :code_range, :definitions)
|
90
|
+
class Insn
|
91
|
+
def check?(insn_cmp, operands_cmp = nil)
|
92
|
+
return insn == insn_cmp && (!operands_cmp || operands == operands_cmp)
|
61
93
|
end
|
94
|
+
end
|
95
|
+
|
96
|
+
ISEQ_FRESH_ID = [0]
|
97
|
+
|
98
|
+
def initialize(iseq, file_info)
|
99
|
+
file_info.created_iseqs << self
|
100
|
+
|
101
|
+
@id = (ISEQ_FRESH_ID[0] += 1)
|
102
|
+
|
103
|
+
_magic, _major_version, _minor_version, _format_type, misc,
|
104
|
+
@name, @path, @absolute_path, @start_lineno, @type,
|
105
|
+
@locals, @fargs_format, catch_table, insns = *iseq
|
62
106
|
|
63
|
-
|
64
|
-
@
|
107
|
+
fl, fc, ll, lc = misc[:code_location]
|
108
|
+
@iseq_code_range = CodeRange.new(CodeLocation.new(fl, fc), CodeLocation.new(ll, lc))
|
65
109
|
|
66
|
-
|
110
|
+
convert_insns(insns, misc[:node_ids] || [], file_info)
|
67
111
|
|
68
|
-
|
69
|
-
|
112
|
+
add_body_start_marker(insns)
|
113
|
+
|
114
|
+
add_exception_cont_marker(insns, catch_table)
|
115
|
+
|
116
|
+
labels = create_label_table(insns)
|
117
|
+
|
118
|
+
@insns = setup_insns(insns, labels, file_info)
|
119
|
+
|
120
|
+
@fargs_format[:opt] = @fargs_format[:opt].map {|l| labels[l] } if @fargs_format[:opt]
|
70
121
|
|
71
122
|
@catch_table = []
|
72
123
|
catch_table.map do |type, iseq, first, last, cont, stack_depth|
|
73
|
-
iseq = iseq ? ISeq.new(iseq) : nil
|
74
|
-
target = labels[
|
124
|
+
iseq = iseq ? ISeq.new(iseq, file_info) : nil
|
125
|
+
target = labels[cont]
|
75
126
|
entry = [type, iseq, target, stack_depth]
|
76
127
|
labels[first].upto(labels[last]) do |i|
|
77
128
|
@catch_table[i] ||= []
|
@@ -79,90 +130,78 @@ module TypeProf
|
|
79
130
|
end
|
80
131
|
end
|
81
132
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
end
|
90
|
-
|
91
|
-
def setup_iseq(insns, special_labels)
|
92
|
-
i = 0
|
93
|
-
labels = {}
|
94
|
-
ninsns = []
|
95
|
-
insns.each do |e|
|
96
|
-
if e.is_a?(Symbol) && e.to_s.start_with?("label")
|
97
|
-
if special_labels[e]
|
98
|
-
labels[:"#{ e }_special"] = i
|
99
|
-
ninsns << [:nop]
|
100
|
-
i += 1
|
101
|
-
end
|
102
|
-
labels[e] = i
|
103
|
-
else
|
104
|
-
i += 1 if e.is_a?(Array)
|
105
|
-
ninsns << e
|
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
|
106
140
|
end
|
107
141
|
end
|
108
142
|
|
109
|
-
|
110
|
-
|
111
|
-
case e
|
112
|
-
when Integer # lineno
|
113
|
-
lineno = e
|
114
|
-
when Symbol # label or trace
|
115
|
-
nil
|
116
|
-
when Array
|
117
|
-
insn, *operands = e
|
118
|
-
operands = (INSN_TABLE[insn] || []).zip(operands).map do |type, operand|
|
119
|
-
case type
|
120
|
-
when "ISEQ"
|
121
|
-
operand && ISeq.new(operand)
|
122
|
-
when "lindex_t", "rb_num_t", "VALUE", "ID", "GENTRY", "CALL_DATA"
|
123
|
-
operand
|
124
|
-
when "OFFSET"
|
125
|
-
labels[operand] || raise("unknown label: #{ operand }")
|
126
|
-
when "IVC", "ISE"
|
127
|
-
raise unless operand.is_a?(Integer)
|
128
|
-
:_cache_operand
|
129
|
-
else
|
130
|
-
raise "unknown operand type: #{ type }"
|
131
|
-
end
|
132
|
-
end
|
143
|
+
rename_insn_types
|
144
|
+
end
|
133
145
|
|
134
|
-
|
135
|
-
|
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+)/
|
136
151
|
else
|
137
|
-
|
152
|
+
/^def\s+(\w+)/
|
138
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)
|
139
172
|
end
|
173
|
+
end
|
140
174
|
|
141
|
-
|
175
|
+
def source_location(pc)
|
176
|
+
"#{ @path }:#{ @insns[pc].lineno }"
|
177
|
+
end
|
142
178
|
|
143
|
-
|
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
|
144
186
|
end
|
145
187
|
|
146
|
-
def
|
147
|
-
@insns.
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
when :branchunless
|
153
|
-
@insns[i] = [:branch, [:unless] + operands]
|
154
|
-
when :branchnil
|
155
|
-
@insns[i] = [:branch, [:nil] + operands]
|
156
|
-
end
|
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]
|
157
194
|
end
|
158
195
|
end
|
159
196
|
|
160
|
-
def
|
161
|
-
|
197
|
+
def add_def_loc(pc, detailed_loc)
|
198
|
+
if detailed_loc && @insns[pc].definitions
|
199
|
+
@insns[pc].definitions << detailed_loc
|
200
|
+
end
|
162
201
|
end
|
163
202
|
|
164
|
-
attr_reader :name, :path, :absolute_path, :start_lineno, :type, :locals, :fargs_format, :catch_table, :insns
|
165
|
-
attr_reader :id
|
203
|
+
attr_reader :name, :path, :absolute_path, :start_lineno, :type, :locals, :fargs_format, :catch_table, :insns
|
204
|
+
attr_reader :id, :iseq_code_range, :callers
|
166
205
|
|
167
206
|
def pretty_print(q)
|
168
207
|
q.text "ISeq["
|
@@ -196,16 +235,252 @@ module TypeProf
|
|
196
235
|
q.text "]"
|
197
236
|
end
|
198
237
|
|
199
|
-
def
|
238
|
+
def <=>(other)
|
239
|
+
@id <=> other.id
|
240
|
+
end
|
241
|
+
|
242
|
+
# Remove lineno entry and convert instructions to Insn instances
|
243
|
+
def convert_insns(insns, node_ids, file_info)
|
244
|
+
ninsns = []
|
245
|
+
lineno = 0
|
246
|
+
insns.each do |e|
|
247
|
+
case e
|
248
|
+
when Integer # lineno
|
249
|
+
lineno = e
|
250
|
+
when Symbol # label or trace
|
251
|
+
ninsns << e
|
252
|
+
when Array
|
253
|
+
insn, *operands = e
|
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)
|
273
|
+
else
|
274
|
+
raise "unknown iseq entry: #{ e }"
|
275
|
+
end
|
276
|
+
end
|
277
|
+
insns.replace(ninsns)
|
278
|
+
end
|
279
|
+
|
280
|
+
# Insert a dummy instruction "_iseq_body_start"
|
281
|
+
def add_body_start_marker(insns)
|
282
|
+
case @type
|
283
|
+
when :method, :block
|
284
|
+
# skip initialization code of optional arguments
|
285
|
+
if @fargs_format[:opt]
|
286
|
+
label = @fargs_format[:opt].last
|
287
|
+
i = insns.index(label) + 1
|
288
|
+
else
|
289
|
+
i = insns.find_index {|insn| insn.is_a?(Insn) }
|
290
|
+
end
|
291
|
+
|
292
|
+
# skip initialization code of keyword arguments
|
293
|
+
while insns[i][0] == :checkkeyword
|
294
|
+
raise if insns[i + 1].insn != :branchif
|
295
|
+
label = insns[i + 1].operands[0]
|
296
|
+
i = insns.index(label) + 1
|
297
|
+
end
|
298
|
+
|
299
|
+
insns.insert(i, Insn.new(:_iseq_body_start, [], @start_lineno, nil, nil))
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Insert "nop" instruction to continuation point of exception handlers
|
304
|
+
def add_exception_cont_marker(insns, catch_table)
|
305
|
+
# rescue/ensure clauses need to have a dedicated return addresses
|
306
|
+
# because they requires to be virtually called.
|
307
|
+
# So, this preprocess adds "nop" to make a new insn for their return addresses
|
308
|
+
exception_cont_labels = {}
|
309
|
+
catch_table.map! do |type, iseq, first, last, cont, stack_depth|
|
310
|
+
if type == :rescue || type == :ensure
|
311
|
+
exception_cont_labels[cont] = true
|
312
|
+
cont = :"#{ cont }_exception_cont"
|
313
|
+
end
|
314
|
+
[type, iseq, first, last, cont, stack_depth]
|
315
|
+
end
|
316
|
+
|
317
|
+
i = 0
|
318
|
+
while i < insns.size
|
319
|
+
e = insns[i]
|
320
|
+
if exception_cont_labels[e]
|
321
|
+
insns.insert(i, :"#{ e }_exception_cont", Insn.new(:nop, []))
|
322
|
+
i += 2
|
323
|
+
end
|
324
|
+
i += 1
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def create_label_table(insns)
|
329
|
+
pc = 0
|
330
|
+
labels = {}
|
331
|
+
insns.each do |e|
|
332
|
+
if e.is_a?(Symbol)
|
333
|
+
labels[e] = pc
|
334
|
+
else
|
335
|
+
pc += 1
|
336
|
+
end
|
337
|
+
end
|
338
|
+
labels
|
339
|
+
end
|
340
|
+
|
341
|
+
def setup_insns(insns, labels, file_info)
|
342
|
+
ninsns = []
|
343
|
+
insns.each do |e|
|
344
|
+
case e
|
345
|
+
when Symbol # label or trace
|
346
|
+
nil
|
347
|
+
when Insn
|
348
|
+
operands = (INSN_TABLE[e.insn] || []).zip(e.operands).map do |type, operand|
|
349
|
+
case type
|
350
|
+
when "ISEQ"
|
351
|
+
operand && ISeq.new(operand, file_info)
|
352
|
+
when "lindex_t", "rb_num_t", "VALUE", "ID", "GENTRY", "CALL_DATA"
|
353
|
+
operand
|
354
|
+
when "OFFSET"
|
355
|
+
labels[operand] || raise("unknown label: #{ operand }")
|
356
|
+
when "IVC", "ISE"
|
357
|
+
raise unless operand.is_a?(Integer)
|
358
|
+
:_cache_operand
|
359
|
+
else
|
360
|
+
raise "unknown operand type: #{ type }"
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
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)
|
370
|
+
else
|
371
|
+
raise "unknown iseq entry: #{ e }"
|
372
|
+
end
|
373
|
+
end
|
374
|
+
ninsns
|
375
|
+
end
|
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
|
+
|
434
|
+
def rename_insn_types
|
435
|
+
@insns.each do |insn|
|
436
|
+
case insn.insn
|
437
|
+
when :branchif
|
438
|
+
insn.insn, insn.operands = :branch, [:if] + insn.operands
|
439
|
+
when :branchunless
|
440
|
+
insn.insn, insn.operands = :branch, [:unless] + insn.operands
|
441
|
+
when :branchnil
|
442
|
+
insn.insn, insn.operands = :branch, [:nil] + insn.operands
|
443
|
+
when :getblockparam, :getblockparamproxy
|
444
|
+
insn.insn = :getlocal
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
# Unify some instructions for flow-sensitive analysis
|
450
|
+
def unify_instructions
|
451
|
+
# This method rewrites instructions to enable flow-sensitive analysis.
|
452
|
+
#
|
453
|
+
# Consider `if x; ...; else; ... end`.
|
454
|
+
# When the variable `x` is of type "Integer | nil",
|
455
|
+
# we want to make sure that `x` is "Integer" in then clause.
|
456
|
+
# So, we need to split the environment to two ones:
|
457
|
+
# one is that `x` is of type "Integer", and the other is that
|
458
|
+
# `x` is type "nil".
|
459
|
+
#
|
460
|
+
# However, `if x` is compiled to "getlocal; branch".
|
461
|
+
# TypeProf evaluates them as follows:
|
462
|
+
#
|
463
|
+
# * "getlocal" pushes the value of `x` to the stack, amd
|
464
|
+
# * "branch" checks the value on the top of the stack
|
465
|
+
#
|
466
|
+
# TypeProf does not keep where the value comes from, so
|
467
|
+
# it is difficult to split the environment when evaluating "branch".
|
468
|
+
#
|
469
|
+
# This method rewrites "getlocal; branch" to "nop; getlocal_branch".
|
470
|
+
# The two instructions are unified to "getlocal_branch" instruction,
|
471
|
+
# so TypeProf can split the environment.
|
472
|
+
#
|
473
|
+
# This is a very fragile appoach because it highly depends on the compiler of Ruby.
|
474
|
+
|
200
475
|
# gather branch targets
|
201
476
|
# TODO: catch_table should be also considered
|
202
477
|
branch_targets = {}
|
203
|
-
@insns.each do |insn
|
204
|
-
case insn
|
478
|
+
@insns.each do |insn|
|
479
|
+
case insn.insn
|
205
480
|
when :branch
|
206
|
-
branch_targets[operands[1]] = true
|
481
|
+
branch_targets[insn.operands[1]] = true
|
207
482
|
when :jump
|
208
|
-
branch_targets[operands[0]] = true
|
483
|
+
branch_targets[insn.operands[0]] = true
|
209
484
|
end
|
210
485
|
end
|
211
486
|
|
@@ -215,25 +490,27 @@ module TypeProf
|
|
215
490
|
case_branch_list = []
|
216
491
|
if CASE_WHEN_CHECKMATCH
|
217
492
|
(@insns.size - 1).times do |i|
|
218
|
-
|
219
|
-
next unless
|
493
|
+
insn = @insns[i]
|
494
|
+
next unless insn.insn == :getlocal && insn.operands[1] == 0
|
495
|
+
getlocal_operands = insn.operands
|
220
496
|
nops = [i]
|
221
497
|
new_insns = []
|
222
498
|
j = i + 1
|
223
499
|
while true
|
224
|
-
case @insns[j]
|
225
|
-
when
|
226
|
-
break unless @insns[j + 1]
|
227
|
-
break unless @insns[j + 2]
|
228
|
-
break unless @insns[j + 3]
|
229
|
-
break unless @insns[j + 4]
|
230
|
-
break unless @insns[j + 5]
|
231
|
-
target_pc = @insns[j + 5][1]
|
232
|
-
break unless @insns[target_pc]
|
500
|
+
case @insns[j].insn
|
501
|
+
when :dup
|
502
|
+
break unless @insns[j + 1].check?(:putnil, [])
|
503
|
+
break unless @insns[j + 2].check?(:putobject, [true])
|
504
|
+
break unless @insns[j + 3].check?(:getconstant) # TODO: support A::B::C
|
505
|
+
break unless @insns[j + 4].check?(:checkmatch, [2])
|
506
|
+
break unless @insns[j + 5].check?(:branch)
|
507
|
+
target_pc = @insns[j + 5].operands[1]
|
508
|
+
break unless @insns[target_pc].check?(:pop, [])
|
233
509
|
nops << j << (j + 4) << target_pc
|
234
|
-
|
510
|
+
branch_operands = @insns[j + 5][1]
|
511
|
+
new_insns << [j + 5, Insn.new(:getlocal_checkmatch_branch, [getlocal_operands, branch_operands])]
|
235
512
|
j += 6
|
236
|
-
when
|
513
|
+
when :pop
|
237
514
|
nops << j
|
238
515
|
case_branch_list << [nops, new_insns]
|
239
516
|
break
|
@@ -244,25 +521,28 @@ module TypeProf
|
|
244
521
|
end
|
245
522
|
else
|
246
523
|
(@insns.size - 1).times do |i|
|
247
|
-
|
248
|
-
next unless
|
524
|
+
insn = @insns[i]
|
525
|
+
next unless insn.insn == :getlocal && insn.operands[1] == 0
|
526
|
+
getlocal_operands = insn.operands
|
249
527
|
nops = []
|
250
528
|
new_insns = []
|
251
529
|
j = i + 1
|
252
530
|
while true
|
253
|
-
|
254
|
-
|
255
|
-
break unless @insns[j + 1]
|
256
|
-
break unless @insns[j + 2]
|
257
|
-
break unless @insns[j + 3]
|
258
|
-
break unless @insns[j + 4]
|
259
|
-
break unless @insns[j + 5]
|
260
|
-
target_pc = @insns[j + 5][1]
|
261
|
-
break unless @insns[target_pc]
|
531
|
+
insn = @insns[j]
|
532
|
+
if insn.check?(:putnil, [])
|
533
|
+
break unless @insns[j + 1].check?(:putobject, [true])
|
534
|
+
break unless @insns[j + 2].check?(:getconstant) # TODO: support A::B::C
|
535
|
+
break unless @insns[j + 3].check?(:topn, [1])
|
536
|
+
break unless @insns[j + 4].check?(:send) && @insns[j + 4].operands[0].slice(:mid, :flag, :orig_argc) == {:mid=>:===, :flag=>20, :orig_argc=>1}
|
537
|
+
break unless @insns[j + 5].check?(:branch)
|
538
|
+
target_pc = @insns[j + 5].operands[1]
|
539
|
+
break unless @insns[target_pc].check?(:pop, [])
|
262
540
|
nops << (j + 4) #<< target_pc
|
263
|
-
|
541
|
+
send_operands = @insns[j + 4][1]
|
542
|
+
branch_operands = @insns[j + 5][1]
|
543
|
+
new_insns << [j + 5, Insn.new(:arg_getlocal_send_branch, [getlocal_operands, send_operands, branch_operands])]
|
264
544
|
j += 6
|
265
|
-
|
545
|
+
elsif insn.check?(:pop, [])
|
266
546
|
#nops << j
|
267
547
|
case_branch_list << [nops, new_insns]
|
268
548
|
break
|
@@ -273,15 +553,15 @@ module TypeProf
|
|
273
553
|
end
|
274
554
|
end
|
275
555
|
case_branch_list.each do |nops, new_insns|
|
276
|
-
nops.each {|i| @insns[i] =
|
556
|
+
nops.each {|i| @insns[i] = Insn.new(:nop, []) }
|
277
557
|
new_insns.each {|i, insn| @insns[i] = insn }
|
278
558
|
end
|
279
559
|
|
280
560
|
# find a pattern: getlocal(recv), ..., send (is_a?, respond_to?), branch
|
281
561
|
recv_getlocal_send_branch_list = []
|
282
562
|
(@insns.size - 1).times do |i|
|
283
|
-
insn
|
284
|
-
if insn == :getlocal && operands[1] == 0
|
563
|
+
insn = @insns[i]
|
564
|
+
if insn.insn == :getlocal && insn.operands[1] == 0
|
285
565
|
j = i + 1
|
286
566
|
sp = 1
|
287
567
|
while @insns[j]
|
@@ -297,94 +577,118 @@ module TypeProf
|
|
297
577
|
end
|
298
578
|
recv_getlocal_send_branch_list.each do |i, j|
|
299
579
|
next if (i + 1 .. j + 1).any? {|i| branch_targets[i] }
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
@insns[j] =
|
304
|
-
@insns[j + 1] =
|
580
|
+
getlocal_operands = @insns[i].operands
|
581
|
+
send_operands = @insns[j].operands
|
582
|
+
branch_operands = @insns[j + 1].operands
|
583
|
+
@insns[j] = Insn.new(:nop, [])
|
584
|
+
@insns[j + 1] = Insn.new(:recv_getlocal_send_branch, [getlocal_operands, send_operands, branch_operands])
|
305
585
|
end
|
306
586
|
|
307
587
|
# find a pattern: getlocal, send (===), branch
|
308
588
|
arg_getlocal_send_branch_list = []
|
309
589
|
(@insns.size - 1).times do |i|
|
310
|
-
insn1
|
311
|
-
next unless insn1 == :getlocal &&
|
312
|
-
insn2
|
313
|
-
next unless insn2 == :send
|
314
|
-
|
315
|
-
next unless
|
316
|
-
insn3
|
317
|
-
next unless insn3 == :branch
|
590
|
+
insn1 = @insns[i]
|
591
|
+
next unless insn1.insn == :getlocal && insn1.operands[1] == 0
|
592
|
+
insn2 = @insns[i + 1]
|
593
|
+
next unless insn2.insn == :send
|
594
|
+
send_operands = insn2.operands[0]
|
595
|
+
next unless send_operands[:flag] == 16 && send_operands[:orig_argc] == 1
|
596
|
+
insn3 = @insns[i + 2]
|
597
|
+
next unless insn3.insn == :branch
|
318
598
|
arg_getlocal_send_branch_list << i
|
319
599
|
end
|
320
600
|
arg_getlocal_send_branch_list.each do |i|
|
321
601
|
next if (i .. i + 2).any? {|i| branch_targets[i] }
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
@insns[i + 1] =
|
326
|
-
@insns[i + 2] =
|
602
|
+
getlocal_operands = @insns[i].operands
|
603
|
+
send_operands = @insns[i + 1].operands
|
604
|
+
branch_operands = @insns[i + 2].operands
|
605
|
+
@insns[i + 1] = Insn.new(:nop, [])
|
606
|
+
@insns[i + 2] = Insn.new(:arg_getlocal_send_branch, [getlocal_operands, send_operands, branch_operands])
|
327
607
|
end
|
328
608
|
|
329
609
|
# find a pattern: send (block_given?), branch
|
330
610
|
send_branch_list = []
|
331
611
|
(@insns.size - 1).times do |i|
|
332
|
-
insn
|
333
|
-
if insn == :send
|
334
|
-
insn
|
335
|
-
if insn == :branch
|
612
|
+
insn = @insns[i]
|
613
|
+
if insn.insn == :send
|
614
|
+
insn = @insns[i + 1]
|
615
|
+
if insn.insn == :branch
|
336
616
|
send_branch_list << i
|
337
617
|
end
|
338
618
|
end
|
339
619
|
end
|
340
620
|
send_branch_list.each do |i|
|
341
621
|
next if branch_targets[i + 1]
|
342
|
-
|
343
|
-
|
344
|
-
@insns[i] =
|
345
|
-
@insns[i + 1] =
|
622
|
+
send_operands = @insns[i].operands
|
623
|
+
branch_operands = @insns[i + 1].operands
|
624
|
+
@insns[i] = Insn.new(:nop, [])
|
625
|
+
@insns[i + 1] = Insn.new(:send_branch, [send_operands, branch_operands])
|
346
626
|
end
|
347
627
|
|
348
628
|
# find a pattern: getlocal, dup, branch
|
349
629
|
(@insns.size - 2).times do |i|
|
350
630
|
next if branch_targets[i + 1] || branch_targets[i + 2]
|
351
|
-
insn0
|
352
|
-
insn1
|
353
|
-
insn2
|
354
|
-
if insn0 == :getlocal && insn1 == :dup && insn2 == :branch &&
|
355
|
-
|
356
|
-
|
357
|
-
|
631
|
+
insn0 = @insns[i]
|
632
|
+
insn1 = @insns[i + 1]
|
633
|
+
insn2 = @insns[i + 2]
|
634
|
+
if insn0.insn == :getlocal && insn1.insn == :dup && insn2.insn == :branch && insn0.operands[1] == 0
|
635
|
+
getlocal_operands = insn0.operands
|
636
|
+
dup_operands = insn1.operands
|
637
|
+
branch_operands = insn2.operands
|
638
|
+
@insns[i ] = Insn.new(:nop, [])
|
639
|
+
@insns[i + 1] = Insn.new(:nop, [])
|
640
|
+
@insns[i + 2] = Insn.new(:getlocal_dup_branch, [getlocal_operands, dup_operands, branch_operands])
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
# find a pattern: dup, setlocal, branch
|
645
|
+
(@insns.size - 2).times do |i|
|
646
|
+
next if branch_targets[i + 1] || branch_targets[i + 2]
|
647
|
+
insn0 = @insns[i]
|
648
|
+
insn1 = @insns[i + 1]
|
649
|
+
insn2 = @insns[i + 2]
|
650
|
+
if insn0.insn == :dup && insn1.insn == :setlocal && insn2.insn == :branch && insn1.operands[1] == 0
|
651
|
+
dup_operands = insn0.operands
|
652
|
+
setlocal_operands = insn1.operands
|
653
|
+
branch_operands = insn2.operands
|
654
|
+
@insns[i ] = Insn.new(:nop, [])
|
655
|
+
@insns[i + 1] = Insn.new(:nop, [])
|
656
|
+
@insns[i + 2] = Insn.new(:dup_setlocal_branch, [dup_operands, setlocal_operands, branch_operands])
|
358
657
|
end
|
359
658
|
end
|
360
659
|
|
361
660
|
# find a pattern: dup, branch
|
362
661
|
(@insns.size - 1).times do |i|
|
363
662
|
next if branch_targets[i + 1]
|
364
|
-
insn0
|
365
|
-
insn1
|
366
|
-
if insn0 == :dup && insn1 == :branch
|
367
|
-
|
368
|
-
|
663
|
+
insn0 = @insns[i]
|
664
|
+
insn1 = @insns[i + 1]
|
665
|
+
if insn0.insn == :dup && insn1.insn == :branch
|
666
|
+
dup_operands = insn0.operands
|
667
|
+
branch_operands = insn1.operands
|
668
|
+
@insns[i ] = Insn.new(:nop, [])
|
669
|
+
@insns[i + 1] = Insn.new(:dup_branch, [dup_operands, branch_operands])
|
369
670
|
end
|
370
671
|
end
|
371
672
|
|
372
673
|
# find a pattern: getlocal, branch
|
373
674
|
(@insns.size - 1).times do |i|
|
374
675
|
next if branch_targets[i + 1]
|
375
|
-
insn0
|
376
|
-
insn1
|
377
|
-
if
|
378
|
-
|
379
|
-
|
676
|
+
insn0 = @insns[i]
|
677
|
+
insn1 = @insns[i + 1]
|
678
|
+
if insn0.insn == :getlocal && insn0.operands[1] == 0 && insn1.insn == :branch
|
679
|
+
getlocal_operands = insn0.operands
|
680
|
+
branch_operands = insn1.operands
|
681
|
+
@insns[i ] = Insn.new(:nop, [])
|
682
|
+
@insns[i + 1] = Insn.new(:getlocal_branch, [getlocal_operands, branch_operands])
|
380
683
|
end
|
381
684
|
end
|
382
685
|
end
|
383
686
|
|
384
687
|
def check_send_branch(sp, j)
|
385
|
-
insn
|
688
|
+
insn = @insns[j]
|
689
|
+
operands = insn.operands
|
386
690
|
|
387
|
-
case insn
|
691
|
+
case insn.insn
|
388
692
|
when :putspecialobject, :putnil, :putobject, :duparray, :putstring,
|
389
693
|
:putself
|
390
694
|
sp += 1
|
@@ -422,7 +726,7 @@ module TypeProf
|
|
422
726
|
argc += 1 # receiver
|
423
727
|
argc += kw_arg.size if kw_arg
|
424
728
|
sp -= argc
|
425
|
-
return :match if insn == :send && sp == 0 && @insns[j + 1]
|
729
|
+
return :match if insn.insn == :send && sp == 0 && @insns[j + 1].insn == :branch
|
426
730
|
sp += 1
|
427
731
|
when :arg_getlocal_send_branch
|
428
732
|
return # not implemented
|