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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +1 -1
  3. data/Gemfile.lock +4 -4
  4. data/exe/typeprof +5 -1
  5. data/lib/typeprof/analyzer.rb +261 -66
  6. data/lib/typeprof/arguments.rb +1 -0
  7. data/lib/typeprof/builtin.rb +30 -22
  8. data/lib/typeprof/cli.rb +22 -2
  9. data/lib/typeprof/code-range.rb +177 -0
  10. data/lib/typeprof/config.rb +43 -18
  11. data/lib/typeprof/container-type.rb +10 -1
  12. data/lib/typeprof/export.rb +199 -20
  13. data/lib/typeprof/import.rb +30 -4
  14. data/lib/typeprof/iseq.rb +504 -200
  15. data/lib/typeprof/lsp.rb +865 -0
  16. data/lib/typeprof/method.rb +17 -13
  17. data/lib/typeprof/type.rb +46 -38
  18. data/lib/typeprof/utils.rb +18 -1
  19. data/lib/typeprof/version.rb +1 -1
  20. data/lib/typeprof.rb +3 -0
  21. data/smoke/array15.rb +1 -1
  22. data/smoke/array6.rb +2 -2
  23. data/smoke/array8.rb +1 -1
  24. data/smoke/block-args2.rb +3 -3
  25. data/smoke/block-args3.rb +4 -4
  26. data/smoke/break2.rb +1 -1
  27. data/smoke/gvar2.rb +0 -3
  28. data/smoke/hash-bot.rb +1 -1
  29. data/smoke/hash4.rb +1 -1
  30. data/smoke/identifier_keywords.rb +17 -0
  31. data/smoke/next2.rb +1 -1
  32. data/smoke/or_raise.rb +18 -0
  33. data/smoke/parameterizedd-self.rb +2 -2
  34. data/smoke/pattern-match1.rb +1 -6
  35. data/smoke/rbs-vars.rb +0 -3
  36. data/testbed/ao.rb +1 -1
  37. data/typeprof-lsp +3 -0
  38. data/typeprof.gemspec +1 -1
  39. data/vscode/.gitignore +5 -0
  40. data/vscode/.vscode/launch.json +16 -0
  41. data/vscode/.vscodeignore +7 -0
  42. data/vscode/README.md +22 -0
  43. data/vscode/development.md +31 -0
  44. data/vscode/package-lock.json +2211 -0
  45. data/vscode/package.json +71 -0
  46. data/vscode/sandbox/test.rb +24 -0
  47. data/vscode/src/extension.ts +285 -0
  48. data/vscode/tsconfig.json +15 -0
  49. 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
- include Utils::StructuralEquality
22
+ def compile_str(str, path = nil)
23
+ compile_core(str, path)
24
+ end
7
25
 
8
- def self.compile(file)
9
- opt = RubyVM::InstructionSequence.compile_option
10
- opt[:inline_const_cache] = false
11
- opt[:peephole_optimization] = false
12
- opt[:specialized_instruction] = false
13
- opt[:operands_unification] = false
14
- opt[:coverage_enabled] = false
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
- def self.compile_str(str, path = nil)
19
- opt = RubyVM::InstructionSequence.compile_option
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
- FRESH_ID = [0]
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
- def initialize(iseq)
31
- @id = FRESH_ID[0]
32
- FRESH_ID[0] += 1
45
+ node_id2node = {}
46
+ build_ast_node_id_table(node, node_id2node) if RICH_AST
33
47
 
34
- _magic, _major_version, _minor_version, _format_type, _misc,
35
- @name, @path, @absolute_path, @start_lineno, @type,
36
- @locals, @fargs_format, catch_table, insns = *iseq
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
- case @type
39
- when :method, :block
40
- if @fargs_format[:opt]
41
- label = @fargs_format[:opt].last
42
- i = insns.index(label) + 1
43
- else
44
- i = insns.find_index {|insn| insn.is_a?(Array) }
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
- # skip keyword initialization
47
- while insns[i][0] == :checkkeyword
48
- raise if insns[i + 1][0] != :branchif
49
- label = insns[i + 1][1]
50
- i = insns.index(label) + 1
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
- insns[i, 0] = [[:_iseq_body_start]]
84
+
85
+ nil
53
86
  end
87
+ end
54
88
 
55
- # rescue/ensure clauses need to have a dedicated return addresses
56
- # because they requires to be virtually called.
57
- # So, this preprocess adds "nop" to make a new insn for their return addresses
58
- special_labels = {}
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
- @insns = []
64
- @linenos = []
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
- labels = setup_iseq(insns, special_labels)
110
+ convert_insns(insns, misc[:node_ids] || [], file_info)
67
111
 
68
- # checkmatch->branch
69
- # send->branch
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[special_labels[cont] ? :"#{ cont }_special" : cont]
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
- merge_branches
83
-
84
- analyze_stack
85
- end
86
-
87
- def <=>(other)
88
- @id <=> other.id
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
- lineno = 0
110
- ninsns.each do |e|
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
- @insns << [insn, operands]
135
- @linenos << lineno
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
- raise "unknown iseq entry: #{ e }"
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
- @fargs_format[:opt] = @fargs_format[:opt].map {|l| labels[l] } if @fargs_format[:opt]
175
+ def source_location(pc)
176
+ "#{ @path }:#{ @insns[pc].lineno }"
177
+ end
142
178
 
