typeprof 0.14.1 → 0.15.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +5 -5
  3. data/lib/typeprof/analyzer.rb +154 -85
  4. data/lib/typeprof/builtin.rb +22 -12
  5. data/lib/typeprof/cli.rb +1 -0
  6. data/lib/typeprof/config.rb +2 -1
  7. data/lib/typeprof/container-type.rb +7 -1
  8. data/lib/typeprof/export.rb +9 -5
  9. data/lib/typeprof/import.rb +60 -21
  10. data/lib/typeprof/iseq.rb +303 -201
  11. data/lib/typeprof/method.rb +2 -2
  12. data/lib/typeprof/type.rb +97 -47
  13. data/lib/typeprof/version.rb +1 -1
  14. data/smoke/alias2.rb +1 -1
  15. data/smoke/array15.rb +1 -1
  16. data/smoke/array3.rb +1 -1
  17. data/smoke/array6.rb +2 -2
  18. data/smoke/array8.rb +1 -1
  19. data/smoke/attr-module.rb +1 -4
  20. data/smoke/attr-vis.rb +1 -1
  21. data/smoke/attr.rb +1 -1
  22. data/smoke/block-args2.rb +3 -3
  23. data/smoke/block-args3.rb +4 -4
  24. data/smoke/break2.rb +1 -1
  25. data/smoke/gvar2.rb +0 -3
  26. data/smoke/hash-bot.rb +1 -1
  27. data/smoke/hash4.rb +1 -1
  28. data/smoke/huge_union.rb +86 -0
  29. data/smoke/identifier_keywords.rb +17 -0
  30. data/smoke/initialize.rb +1 -1
  31. data/smoke/ivar2.rb +1 -1
  32. data/smoke/ivar3.rb +1 -1
  33. data/smoke/kwrest.rb +2 -2
  34. data/smoke/kwrest.rbs +1 -1
  35. data/smoke/method_missing.rb +1 -1
  36. data/smoke/next2.rb +1 -1
  37. data/smoke/noname.rb +9 -0
  38. data/smoke/or_raise.rb +18 -0
  39. data/smoke/parameterizedd-self.rb +2 -2
  40. data/smoke/pattern-match1.rb +1 -6
  41. data/smoke/proc6.rb +13 -0
  42. data/smoke/proc7.rb +32 -0
  43. data/smoke/rbs-tyvar4.rb +1 -1
  44. data/smoke/rbs-vars.rb +0 -3
  45. data/smoke/require1.rb +13 -0
  46. data/smoke/require2.rb +13 -0
  47. data/smoke/struct5.rb +1 -1
  48. data/smoke/struct6.rb +1 -1
  49. data/smoke/struct7.rb +1 -1
  50. data/smoke/super3.rb +1 -1
  51. data/smoke/symbol-proc-attr.rb +1 -1
  52. data/smoke/symbol-proc-attr2.rb +1 -1
  53. data/testbed/ao.rb +1 -1
  54. data/typeprof.gemspec +1 -1
  55. metadata +12 -4
@@ -73,9 +73,9 @@ module TypeProf
73
73
  else
74
74
  ty = Type::Instance.new(recv)
75
75
  end
76
- meths = scratch.get_method(recv, false, :initialize)
76
+ meths = scratch.get_method(recv, false, false, :initialize)
77
77
  meths.flat_map do |meth|
78
- meth.do_send(ty, :initialize, aargs, ep, env, scratch) do |ret_ty, ep, env|
78
+ meth.do_send(ty, :initialize, aargs, ep, env, scratch) do |_ret_ty, ep, env|
79
79
  ctn[ty, ep, env]
80
80
  end
81
81
  end
@@ -104,7 +104,7 @@ module TypeProf
104
104
  sym = get_sym("respond_to?", aargs.lead_tys[0], ep, scratch)
105
105
  if sym
106
106
  klass, singleton = recv.method_dispatch_info
107
- if scratch.get_method(klass, singleton, sym)
107
+ if scratch.get_method(klass, singleton, false, sym)
108
108
  true_val = Type::Instance.new(Type::Builtin[:true])
109
109
  ctn[true_val, ep, env]
110
110
  else
@@ -334,7 +334,7 @@ module TypeProf
334
334
  else
335
335
  aargs.lead_tys.each do |aarg|
336
336
  sym = get_sym("module_function", aarg, ep, scratch) or next
337
- meths = scratch.get_method(recv, false, sym)
337
+ meths = scratch.get_method(recv, false, false, sym)
338
338
  meths.each do |mdef|
339
339
  scratch.add_method(recv, sym, true, mdef)
340
340
  end
@@ -350,7 +350,7 @@ module TypeProf
350
350
  if recv.is_a?(Type::Class)
351
351
  aargs.lead_tys.each do |aarg|
352
352
  sym = get_sym("public", aarg, ep, scratch) or next
353
- meths = scratch.get_method(recv, false, sym)
353
+ meths = scratch.get_method(recv, false, false, sym)
354
354
  next unless meths
355
355
  meths.each do |mdef|
356
356
  mdef.pub_meth = true if mdef.respond_to?(:pub_meth=)
@@ -370,7 +370,7 @@ module TypeProf
370
370
  if recv.is_a?(Type::Class)
371
371
  aargs.lead_tys.each do |aarg|
372
372
  sym = get_sym("private", aarg, ep, scratch) or next
373
- meths = scratch.get_method(recv, false, sym)
373
+ meths = scratch.get_method(recv, false, false, sym)
374
374
  next unless meths
