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

Revisiting TypeProf - IDE support as a primary feature

Revisiting TypeProf - IDE support as a primary feature

Yusuke Endoh

May 14, 2023
Tweet

More Decks by Yusuke Endoh

Other Decks in Programming

Transcript

  1. Yusuke Endoh / @mametter • A Ruby committer working at

    Cookpad w/ @ko1 • My major contributions to Ruby • Keyword arguments • coverage.so • error_highlight • TypeProf  Today's topic 2
  2. Today's topic: TypeProf • A type inference tool since Ruby

    3.0 • Not much used yet…😢 Why? • What we need is "a great developer experience" • Type inference is not enough! • Now working on TypeProf v2 • IDE support is a primary goal 3 def foo: (Integer)->String def foo(n) n.to_s end 5.ti| 1 + "str" Is this a bug? Do you mean: 5.times
  3. 5

  4. 6

  5. Demo (recap) • VSCode extension with TypeProf • inferred method

    types, error detection • go to definition, completion, flow-sensitive analysis • type popup by hover (since v2) • inline RBS in a comment (since v2) • The analysis is very responsive! • Currently, <0.1 sec. per edit • I am now actually dogfooding TypeProf v2 • to develop TypeProf v2 itself 7
  6. Beginning • In RubyKaigi 2016, matz wanted: Static type checking

    for Ruby with no type annotations • No type annotations? Then let's infer them! ➔ TypeProf 10
  7. TypeProf v1: Original goal • A type inference and checking

    tool for Ruby • Assumptions • Analysis speed was not important • The target code was complete 11 NoMethodError: String#upcaase def foo(n) "str".upcaase n.to_s end def foo: (Integer) -> String
  8. TypeProf v1: Goalpost was moved! • What we need is

    IDE support, not a type itself • Assumptions • Analysis speed was not important → Very important! • The target code was complete → Incomplete! 12 5.ti| Do you mean: 5.times 1 + "str" Is this a bug?
  9. TypeProf v1: Postmortem • Completed roughly as a type inference

    tool • But it was not enough to improve the developer experience • Adding IDE support later was difficult • The algorithmic assumptions are quite different ➔ Reboot TypeProf v2 with IDE-aware algorithm 13
  10. Assumptions of new algorithm • Goal: Analysis finishes in <0.1

    second for each edit • Target: Medium-sized projects (~10K or 100K LOC?) • Larger projects should be split to gems (or directories) • API between gems should be declared by type definitions • Key approach: Incremental update of the analysis result 15 0.1 second is about the limit for having the user feel that the system is reacting instantaneously J. Nielsen, "Usability Engineering", 1993
  11. Idea: Flow types to a dataflow graph • Convert Ruby

    code to a dataflow graph • vertex: local variable, etc. / edge: dataflow • Flow types from literals to vertexes 16 y x "str" String .size String#size String z Integer x = "str" y = x z = x.size String
  12. Analysis for methods 17 String def foo Integer#to_s .to_s r

    def main Integer 123 x foo( ) def foo(n) n.to_s end def main x = 123 r = foo(x) ... end Integer n
  13. def foo(n) n.to_f end def main x = 123 r

    = foo(x) ... end Incremental update 18 n String def foo .to_s Integer#to_s Integer r foo( ) 123 def main x Integer Changed
  14. def foo' Incremental update 19 n Integer String def foo

    .to_s Integer#to_s Integer Float Integer#to_f .to_f r foo( ) 123 def main x n def foo(n) n.to_f end def main x = 123 r = foo(x) ... end Integer Changed
  15. Incremental update 20 Integer Float def foo' Integer#to_f .to_f r

    foo( ) 123 def main x n Integer String def foo .to_s Integer#to_s n def foo(n) n.to_f end def main x = 123 r = foo(x) ... end Integer Changed
  16. Incremental update 21 n Integer Float def foo' .to_f Integer#to_f

    The subgraph does not change r foo( ) 123 def main x def foo(n) n.to_f end def main x = 123 r = foo(x) ... end Changed Integer
  17. New algorithm: summary • Basic approach • Convert Ruby code

    to a dataflow graph • Flow types to the graph • Incremental update • Replace only a subgraph of a modified method • Q. A subgraph change may lead to long type propagation? 22
  18. Long type propagation 24 24 foo( ) Integer foo n

    bar( ) bar n baz( ) baz n return return r def baz(n) = n def bar(n) = baz(n) def foo(n) = bar(n) r = foo(1) Integer Integer Integer Integer Integer Integer
  19. Long type propagation 25 25 foo( ) Integer foo n

    bar( ) bar n baz( ) baz n return return r def baz(n) = n def bar(n) = baz(n) def foo(n) = bar(n) r = foo(1) ➔ foo(1.0) Integer Integer Integer Integer Integer Integer Float foo( ) Float Float | Float | | Float | Float | Float
  20. Long type propagation 26 26 foo n bar( ) bar

    n baz( ) baz n return return r def baz(n) = n def bar(n) = baz(n) def foo(n) = bar(n) r = foo(1) ➔ foo(1.0) Integer Integer Integer foo( ) Integer Integer Integer Integer Float foo( ) Float Float | Float | | Float | Float | Float
  21. Long propagation happens actually? • Indeed, long propagation may take

    time possibly • even if subgraph change is small • But not so often under 10K LOC (hopefully) • At least, I haven't experienced a terrible chain in dogfooding • Almost all methods accepts consistent types • For worse case / For larger projects • Write type declarations to stop propagation 27
  22. Type annotation 28 28 foo( ) Integer foo (Ruby) n

    bar( ) bar n baz( ) return return r foo( ) Float foo (type) (Integer) -> Integer × Stop the propagation #: (Integer) -> Integer def foo(n) = bar(n) foo(1); foo(1.0)
  23. Changing superclass can be super slow • TypeProf invalidates the

    analysis of many dependencies • Foo#something, Foo::Something, @something in Foo • Subclass determination on Foo, etc… • Limits superclass changes to at most one per edit • First, only class definitions and constants are analyzed • to fix the class inheritance relationships • Then, other expressions are analyzed 30 class Foo < Bar class Foo < Baz
  24. Idea of flow-sensitive analysis • Filter vertex: Pass any types

    other than a specific type • e.g., var cannot be nil in a then clause of "if var" 31 s String s NilClass String String NilClass Filter vertex # s: String|NilClass if s # s: String s.gsub(...) end
  25. Meta programming • Meta programming is very casual in Ruby

    • Module#include, attr_reader, etc. • TypeProf treats them as syntax, not method calls • Future work: we will need to support DSL individually • has_many, belongs_to, … in Rails • it, let, … in RSpec 32
  26. Evaluation • Target: the source code of TypeProf v2 •

    27 files, 7,000+ LOC • TypeProf v1: • An analysis takes 3 sec. • Incremental update is unsupported (i.e., each edit takes 3 sec.) • TypeProf v2 • A full analysis from scratch: 1.003 sec. • An average incremental update per edit: 0.029 sec. • Note: It could be slower if we proceed with the development 34
  27. Related work • Tern: code-analysis engine for JavaScript • Analysis

    based on dataflow graph • SpiderMonkey's type inference algorithm • B. Hackett, et al. "Fast and Precise Hybrid Type Inference for JavaScript", PLDI 2012 36 https://ternjs.net/
  28. Future work (until Ruby 3.3) • Implement all Ruby and

    RBS syntaxes • Implement and improve IDE features • More error diagnostics • Definition jumps for local variables, instance variables, etc. • Completion for various-type method calls, constants, variables, etc. • Displaying documents, etc. etc. etc. • Dogfooding, dogfooding, dogfooding… 37
  29. Conclusion • TypeProf v2 will make IDE support as a

    primary feature • Aim to make it available by Ruby 3.3 • First target: plain old Ruby code (no DSL), <10,000 LOC? 38
  30. My recent change of Ruby • Ruby 3.3 will change

    the message of NoMethodError • Rationale: #inspect can be very long • Welcome feedback: https://bugs.ruby-lang.org/issues/18285 40 undefined method `firrst' for [1, 2, 3]:Array Ruby 3.2 undefined method `firrst' for an instance of Array Ruby 3.3 (planned)