Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Type Profiler: Ambitious Type Inference for Ruby 3

Avatar for Yusuke Endoh Yusuke Endoh
September 03, 2020

Type Profiler: Ambitious Type Inference for Ruby 3

Avatar for Yusuke Endoh

Yusuke Endoh

September 03, 2020
Tweet

More Decks by Yusuke Endoh

Other Decks in Programming

Transcript

  1. Goals of Ruby 3’s Static Analysis •Make Ruby programming easier

    • Bug detection before execution • Completion and document in IDE … with no type annotation in code! (Ruby 3 Type Challenge) 2 42 + "str" Is this a bug? 42.ti| Do you mean: 42.times {|i| | }
  2. Ruby 3 will provide three “type” items 3 # app.rbs

    def inc: (Integer) -> Integer ① Type Signature Format (RBS) # app.rb def inc(n) n+1 end Ruby code ② Type Inference (ruby-type-profiler) ③ Type Check (Steep, Sorbet, RDL, …)
  3. Ruby 3 Development Experience 4 gem lib.rb lib.rbs app app.rb

    app.rbs 42.ti| 42 + "str" Is this a bug? Do you mean: 42.times {|i| | } ② Type Inference • Generate an RBS prototype • Simply check the code ③ Type Checker • Verify .rb and .rbs • Serve as LSP server You can also write RBS manually if you want Type-guided linter Dynamic type checker More dedicated dev. environments Monitor and Harness Ruby code in run-time Code formatter by leveraging types Rubymine, Tabnine, and other development tools may use type information https://github.com/pocke/rubocop-typed https://github.com/ruby/rbs/blob/master/ lib/rbs/test/type_check.rb RBS may inspire other dreams Today we talk about ①RBS
  4. What is Type Profiler Abstract ("type-level") interpreter of Ruby Generates

    an RBS prototype by gathering what types a method accepts and returns 6 def foo(n) n.to_s end foo(42) Integer (not 42) String (not "42") def foo: (Integer) -> String
  5. Why is the name "Type Profiler"? • Just for a

    historical reason • The initial version was runtime analysis (profiling) • Now it is a bit confusing with a normal profiler •Invite suggestions for the name • Should it start with "S"? (Steep, Sorbet, …) 7
  6. Difference from traditional type system •Traditional type systems use intra-procedural

    (per-method) analysis • Can't handle unannotated method parameter well • … especially when there are many classes that respond to foo 8 def f(x) x.foo end What type is "x"? class Foo def foo; ...; end end class Bar def foo; ...; end end class Baz def foo; ...; end end ...
  7. Difference from traditional type system •Solutions 1. Write type annotation

    → Avoid this 2. Infer type based on its usage → Too strict or too conservative • foo must be unique • or, structural type inference? 3. Use "inter-procedural" analysis • Pros: More powerful analysis • Cons: Slow and hard to control (Challenging) 9 def f(x: Foo) x.foo end def f(x) x.foo end f(Foo.new) "x" is a Foo! def f(x) x.foo end "x" is an object that responds to foo that accepts no argument and returns the same type of a return value of this method
  8. There are many, many topics (but omit) Theoretical issues •

    Recursion and closures[RubyKaigi 2019] • Type-changing variable assignment • Container types and destructive operations[Osaka Ruby Kaigi 2019] • Flow-sensitive analysis[EuRuKo 2019] • Context-insensitive analysis[PPL 2019] • Aid of escape analysis • Cumbersome "untyped" type • Meta-programming features • etc, etc. Practical issues • Trade-off between precision and performance [Nagoya Ruby Kaigi 2019] • Tuple-like and sequential array • Method-local container type [Osaka Ruby Kaigi 2019] • Diagnosis features[Ruby 3 Summit] • Unreachable method analysis • Limitation of byte code • Super-rich Ruby features • Too complex Ruby features • etc, etc. 10
  9. Agenda •Type Profiler: Type Inference for Ruby 3 ➔Demo •

    Simple case • Real-world program case • Library case •How to use • Future Plan 11
  10. Demo 1: ao.rb •Simple case • A 3D renderer (~300

    LoC) • Written by Hideki Miura • Original version was written by Syoyo Fujita https://code.google.com/archive/p/aobench/ •Analysis time < 1 sec. 12
  11. Demo 1: ao.rb 13 class Vec attr_accessor x : Float

    attr_accessor y : Float attr_accessor z : Float def initialize : (Float, Float, Float) -> Float def vadd : (Vec) -> Vec def vsub : (Vec) -> Vec def vcross : (Vec) -> Vec def vdot : (Vec) -> Float def vlength : -> Float def vnormalize : -> Vec end NEW! attr_accessor Formerly, "def x=" and "def x"
  12. Demo 1: ao.rb 14 class Sphere attr_reader center : Vec

    attr_reader radius : Float def initialize : (Vec, Float) -> Float def intersect : (Ray, Isect) -> Vec? end class Isect attr_accessor t : Float attr_accessor hit : bool attr_accessor pl : Vec attr_accessor n : Vec def initialize : -> Vec end NEW! optional type Formerly, "Vec | NilClass" NEW! bool type Formerly, "TrueClass | FalseClass"
  13. Demo 2: Goodcheck • Real-world program case • A customizable

    linter for Ruby (~2000 LoC) • It has "hand-written" RBS • Analysis time < 30 sec. • Note: It requires many libraries which have no RBS • activesupport, concurrent-ruby, cgi, optparse, etc. • Type Profiler analyzed not only Goodcheck but also them • In future, we expect they have own RBS • Type Profiler can use RBS instead of the code itself 15
  14. Manually reformatted to make comparison easy Demo 2: Goodcheck 16

    class Goodcheck::Trigger attr_reader patterns : Array[(Goodcheck::Pattern::Literal | Goodcheck::Pattern::Regexp | Goodcheck::Pattern::Token)?] attr_reader globs : Array[Goodcheck::Glob?] attr_reader passes : Array[Array[untyped]] attr_reader fails : Array[Array[untyped]] attr_reader negated : bool ... end class Goodcheck::Trigger attr_reader patterns : Array[pattern] attr_reader globs: Array[Glob] attr_reader passes: Array[String] attr_reader fails: Array[String] attr_reader negated: bool ... end type Goodcheck::pattern = Pattern::Literal|Pattern::Regexp|Pattern::Token RBS inferred by Type Profiler Hand-written by Soutaro Wrong guess Type alias Not so bad? Extra optional Redundant namescope
  15. Demo 2: Goodcheck 17 class Goodcheck::Pattern::Token attr_reader source : untyped

    attr_reader case_sensitive : true attr_reader variables : {} def initialize : (source: untyped, variables: {}, case_sensitive: true) -> true @regexp : Regexp def regexp : -> Regexp def test_variables : (untyped) -> bool def self.expand : (untyped, untyped, ?depth: Integer) -> Array[Regexp] def self.regexp_for_type : (name: untyped, type: :__, scanner: untyped) -> Regexp? def self.compile_tokens : (untyped, {}, case_sensitive: true) -> Regexp @@TYPES : {} end class Goodcheck::Pattern::Token attr_reader source: String attr_reader case_sensitive: bool attr_reader variables: Hash[Symbol, VarPattern] def initialize: (source: String, variables: Hash[Symbol, VarPattern], case_sensitive: bool) -> void def regexp: -> ::Regexp def self.expand: (String, String, ?depth: Integer) -> Array[::Regexp] def self.regexp_for_type: (name: Symbol, type: Symbol, scanner: StringScanner) -> ::Regexp def self.compile_tokens: (String source, Hash[Symbol, VarPattern] variables, case_sensitive: bool) -> void @@TYPES: Hash[Symbol, ^(String) -> ::Regexp] end RBS inferred by Type Profiler Hand-written by Soutaro Too specfic Failed to track the elements C lib constant (Lack of RBS) void is intended I think not so bad
  16. Demo 3: diff-lcs •Real-world library case Famous algorithmic library •Hand-written

    entry point •Analysis time < 1 sec. 18 https://bestgems.org/ require_relative "lib/diff/lcs" class T; end Diff::LCS.diff([T.new]+[T.new], [T.new]+[T.new]) {}
  17. Demo: diff-lcs 19 class Diff::LCS::Change include Comparable attr_reader action :

    String attr_reader position : Integer attr_reader element : (Array[T] | T)? def self.valid_action? : (String) -> untyped def initialize : (String, Integer, (Array[T] | T)?) -> nil def inspect : -> String ... def == : (untyped) -> bool def <=> : (untyped) -> Integer? def adding? : -> bool def deleting? : -> bool def unchanged? : -> bool def changed? : -> bool def finished_a? : -> bool def finished_b? : -> bool end predicate methods
  18. Agenda •Type Profiler: Type Inference for Ruby 3 •Demo ➔

    How to use Type Profiler • Planned experience • Specific usage •Future plan 20
  19. Typical usage and experience (Plan) 1. Write an entry point

    program (if needed) 2. Apply Type Profiler 3. Partially write RBS for wrong-guessed methods 4. Re-apply Type Profiler 21 lib.rb app.rb Type Profiler lib.rbs (may include wrong guesses) partial RBS for difficult methods lib.rbs (final) ① ② ③ ④ "Partial RBS specification" has been implemented
  20. How to use TP specifically Will be written until the

    RubyKaigi Takeout! (hopefully) https://github.com/mame/ruby-type-profiler 22 I have written the document
  21. Agenda •Type Profiler: Type Inference for Ruby 3 •Demo •How

    to use Type Profiler ➔Future plan • Recent updates • Future plan • Conclusion 23
  22. Recent updates • Improve cosmetics (attr_*, optional, bool, …) •

    Import Array and Hash methods from RBS • Type variable • Support Enumerable, Enumerator, and Struct • Support global variables • Improve flow-sensitive analysis • Improve analysis performance • Fix many, many bugs 24
  23. Future plan • Until Ruby 3: Make it possible for

    plain Ruby code • Support partial RBS specification • Write document and release a gem • Continue to experiment, improve, etc, etc. • After the release: Support Sinatra app, Rails app… • Maybe need dedicated hard-coding for the frameworks • Concern is almost a language extension • ActiveRecord is super-meta feature • Please go easy on 🙏 25
  24. Acknowledgment •Hideki Miura •Matz, Akr, Ko1 • Soutaro Matsumoto •

    Katsuhiro Ueno •Eijiro Sumii •Sorbet developers (Stripe, Shopify, etc.) •Jeff Foster 26
  25. Conclusion •Type Profiler is crawling to you • Inviting suggestions

    for the tool name! • Ask me anything: @mametter (Twitter) 27