375
375
  meths.each do |mdef|
376
376
  mdef.pub_meth = false if mdef.respond_to?(:pub_meth=)
@@ -635,9 +635,6 @@ module TypeProf
635
635
  end
636
636
 
637
637
  def self.file_require(feature, scratch)
638
- return :done, :false if scratch.loaded_features[feature]
639
- scratch.loaded_features[feature] = true
640
-
641
638
  # XXX: dynamic RBS load is really needed?? Another idea:
642
639
  #
643
640
  # * RBS should be loaded in advance of analysis
@@ -653,7 +650,11 @@ module TypeProf
653
650
 
654
651
  begin
655
652
  filetype, path = $LOAD_PATH.resolve_feature_path(feature)
653
+
656
654
  if filetype == :rb
655
+ return :done, :false if scratch.loaded_files[path]
656
+ scratch.loaded_files[path] = true
657
+
657
658
  return :do, path if File.readable?(path)
658
659
 
659
660
  return :error, "failed to load: #{ path }"
@@ -714,13 +715,14 @@ module TypeProf
714
715
  return ctn[Type.any, ep, env]
715
716
  end
716
717
 
717
- if scratch.loaded_features[feature]
718
+ path = File.join(File.dirname(ep.ctx.iseq.absolute_path), feature) + ".rb" # XXX
719
+
720
+ if scratch.loaded_files[path]
718
721
  result = Type::Instance.new(Type::Builtin[:false])
719
722
  return ctn[result, ep, env]
720
723
  end
721
- scratch.loaded_features[feature] = true
724
+ scratch.loaded_files[path] = true
722
725
 
723
- path = File.join(File.dirname(ep.ctx.iseq.path), feature) + ".rb" # XXX
724
726
  return Builtin.file_load(path, ep, env, scratch, &ctn) if File.readable?(path)
725
727
 
726
728
  scratch.warn(ep, "failed to load: #{ path }")
@@ -811,6 +813,7 @@ module TypeProf
811
813
  Type::Builtin[:exc] = scratch.get_constant(klass_obj, :Exception)
812
814
  Type::Builtin[:encoding] = scratch.get_constant(klass_obj, :Encoding)
813
815
  Type::Builtin[:enumerator] = scratch.get_constant(klass_obj, :Enumerator)
816
+ Type::Builtin[:kernel] = scratch.get_constant(klass_obj, :Kernel)
814
817
 
815
818
  klass_vmcore = Type::Builtin[:vmcore]
816
819
  klass_ary = Type::Builtin[:ary]
@@ -823,6 +826,7 @@ module TypeProf
823
826
  scratch.set_custom_method(klass_vmcore, :"core#undef_method", Builtin.method(:vmcore_undef_method))
824
827
  scratch.set_custom_method(klass_vmcore, :"core#hash_merge_kwd", Builtin.method(:vmcore_hash_merge_kwd))
825
828
  scratch.set_custom_method(klass_vmcore, :"core#raise", Builtin.method(:vmcore_raise))
829
+
826
830
  scratch.set_custom_method(klass_vmcore, :lambda, Builtin.method(:lambda))
827
831
  scratch.set_singleton_custom_method(klass_obj, :"new", Builtin.method(:object_s_new))
828
832
  scratch.set_custom_method(klass_obj, :p, Builtin.method(:kernel_p), false)
@@ -878,6 +882,12 @@ module TypeProf
878
882
  str_ty = Type::Instance.new(Type::Builtin[:str])
879
883
  env_ty = Type.gen_hash {|h| h[str_ty] = Type.optional(str_ty) }
880
884
  scratch.add_constant(klass_obj, :ENV, env_ty, false)
885
+
886
+ scratch.search_method(Type::Builtin[:kernel], false, :sprintf) do |mdefs,|
887
+ mdefs.each do |mdef|
888
+ scratch.add_method(klass_vmcore, :"core#sprintf", false, mdef)
889
+ end
890
+ end
881
891
  end
882
892
  end
883
893
  end
data/lib/typeprof/cli.rb CHANGED
@@ -64,6 +64,7 @@ module TypeProf
64
64
  opt.separator "Advanced options:"
65
65
  opt.on("--[no-]stub-execution", "Force to call all unreachable methods with \"untyped\" arguments") {|v| options[:stub_execution] = v }
66
66
  opt.on("--type-depth-limit DEPTH", Integer, "Limit the maximum depth of nested types") {|v| options[:type_depth_limit] = v }
67
+ opt.on("--union-width-limit WIDTH", Integer, "Limit the maximum count of class instances in one union type") {|v| options[:union_width_limit] = v }
67
68
  opt.on("--debug", "Display analysis log (for debugging purpose)") { verbose = 2 }
68
69
  opt.on("--[no-]stackprof MODE", /\Acpu|wall|object\z/, "Enable stackprof (for debugging purpose)") {|v| options[:stackprof] = v.to_sym }
69
70
 
@@ -40,6 +40,7 @@ module TypeProf
40
40
  show_source_locations: false,
41
41
  stub_execution: true,
42
42
  type_depth_limit: 5,
43
+ union_width_limit: 10,
43
44
  stackprof: nil,
44
45
  }.merge(opt[:options])
45
46
  super(**opt)
@@ -98,7 +99,7 @@ module TypeProf
98
99
  if rb.is_a?(Array) # [String name, String content]
99
100
  iseq = ISeq.compile_str(*rb.reverse)
100
101
  else
