typeprof 0.15.3 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
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
- return new(iseq.to_a)
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, _misc,
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
- convert_insns(insns)
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
- unify_instructions
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
- ninsns << Insn.new(insn, operands, lineno)
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
- ninsns << Insn.new(e.insn, operands, e.lineno)
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, [{:mid=>:===, :flag=>20, :orig_argc=>1}, nil])
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, [])