143
- labels
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 merge_branches
147
- @insns.size.times do |i|
148
- insn, operands = @insns[i]
149
- case insn
150
- when :branchif
151
- @insns[i] = [:branch, [:if] + operands]
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 source_location(pc)
161
- "#{ @path }:#{ @linenos[pc] }"
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, :linenos
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 analyze_stack
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, operands|
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
- insn0, getlocal_operands = @insns[i]
219
- next unless [:getlocal, :getblockparam, :getblockparamproxy].include?(insn0) && getlocal_operands[1] == 0
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 [:dup, []]
226
- break unless @insns[j + 1] == [:putnil, []]
227
- break unless @insns[j + 2] == [:putobject, [true]]
228
- break unless @insns[j + 3][0] == :getconstant # TODO: support A::B::C
229
- break unless @insns[j + 4] == [:checkmatch, [2]]
230
- break unless @insns[j + 5][0] == :branch
231
- target_pc = @insns[j + 5][1][1]
232
- break unless @insns[target_pc] == [:pop, []]
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
- new_insns << [j + 5, [:getlocal_checkmatch_branch, [getlocal_operands, @insns[j + 5][1]]]]
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 [:pop, []]
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
- insn0, getlocal_operands = @insns[i]
248
- next unless [:getlocal, :getblockparam, :getblockparamproxy].include?(insn0) && getlocal_operands[1] == 0
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
- case @insns[j]
254
- when [:putnil, []]
255
- break unless @insns[j + 1] == [:putobject, [true]]
256
- break unless @insns[j + 2][0] == :getconstant # TODO: support A::B::C
257
- break unless @insns[j + 3] == [:topn, [1]]
258
- break unless @insns[j + 4] == [:send, [{:mid=>:===, :flag=>20, :orig_argc=>1}, nil]]
259
- break unless @insns[j + 5][0] == :branch
260
- target_pc = @insns[j + 5][1][1]
261
- break unless @insns[target_pc] == [:pop, []]
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
- new_insns << [j + 5, [:arg_getlocal_send_branch, [getlocal_operands, @insns[j + 4][1], @insns[j + 5][1]]]]
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
- when [:pop, []]
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] = [:nop, []] }
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, operands = @insns[i]
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
- _insn, getlocal_operands = @insns[i]
301
- _insn, send_operands = @insns[j]
302
- _insn, branch_operands = @insns[j + 1]
303
- @insns[j] = [:nop]
304
- @insns[j + 1] = [:recv_getlocal_send_branch, [getlocal_operands, send_operands, branch_operands]]
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, operands1 = @insns[i]
311
- next unless insn1 == :getlocal && operands1[1] == 0
312
- insn2, operands2 = @insns[i + 1]
313
- next unless insn2 == :send
314
- send_opt = operands2[0]
315
- next unless send_opt[:flag] == 16 && send_opt[:orig_argc] == 1
316
- insn3, _operands3 = @insns[i + 2]
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
- _insn, getlocal_operands = @insns[i]
323
- _insn, send_operands = @insns[i + 1]
324
- _insn, branch_operands = @insns[i + 2]
325
- @insns[i + 1] = [:nop]
326
- @insns[i + 2] = [:arg_getlocal_send_branch, [getlocal_operands, send_operands, branch_operands]]
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, _operands = @insns[i]
333
- if insn == :send
334
- insn, _operands = @insns[i + 1]
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
- _insn, send_operands = @insns[i]
343
- _insn, branch_operands = @insns[i + 1]
344
- @insns[i] = [:nop]
345
- @insns[i + 1] = [:send_branch, [send_operands, branch_operands]]
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, getlocal_operands = @insns[i]
352
- insn1, dup_operands = @insns[i + 1]
353
- insn2, branch_operands = @insns[i + 2]
354
- if insn0 == :getlocal && insn1 == :dup && insn2 == :branch && getlocal_operands[1] == 0
355
- @insns[i ] = [:nop]
356
- @insns[i + 1] = [:nop]
357
- @insns[i + 2] = [:getlocal_dup_branch, [getlocal_operands, dup_operands, branch_operands]]
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, dup_operands = @insns[i]
365
- insn1, branch_operands = @insns[i + 1]
366
- if insn0 == :dup && insn1 == :branch
367
- @insns[i ] = [:nop]
368
- @insns[i + 1] = [:dup_branch, [dup_operands, branch_operands]]
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, getlocal_operands = @insns[i]
376
- insn1, branch_operands = @insns[i + 1]
377
- if [:getlocal, :getblockparam, :getblockparamproxy].include?(insn0) && getlocal_operands[1] == 0 && insn1 == :branch
378
- @insns[i ] = [:nop]
379
- @insns[i + 1] = [:getlocal_branch, [getlocal_operands, branch_operands]]
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, operands = @insns[j]
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][0] == :branch
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