101
- iseq = ISeq.compile(rb)
102
+ iseq = rb
102
103
  end
103
104
  scratch.add_entrypoint(iseq)
104
105
  end
@@ -307,7 +307,11 @@ module TypeProf
307
307
  def screen_name(scratch)
308
308
  if @rest_ty == Type.bot
309
309
  if @lead_tys.empty?
310
- return "Array[bot]" # RBS does not allow an empty tuple "[]"
310
+ # This is heuristic: in general, an empty array is a wrong guess.
311
+ # Note that an empty array is representable as "[ ]" in RBS, but
312
+ # users often have to modify it to "Array[something]".
313
+ # In this term, "Array[untyped]" is considered more useful than "[ ]".
314
+ return "Array[untyped]"
311
315
  end
312
316
  s = @lead_tys.map do |ty|
313
317
  ty.screen_name(scratch)
@@ -673,6 +677,8 @@ module TypeProf
673
677
  k_ty = k_ty.union(k)
674
678
  v_ty = v_ty.union(v)
675
679
  end
680
+ k_ty = Type.any if k_ty == Type.bot
681
+ v_ty = Type.any if v_ty == Type.bot
676
682
  k_ty = k_ty.screen_name(scratch)
677
683
  v_ty = v_ty.screen_name(scratch)
678
684
  "Hash[#{ k_ty }, #{ v_ty }]"
@@ -69,14 +69,17 @@ module TypeProf
69
69
  end
70
70
 
71
71
  def show_gvars(scratch, gvars, output)
72
+ gvars = gvars.dump.filter_map do |gvar_name, entry|
73
+ if entry.type != Type.bot && !entry.rbs_declared
74
+ [gvar_name, entry]
75
+ end
76
+ end
72
77
  # A signature for global variables is not supported in RBS
73
- return if gvars.dump.empty?
78
+ return if gvars.empty?
74
79
 
75
80
  output.puts "# Global variables"
76
- gvars.dump.each do |gvar_name, entry|
77
- next if entry.type == Type.bot
78
- s = entry.rbs_declared ? "#" : ""
79
- output.puts s + "#{ gvar_name }: #{ entry.type.screen_name(scratch) }"
81
+ gvars.each do |gvar_name, entry|
82
+ output.puts "#{ gvar_name }: #{ entry.type.screen_name(scratch) }"
80
83
  end
81
84
  output.puts
82
85
  end
@@ -149,6 +152,7 @@ module TypeProf
149
152
  source_locations[key] ||= ctx.iseq.source_location(0)
150
153
  (methods[key] ||= []) << @scratch.show_method_signature(ctx)
151
154
  when AliasMethodDef
155
+ next if mdef.def_ep && Config.check_dir_filter(mdef.def_ep.source_location) == :exclude
152
156
  alias_name, orig_name = mid, mdef.orig_mid
153
157
  if singleton
154
158
  alias_name = "self.#{ alias_name }"
@@ -373,25 +373,30 @@ module TypeProf
373
373
  end
374
374
 
375
375
  def conv_block(rbs_block)
376
- type = rbs_block.type
376
+ blk = rbs_block.type
377
377
 
378
- # XXX
379
- raise NotImplementedError unless type.optional_keywords.empty?
380
- raise NotImplementedError unless type.required_keywords.empty?
381
- raise NotImplementedError if type.rest_keywords
382
-
383
- req = rbs_block.required
384
-
385
- lead_tys = type.required_positionals.map do |type|
386
- conv_type(type.type)
387
- end
388
- opt_tys = type.optional_positionals.map do |type|
389
- conv_type(type.type)
390
- end
378
+ lead_tys = blk.required_positionals.map {|type| conv_type(type.type) }
379
+ opt_tys = blk.optional_positionals.map {|type| conv_type(type.type) }
380
+ rest_ty = blk.rest_positionals
381
+ rest_ty = conv_type(rest_ty.type) if rest_ty
382
+ opt_kw_tys = blk.optional_keywords.to_h {|key, type| [key, conv_type(type.type)] }
383
+ req_kw_tys = blk.required_keywords.to_h {|key, type| [key, conv_type(type.type)] }
384
+ rest_kw_ty = blk.rest_keywords
385
+ rest_kw_ty = conv_type(rest_kw_ty.type) if rest_kw_ty
391
386
 
392
- ret_ty = conv_type(type.return_type)
387
+ ret_ty = conv_type(blk.return_type)
393
388
 
394
- [req, lead_tys, opt_tys, ret_ty]
389
+ {
390
+ required_block: rbs_block.required,
391
+ lead_tys: lead_tys,
392
+ opt_tys: opt_tys,
393
+ rest_ty: rest_ty,
394
+ req_kw_tys: req_kw_tys,
395
+ opt_kw_tys: opt_kw_tys,
396
+ rest_kw_ty: rest_kw_ty,
397
+ blk: blk,
398
+ ret_ty: ret_ty,
399
+ }
395
400
  end
396
401
 
397
402
  def conv_type(ty)
@@ -454,6 +459,8 @@ module TypeProf
454
459
  end
455
460
  when RBS::Types::Union
456
461
  [:union, ty.types.map {|ty2| conv_type(ty2) }.compact]
462
+ when RBS::Types::Intersection
463
+ [:intersection, ty.types.map {|ty2| conv_type(ty2) }.compact]
457
464
  when RBS::Types::Optional
458
465
  [:optional, conv_type(ty.type)]
459
466
  when RBS::Types::Interface
@@ -630,7 +637,13 @@ module TypeProf
630
637
  kw_tys = []
