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. Revisiting TypeProf:
    IDE support as a primary feature
    Yusuke Endoh (@mametter)
    RubyKaigi 2023

    View Slide

  2. 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

    View Slide

  3. 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

    View Slide

  4. TypeProf v2:
    Demo with VSCode
    4

    View Slide

  5. 5

    View Slide

  6. 6

    View Slide

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

    View Slide

  8. Agenda
    • Revisiting TypeProf v1
    • TypeProf v2 algorithm
    • Conclusion
    8

    View Slide

  9. Revisiting TypeProf v1
    9

    View Slide

  10. 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

    View Slide

  11. 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

    View Slide

  12. 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?

    View Slide

  13. 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

    View Slide

  14. TypeProf v2:
    Algorithm for IDE support
    14

    View Slide

  15. 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

    View Slide

  16. 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

    View Slide

  17. 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

    View Slide

  18. 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

    View Slide

  19. 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

    View Slide

  20. 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

    View Slide

  21. 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

    View Slide

  22. 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

    View Slide

  23. TypeProf v2:
    Long type propagation
    problem
    23

    View Slide

  24. 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

    View Slide

  25. 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

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

  28. 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)

    View Slide

  29. TypeProf v2: A few details
    29

    View Slide

  30. 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

    View Slide

  31. 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

    View Slide

  32. 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

    View Slide

  33. TypeProf v2:
    Performance evaluation
    33

    View Slide

  34. 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

    View Slide

  35. Related work,
    Conclusion, and future
    35

    View Slide

  36. 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/

    View Slide

  37. 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

    View Slide

  38. 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

    View Slide

  39. One more thing…
    39

    View Slide

  40. 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)

    View Slide