Slide 1

Slide 1 text

Revisiting TypeProf: IDE support as a primary feature Yusuke Endoh (@mametter) RubyKaigi 2023

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

TypeProf v2: Demo with VSCode 4

Slide 5

Slide 5 text

5

Slide 6

Slide 6 text

6

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Revisiting TypeProf v1 9

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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?

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

TypeProf v2: Algorithm for IDE support 14

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

TypeProf v2: Long type propagation problem 23

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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)

Slide 29

Slide 29 text

TypeProf v2: A few details 29

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

TypeProf v2: Performance evaluation 33

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Related work, Conclusion, and future 35

Slide 36

Slide 36 text

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/

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

One more thing… 39

Slide 40

Slide 40 text

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)