631
638
  req_kw_tys.each {|key, ty| kw_tys << [true, key, conv_type(ty)] }
632
639
  opt_kw_tys.each {|key, ty| kw_tys << [false, key, conv_type(ty)] }
633
- kw_rest_ty = conv_type(rest_kw_ty) if rest_kw_ty
640
+ if rest_kw_ty
641
+ ty = conv_type(rest_kw_ty)
642
+ kw_rest_ty = Type.gen_hash do |h|
643
+ k_ty = Type::Instance.new(Type::Builtin[:sym])
644
+ h[k_ty] = ty
645
+ end
646
+ end
634
647
 
635
648
  blks = conv_block(blk)
636
649
 
@@ -643,13 +656,36 @@ module TypeProf
643
656
 
644
657
  def conv_block(blk)
645
658
  return [Type.nil] unless blk
646
- req, lead_tys, opt_tys, ret_ty = blk
659
+
660
+ required_block = blk[:required_block]
661
+ lead_tys = blk[:lead_tys]
662
+ opt_tys = blk[:opt_tys]
663
+ rest_ty = blk[:rest_ty]
664
+ req_kw_tys = blk[:req_kw_tys]
665
+ opt_kw_tys = blk[:opt_kw_tys]
666
+ rest_kw_ty = blk[:rest_kw_ty]
667
+ ret_ty = blk[:ret_ty]
668
+
647
669
  lead_tys = lead_tys.map {|ty| conv_type(ty) }
648
670
  opt_tys = opt_tys.map {|ty| conv_type(ty) }
649
- msig = MethodSignature.new(lead_tys, opt_tys, nil, [], {}, nil, Type.nil)
671
+ rest_ty = conv_type(rest_ty) if rest_ty
672
+ kw_tys = []
673
+ req_kw_tys.each {|key, ty| kw_tys << [true, key, conv_type(ty)] }
674
+ opt_kw_tys.each {|key, ty| kw_tys << [false, key, conv_type(ty)] }
675
+ if rest_kw_ty
676
+ ty = conv_type(rest_kw_ty)
677
+ kw_rest_ty = Type.gen_hash do |h|
678
+ k_ty = Type::Instance.new(Type::Builtin[:sym])
679
+ h[k_ty] = ty
680
+ end
681
+ end
682
+
683
+ msig = MethodSignature.new(lead_tys, opt_tys, rest_ty, [], kw_tys, kw_rest_ty, Type.nil)
684
+
650
685
  ret_ty = conv_type(ret_ty)
686
+
651
687
  ret = [Type::Proc.new(TypedBlock.new(msig, ret_ty), Type::Builtin[:proc])]
652
- ret << Type.nil unless req
688
+ ret << Type.nil unless required_block
653
689
  ret
654
690
  end
655
691
 
@@ -694,7 +730,10 @@ module TypeProf
694
730
  end
695
731
  when :union
696
732
  tys = ty[1]
697
- Type::Union.new(Utils::Set[*tys.map {|ty2| conv_type(ty2) }], nil).normalize # XXX: Array and Hash support
733
+ Type::Union.create(Utils::Set[*tys.map {|ty2| conv_type(ty2) }], nil) # XXX: Array and Hash support
734
+ when :intersection
735
+ tys = ty[1]
736
+ conv_type(tys.first) # XXX: This is wrong! We need to support intersection type
698
737
  when :var
699
738
  Type::Var.new(ty[1])
700
739
  when :proc
data/lib/typeprof/iseq.rb CHANGED
@@ -3,75 +3,65 @@ module TypeProf
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
5
 
6
- include Utils::StructuralEquality
7
-
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)
6
+ class << self
7
+ def compile(file)
8
+ compile_core(nil, file)
9
+ end
10
+
11
+ def compile_str(str, path = nil)
12
+ compile_core(str, path)
13
+ end
14
+
15
+ private def compile_core(str, path)
16
+ opt = RubyVM::InstructionSequence.compile_option
17
+ opt[:inline_const_cache] = false
18
+ opt[:peephole_optimization] = false
19
+ opt[:specialized_instruction] = false
20
+ opt[:operands_unification] = false
21
+ opt[:coverage_enabled] = false
22
+
23
+ if str
24
+ iseq = RubyVM::InstructionSequence.compile(str, path, **opt)
25
+ else
26
+ iseq = RubyVM::InstructionSequence.compile_file(path, **opt)
27
+ end
28
+
29
+ return new(iseq.to_a)
30
+ end
16
31
  end
17
32
 
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)
33
+ Insn = Struct.new(:insn, :operands, :lineno)
34
+ class Insn
35
+ def check?(insn_cmp, operands_cmp = nil)
36
+ return insn == insn_cmp && (!operands_cmp || operands == operands_cmp)
37
+ end
26
38
  end
27
39
 
28
- FRESH_ID = [0]
40
+ ISEQ_FRESH_ID = [0]
29
41
 
30
42
  def initialize(iseq)
31
- @id = FRESH_ID[0]
32
- FRESH_ID[0] += 1
43
+ @id = (ISEQ_FRESH_ID[0] += 1)
33
44
 
34
45
  _magic, _major_version, _minor_version, _format_type, _misc,
35
46
  @name, @path, @absolute_path, @start_lineno, @type,
36
47
  @locals, @fargs_format, catch_table, insns = *iseq
