Share
Embed
Transcript
40 slides
-
Revisiting TypeProf: IDE support as a primary feature Yusuke Endoh
(@mametter) RubyKaigi 2023 -
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 -
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 -
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 -
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 -
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 -
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? -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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) -
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 -
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 -
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 -
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 -
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/ -
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 -
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 -
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)