typeprof 0.14.1 → 0.15.3

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 (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