37
48
 
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) }
45
- 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
51
- end
52
- insns[i, 0] = [[:_iseq_body_start]]
53
- end
49
+ convert_insns(insns)
54
50
 
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
61
- end
51
+ add_body_start_marker(insns)
62
52
 
63
- @insns = []
64
- @linenos = []
53
+ add_exception_cont_marker(insns, catch_table)
65
54
 
66
- labels = setup_iseq(insns, special_labels)
55
+ labels = create_label_table(insns)
67
56
 
68
- # checkmatch->branch
69
- # send->branch
57
+ @insns = setup_insns(insns, labels)
58
+
59
+ @fargs_format[:opt] = @fargs_format[:opt].map {|l| labels[l] } if @fargs_format[:opt]
70
60
 
71
61
  @catch_table = []
72
62
  catch_table.map do |type, iseq, first, last, cont, stack_depth|
73
63
  iseq = iseq ? ISeq.new(iseq) : nil
74
- target = labels[special_labels[cont] ? :"#{ cont }_special" : cont]
64
+ target = labels[cont]
75
65
  entry = [type, iseq, target, stack_depth]
76
66
  labels[first].upto(labels[last]) do |i|
77
67
  @catch_table[i] ||= []
@@ -79,43 +69,143 @@ module TypeProf
79
69
  end
80
70
  end
81
71
 
82
- merge_branches
72
+ rename_insn_types
73
+
74
+ unify_instructions
75
+ end
76
+
77
+ def source_location(pc)
78
+ "#{ @path }:#{ @insns[pc].lineno }"
79
+ end
83
80
 
84
- analyze_stack
81
+ attr_reader :name, :path, :absolute_path, :start_lineno, :type, :locals, :fargs_format, :catch_table, :insns
82
+ attr_reader :id
83
+
84
+ def pretty_print(q)
85
+ q.text "ISeq["
86
+ q.group do
87
+ q.nest(1) do
88
+ q.breakable ""
89
+ q.text "@type= #{ @type }"
90
+ q.breakable ", "
91
+ q.text "@name= #{ @name }"
92
+ q.breakable ", "
93
+ q.text "@path= #{ @path }"
94
+ q.breakable ", "
95
+ q.text "@absolute_path= #{ @absolute_path }"
96
+ q.breakable ", "
97
+ q.text "@start_lineno= #{ @start_lineno }"
98
+ q.breakable ", "
99
+ q.text "@fargs_format= #{ @fargs_format.inspect }"
100
+ q.breakable ", "
101
+ q.text "@insns="
102
+ q.group(2) do
103
+ @insns.each_with_index do |(insn, *operands), i|
104
+ q.breakable
105
+ q.group(2, "#{ i }: #{ insn.to_s }", "") do
106
+ q.pp operands
107
+ end
108
+ end
109
+ end
110
+ end
111
+ q.breakable
112
+ end
113
+ q.text "]"
85
114
  end
86
115
 
87
116
  def <=>(other)
88
117
  @id <=> other.id
89
118
  end
90
119
 
91
- def setup_iseq(insns, special_labels)
120
+ # Remove lineno entry and convert instructions to Insn instances
121
+ def convert_insns(insns)
122
+ ninsns = []
123
+ lineno = 0
124
+ insns.each do |e|
125
+ case e
126
+ when Integer # lineno
127
+ lineno = e
128
+ when Symbol # label or trace
129
+ ninsns << e
130
+ when Array
131
+ insn, *operands = e
132
+ ninsns << Insn.new(insn, operands, lineno)
133
+ else
134
+ raise "unknown iseq entry: #{ e }"
135
+ end
136
+ end
137
+ insns.replace(ninsns)
138
+ end
139
+
140
+ # Insert a dummy instruction "_iseq_body_start"
141
+ def add_body_start_marker(insns)
142
+ case @type
143
+ when :method, :block
144
+ # skip initialization code of optional arguments
145
+ if @fargs_format[:opt]
146
+ label = @fargs_format[:opt].last
147
+ i = insns.index(label) + 1
148
+ else
149
+ i = insns.find_index {|insn| insn.is_a?(Insn) }
150
+ end
151
+
152
+ # skip initialization code of keyword arguments
153
+ while insns[i][0] == :checkkeyword
154
+ raise if insns[i + 1].insn != :branchif
155
+ label = insns[i + 1].operands[0]
156
+ i = insns.index(label) + 1
157
+ end
158
+
159
+ insns.insert(i, Insn.new(:_iseq_body_start, [], @start_lineno))
160
+ end
161
+ end
162
+
163
+ # Insert "nop" instruction to continuation point of exception handlers
164
+ def add_exception_cont_marker(insns, catch_table)
165
+ # rescue/ensure clauses need to have a dedicated return addresses
166
+ # because they requires to be virtually called.
167
+ # So, this preprocess adds "nop" to make a new insn for their return addresses
168
+ exception_cont_labels = {}
169
+ catch_table.map! do |type, iseq, first, last, cont, stack_depth|
170
+ if type == :rescue || type == :ensure
171
+ exception_cont_labels[cont] = true
172
+ cont = :"#{ cont }_exception_cont"
173
+ end
174
+ [type, iseq, first, last, cont, stack_depth]
175
+ end
176
+
92
177
  i = 0
178
+ while i < insns.size
179
+ e = insns[i]
180
+ if exception_cont_labels[e]
181
+ insns.insert(i, :"#{ e }_exception_cont", Insn.new(:nop, []))
182
+ i += 2
183
+ end
184
+ i += 1
185
+ end
186
+ end
187
+
188
+ def create_label_table(insns)
189
+ pc = 0
93
190
  labels = {}
