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.
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, [])