94
- ninsns = []
95
191
  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
192
+ if e.is_a?(Symbol)
193
+ labels[e] = pc
103
194
  else
104
- i += 1 if e.is_a?(Array)
105
- ninsns << e
195
+ pc += 1
106
196
  end
107
197
  end
198
+ labels
199
+ end
108
200
 
109
- lineno = 0
110
- ninsns.each do |e|
201
+ def setup_insns(insns, labels)
202
+ ninsns = []
203
+ insns.each do |e|
111
204
  case e
112
- when Integer # lineno
113
- lineno = e
114
205
  when Symbol # label or trace
115
206
  nil
116
- when Array
117
- insn, *operands = e
118
- operands = (INSN_TABLE[insn] || []).zip(operands).map do |type, operand|
207
+ when Insn
208
+ operands = (INSN_TABLE[e.insn] || []).zip(e.operands).map do |type, operand|
119
209
  case type
120
210
  when "ISEQ"
121
211
  operand && ISeq.new(operand)
@@ -131,81 +221,64 @@ module TypeProf
131
221
  end
132
222
  end
133
223
 
134
- @insns << [insn, operands]
135
- @linenos << lineno
224
+ ninsns << Insn.new(e.insn, operands, e.lineno)
136
225
  else
137
226
  raise "unknown iseq entry: #{ e }"
138
227
  end
139
228
  end
140
-
141
- @fargs_format[:opt] = @fargs_format[:opt].map {|l| labels[l] } if @fargs_format[:opt]
142
-
143
- labels
229
+ ninsns
144
230
  end
145
231
 
146
- def merge_branches
147
- @insns.size.times do |i|
148
- insn, operands = @insns[i]
149
- case insn
232
+ def rename_insn_types
233
+ @insns.each do |insn|
234
+ case insn.insn
150
235
  when :branchif
151
- @insns[i] = [:branch, [:if] + operands]
236
+ insn.insn, insn.operands = :branch, [:if] + insn.operands
152
237
  when :branchunless
153
- @insns[i] = [:branch, [:unless] + operands]
238
+ insn.insn, insn.operands = :branch, [:unless] + insn.operands
154
239
  when :branchnil
155
- @insns[i] = [:branch, [:nil] + operands]
240
+ insn.insn, insn.operands = :branch, [:nil] + insn.operands
241
+ when :getblockparam, :getblockparamproxy
242
+ insn.insn = :getlocal
156
243
  end
157
244
  end
158
245
  end
159
246
 
160
- def source_location(pc)
161
- "#{ @path }:#{ @linenos[pc] }"
162
- end
163
-
164
- attr_reader :name, :path, :absolute_path, :start_lineno, :type, :locals, :fargs_format, :catch_table, :insns, :linenos
165
- attr_reader :id
166
-
167
- def pretty_print(q)
168
- q.text "ISeq["
169
- q.group do
170
- q.nest(1) do
171
- q.breakable ""
172
- q.text "@type= #{ @type }"
173
- q.breakable ", "
174
- q.text "@name= #{ @name }"
175
- q.breakable ", "
176
- q.text "@path= #{ @path }"
177
- q.breakable ", "
178
- q.text "@absolute_path= #{ @absolute_path }"
179
- q.breakable ", "
180
- q.text "@start_lineno= #{ @start_lineno }"
181
- q.breakable ", "
182
- q.text "@fargs_format= #{ @fargs_format.inspect }"
183
- q.breakable ", "
184
- q.text "@insns="
185
- q.group(2) do
186
- @insns.each_with_index do |(insn, *operands), i|
187
- q.breakable
188
- q.group(2, "#{ i }: #{ insn.to_s }", "") do
189
- q.pp operands
190
- end
191
- end
192
- end
193
- end
194
- q.breakable
195
- end
196
- q.text "]"
197
- end
247
+ # Unify some instructions for flow-sensitive analysis
248
+ def unify_instructions
249
+ # This method rewrites instructions to enable flow-sensitive analysis.
250
+ #
251
+ # Consider `if x; ...; else; ... end`.
252
+ # When the variable `x` is of type "Integer | nil",
253
+ # we want to make sure that `x` is "Integer" in then clause.
254
+ # So, we need to split the environment to two ones:
255
+ # one is that `x` is of type "Integer", and the other is that
256
+ # `x` is type "nil".
257
+ #
258
+ # However, `if x` is compiled to "getlocal; branch".
259
+ # TypeProf evaluates them as follows:
260
+ #
261
+ # * "getlocal" pushes the value of `x` to the stack, amd
262
+ # * "branch" checks the value on the top of the stack
263
+ #
264
+ # TypeProf does not keep where the value comes from, so
265
+ # it is difficult to split the environment when evaluating "branch".
266
+ #
267
+ # This method rewrites "getlocal; branch" to "nop; getlocal_branch".
268
+ # The two instructions are unified to "getlocal_branch" instruction,
269
+ # so TypeProf can split the environment.
270
+ #
271
+ # This is a very fragile appoach because it highly depends on the compiler of Ruby.
198
272
 
199
- def analyze_stack
200
273
  # gather branch targets
201
274
  # TODO: catch_table should be also considered
202
275
  branch_targets = {}
203
- @insns.each do |insn, operands|
204
- case insn
276
+ @insns.each do |insn|
277
+ case insn.insn
205
278
  when :branch
206
- branch_targets[operands[1]] = true
279
+ branch_targets[insn.operands[1]] = true
207
280
  when :jump
208
- branch_targets[operands[0]] = true
281
+ branch_targets[insn.operands[0]] = true
209
282
  end
210
283
  end
211
284
 
@@ -215,25 +288,27 @@ module TypeProf
215
288
  case_branch_list = []
216
289
  if CASE_WHEN_CHECKMATCH
217
290
  (@insns.size - 1).times do |i|
218
- insn0, getlocal_operands = @insns[i]
219
- next unless [:getlocal, :getblockparam, :getblockparamproxy].include?(insn0) && getlocal_operands[1] == 0
291
+ insn = @insns[i]
292
+ next unless insn.insn == :getlocal && insn.operands[1] == 0
293
+ getlocal_operands = insn.operands
220
294
  nops = [i]
221
295
  new_insns = []
222
296
  j = i + 1
223
297
  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, []]
298
+ case @insns[j].insn
299
+ when :dup
300
+ break unless @insns[j + 1].check?(:putnil, [])
301
+ break unless @insns[j + 2].check?(:putobject, [true])
302
+ break unless @insns[j + 3].check?(:getconstant) # TODO: support A::B::C
303
+ break unless @insns[j + 4].check?(:checkmatch, [2])
304
+ break unless @insns[j + 5].check?(:branch)
305
+ target_pc = @insns[j + 5].operands[1]
306
+ break unless @insns[target_pc].check?(:pop, [])
233
307
  nops << j << (j + 4) << target_pc
234
- new_insns << [j + 5, [:getlocal_checkmatch_branch, [getlocal_operands, @insns[j + 5][1]]]]
308
+ branch_operands = @insns[j + 5][1]
309
+ new_insns << [j + 5, Insn.new(:getlocal_checkmatch_branch, [getlocal_operands, branch_operands])]
235
310
  j += 6
236
- when [:pop, []]
311
+ when :pop
237
312
  nops << j
238
313
  case_branch_list << [nops, new_insns]
239
314
  break
@@ -244,25 +319,28 @@ module TypeProf
244
319
  end
245
320
  else
246
321
  (@insns.size - 1).times do |i|
247
- insn0, getlocal_operands = @insns[i]
248
- next unless [:getlocal, :getblockparam, :getblockparamproxy].include?(insn0) && getlocal_operands[1] == 0
322
+ insn = @insns[i]
323
+ next unless insn.insn == :getlocal && insn.operands[1] == 0
324
+ getlocal_operands = insn.operands
249
325
  nops = []
250
326
  new_insns = []
251
327
  j = i + 1
252
328
  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, []]
329
+ insn = @insns[j]
330
+ if insn.check?(:putnil, [])
331
+ break unless @insns[j + 1].check?(:putobject, [true])
332
+ break unless @insns[j + 2].check?(:getconstant) # TODO: support A::B::C
333
+ break unless @insns[j + 3].check?(:topn, [1])
334
+ break unless @insns[j + 4].check?(:send, [{:mid=>:===, :flag=>20, :orig_argc=>1}, nil])
335
+ break unless @insns[j + 5].check?(:branch)
336
+ target_pc = @insns[j + 5].operands[1]
337
+ break unless @insns[target_pc].check?(:pop, [])
262
338
  nops << (j + 4) #<< target_pc
263
- new_insns << [j + 5, [:arg_getlocal_send_branch, [getlocal_operands, @insns[j + 4][1], @insns[j + 5][1]]]]
339
+ send_operands = @insns[j + 4][1]
340
+ branch_operands = @insns[j + 5][1]
341
+ new_insns << [j + 5, Insn.new(:arg_getlocal_send_branch, [getlocal_operands, send_operands, branch_operands])]
264
342
  j += 6
265
- when [:pop, []]
343
+ elsif insn.check?(:pop, [])
266
344
  #nops << j
267
345
  case_branch_list << [nops, new_insns]
268
346
  break
@@ -273,15 +351,15 @@ module TypeProf
273
351
  end
274
352
  end
275
353
  case_branch_list.each do |nops, new_insns|
276
- nops.each {|i| @insns[i] = [:nop, []] }
354
+ nops.each {|i| @insns[i] = Insn.new(:nop, []) }
277
355
  new_insns.each {|i, insn| @insns[i] = insn }
278
356
  end
279
357
 
280
358
  # find a pattern: getlocal(recv), ..., send (is_a?, respond_to?), branch
281
359
  recv_getlocal_send_branch_list = []
282
360
  (@insns.size - 1).times do |i|
283
- insn, operands = @insns[i]
284
- if insn == :getlocal && operands[1] == 0
361
+ insn = @insns[i]
362
+ if insn.insn == :getlocal && insn.operands[1] == 0
285
363
  j = i + 1
286
364
  sp = 1
287
365
  while @insns[j]
@@ -297,94 +375,118 @@ module TypeProf
297
375
  end
298
376
  recv_getlocal_send_branch_list.each do |i, j|
299
377
  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]]
378
+ getlocal_operands = @insns[i].operands
379
+ send_operands = @insns[j].operands
380
+ branch_operands = @insns[j + 1].operands
381
+ @insns[j] = Insn.new(:nop, [])
382
+ @insns[j + 1] = Insn.new(:recv_getlocal_send_branch, [getlocal_operands, send_operands, branch_operands])
305
383
  end
306
384
 
307
385
  # find a pattern: getlocal, send (===), branch
308
386
  arg_getlocal_send_branch_list = []
309
387
  (@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
388
+ insn1 = @insns[i]
389
+ next unless insn1.insn == :getlocal && insn1.operands[1] == 0
390
+ insn2 = @insns[i + 1]
391
+ next unless insn2.insn == :send
392
+ send_operands = insn2.operands[0]
393
+ next unless send_operands[:flag] == 16 && send_operands[:orig_argc] == 1
394
+ insn3 = @insns[i + 2]
395
+ next unless insn3.insn == :branch
318
396
  arg_getlocal_send_branch_list << i
319
397
  end
320
398
  arg_getlocal_send_branch_list.each do |i|
321
399
  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]]
400
+ getlocal_operands = @insns[i].operands
401
+ send_operands = @insns[i + 1].operands
402
+ branch_operands = @insns[i + 2].operands
403
+ @insns[i + 1] = Insn.new(:nop, [])
404
+ @insns[i + 2] = Insn.new(:arg_getlocal_send_branch, [getlocal_operands, send_operands, branch_operands])
327
405
  end
328
406
 
329
407
  # find a pattern: send (block_given?), branch
330
408
  send_branch_list = []
331
409
  (@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
410
+ insn = @insns[i]
411
+ if insn.insn == :send
412
+ insn = @insns[i + 1]
413
+ if insn.insn == :branch
336
414
  send_branch_list << i
337
415
  end
338
416
  end
339
417
  end
340
418
  send_branch_list.each do |i|
341
419
  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]]
420
+ send_operands = @insns[i].operands
421
+ branch_operands = @insns[i + 1].operands
422
+ @insns[i] = Insn.new(:nop, [])
423
+ @insns[i + 1] = Insn.new(:send_branch, [send_operands, branch_operands])
346
424
  end
347
425
 
348
426
  # find a pattern: getlocal, dup, branch
349
427
  (@insns.size - 2).times do |i|
350
428
  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]]
429
+ insn0 = @insns[i]
430
+ insn1 = @insns[i + 1]
431
+ insn2 = @insns[i + 2]
432
+ if insn0.insn == :getlocal && insn1.insn == :dup && insn2.insn == :branch && insn0.operands[1] == 0
433
+ getlocal_operands = insn0.operands
434
+ dup_operands = insn1.operands
435
+ branch_operands = insn2.operands
436
+ @insns[i ] = Insn.new(:nop, [])
437
+ @insns[i + 1] = Insn.new(:nop, [])
438
+ @insns[i + 2] = Insn.new(:getlocal_dup_branch, [getlocal_operands, dup_operands, branch_operands])
439
+ end
440
+ end
441
+
442
+ # find a pattern: dup, setlocal, branch
443
+ (@insns.size - 2).times do |i|
444
+ next if branch_targets[i + 1] || branch_targets[i + 2]
445
+ insn0 = @insns[i]
446
+ insn1 = @insns[i + 1]
447
+ insn2 = @insns[i + 2]
448
+ if insn0.insn == :dup && insn1.insn == :setlocal && insn2.insn == :branch && insn1.operands[1] == 0
449
+ dup_operands = insn0.operands
450
+ setlocal_operands = insn1.operands
451
+ branch_operands = insn2.operands
452
+ @insns[i ] = Insn.new(:nop, [])
453
+ @insns[i + 1] = Insn.new(:nop, [])
454
+ @insns[i + 2] = Insn.new(:dup_setlocal_branch, [dup_operands, setlocal_operands, branch_operands])
358
455
  end
359
456
  end
360
457
 
361
458
  # find a pattern: dup, branch
362
459
  (@insns.size - 1).times do |i|
363
460
  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]]
461
+ insn0 = @insns[i]
462
+ insn1 = @insns[i + 1]
463
+ if insn0.insn == :dup && insn1.insn == :branch
464
+ dup_operands = insn0.operands
465
+ branch_operands = insn1.operands
466
+ @insns[i ] = Insn.new(:nop, [])
467
+ @insns[i + 1] = Insn.new(:dup_branch, [dup_operands, branch_operands])
369
468
  end
370
469
  end
371
470
 
372
471
  # find a pattern: getlocal, branch
373
472
  (@insns.size - 1).times do |i|
374
473
  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]]
474
+ insn0 = @insns[i]
475
+ insn1 = @insns[i + 1]
476
+ if insn0.insn == :getlocal && insn0.operands[1] == 0 && insn1.insn == :branch
477
+ getlocal_operands = insn0.operands
478
+ branch_operands = insn1.operands
479
+ @insns[i ] = Insn.new(:nop, [])
480
+ @insns[i + 1] = Insn.new(:getlocal_branch, [getlocal_operands, branch_operands])
380
481
  end
381
482
  end
382
483
  end
383
484
 
384
485
  def check_send_branch(sp, j)
385
- insn, operands = @insns[j]
486
+ insn = @insns[j]
487
+ operands = insn.operands
386
488
 
387
- case insn
489
+ case insn.insn
388
490
  when :putspecialobject, :putnil, :putobject, :duparray, :putstring,
389
491
  :putself
390
492
  sp += 1
@@ -422,7 +524,7 @@ module TypeProf
422
524
  argc += 1 # receiver
423
525
  argc += kw_arg.size if kw_arg
424
526
  sp -= argc
425
- return :match if insn == :send && sp == 0 && @insns[j + 1][0] == :branch
527
+ return :match if insn.insn == :send && sp == 0 && @insns[j + 1].insn == :branch
426
528
  sp += 1
427
529
  when :arg_getlocal_send_branch
428
530
  return # not implemented