A Static Type Analyzer of Untyped
Ruby Code for Ruby 3
Yusuke Endoh
RubyConf 2019
19th Nov. 2019
1
Yusuke Endoh (@mametter)
• A Ruby committer:
• Keyword argument design and implementation (2.0)
• Optcarrot: a benchmark for Ruby 3x3
• Ruby 2.0 release manager
• Working at Cookpad Inc. w/ @ko1
2
PR: Cookpad Inc.
• Mission: "Make Everyday Cooking Fun"
• cookpad.com: A recipe sharing service
• Monthly Average Users: 93 million
3
PR: Cookpad Inc.
4
We're hiring!
HQ is in Bristol, UK
Aim to be No.1 in 100 countries
(Now in 30 Languages and 73 Countries)
This Talk: Types in Ruby 3
• Matz's plan for Types in Ruby 3
Multiple type checkers in one blueprint
• Ruby Signature
The standard type signature format for stdlib and gems
• Type Profiler
A type analysis for non-annotated Ruby code
5
This Talk: Types in Ruby 3
•➔ Matz's plan for Ruby 3 Types 
• Ruby Signature
• Type Profiler
6
The Objective of Ruby 3 types
Point out possible bugs without execution
• To improve development experience
(In other words, a few wrong alerts are acceptable)
7
def increment(n)
n.timees { }
n + "STRING"
end
increment(42)
NoMethodError?
TypeError?
Question 🙋
Do you want to write a type annotation?
8
extend T::Sig
sig {params(n:Integer).returns(Integer)}
def increment(n)
n + 1
end
increment(42)
type annotation
(in Sorbet style)
source code
Thank you. Just as I expected
• In Ruby 3, you can write annotations if you like
• You will gain relatively strong type checking
• Moreover, in Ruby 3,
• You don't have to write annotations manually
• You can even check the code with no annotations!
(if my project succeeds ☺)
9
Thank you. That was unexpected
• In Ruby 3, you can write annotations if you like
• You will gain relatively strong type checking
• Moreover, in Ruby 3,
• You don't have to write annotations manually
• You can even check the code with no annotations!
(if my project succeeds ☺)
10
Ruby 3 will have three items
1. Ruby Signature (RBS) language
2. Type inference for non-annotated code
(Type Profiler)
3. Type checking for annotated code
(Sorbet, RDL, Steep, etc.)
I explain 1 and 2 in this talk...
11
This Talk: Ruby 3 types
• Matz's plan for Ruby 3 Types
•➔Ruby Signature
• What it is / Examples / What Ruby 3 will ship
• Type Profiler
12
Soutaro Matsumoto (@soutaro)
• Leads the design and implementation of RBS
• https://github.com/ruby/ruby-signature
• Develops own static type checker "Steep"
• [Correction] No session about Steep this year!
• Working for Square
13
1. Ruby signature language (RBS)
• The standard language to describe types of Ruby programs
• Different syntax: to keep Ruby code unannotated
• Ruby3 will ship with signatures for stdlib and a library for RBS
14
class Inc
def increment(n)
n + 1
end
end
inc.rbs
inc.rb
class Inc
def increment: (Integer) -> Integer
end
separated
files
15
class Array[T]
def []: (Integer) -> T
| (Integer, Integer) -> Array[T]
def first: () -> T?
def each: () { (T) -> void } -> Array[T]
include Enumerable[T, Array[T]]
end
Generics
Optional Types
Overloading
Block
Mixin
interface _Duck
def quack: () -> void
end
Duck typing
Using RBS
• For type-checking
• Static type checking needs signatures of libraries
• Steep uses RBS to define the signature of your Ruby
applications
• For documentation
• RBS explains the API of a gem
16
Ship your gems with RBS
• Scaffold from Ruby code / Sorbet RBI
• Generate using Type Profiler
• [WIP] A tool to test RBS definitions by dynamic type
checking
17
$ rbs scaffold rb # from unannotated Ruby code
$ rbs scaffold rbi # from Sorbet annotated code
This Talk: Ruby 3 types
• Matz's plan for Ruby 3 Types
• Ruby Signature
•➔Type Profiler
• Demo
• Approach
• Problems
18
Type Profiler
A kind of "type inference" for non-annotated code
def increment(n)
n + 1
end
increment(42)
19
github.com/mame/ruby-type-profiler
infer def increment:
(Integer) -> Integer
Type Profiler
Serves as a (weak) type checker
def increment(n)
n.timees { }
n + "STRING"
end
increment(42)
20
[error] undefined method:
Integer#timees
check
[error] failed to resolve
overload: Integer#+(String)
check
Demo
• ao.rb
• optcarrot
21
Demo: ao.rb
• A 3D rendering program
• ~300 lines of code
• Written by Hideki Miura
• Original version (js) was
created by Syoyo Fujita
https://code.google.com/archive/p/aobench/
• Analysis time < 1sec.
22
Demo: ao.rb
23
class Vec
@x : Complex | Float | any
@y : Complex | Float | any
@z : Complex | Float | any
initialize : (Complex, Complex, Complex) -> Complex
| (Complex, Complex, Float) -> Float
…
vnormalize : () -> Vec
vlength : () -> any
vdot : (Vec) -> (Complex | Float)
x : () -> (Complex | Float)
x= : (Complex) -> Complex
| (Float) -> Float
A class signature for
Vec (3D vector)
vector
operations
"any" should be
fixed manually
Three instance variables
Demo: ao.rb
24
class Scene
@spheres : []
@plane : Plane
initialize : () -> Plane
render : (Integer, Integer, Integer) -> Integer
ambient_occlusion : (Isect) -> Vec
end
class Plane
@p : Vec
@n : Vec
initialize : (Vec, Vec) -> Vec
intersect : (Ray, Isect) -> (NilClass | Vec)
Demo: ao.rb
25
class Ray
@org : Vec
@dir : Vec
initialize : (Vec, Vec) -> Vec
dir : () -> Vec
org : () -> Vec
end
class Isect
@t : Complex | Float | any
@hit : Boolean
@pl : Vec
@n : Vec
Demo: ao.rb
• TP generates a good prototype of signatures
• Can be used as a signature with some fixes
• May be also useful for program understanding
• There are some wrong / incomplete guesses
• Due to lack of knowledge of methods, analysis
limitation, etc.
• Some of them can be fixed by TP improvement
26
Demo: optcarrot
• 8-bit machine emulator
• Circuit emulation program
• 5000 LOC
• Author: me ☺
• Analysis time ~ 20 sec.
27
https://eregon.me/blog/2016/11/28/optcarrot.html
Demo: optcarrot
28
class Optcarrot::NES
@conf : Optcarrot::Config
@video : any
@audio : any
@input : any
@cpu : Optcarrot::CPU
@apu : Optcarrot::APU
@ppu : Optcarrot::PPU
initialize : () -> None
end
Three circuit modules:
CPU, APU (Audio), PPU (Graphics)
Failed to detect
other methods
Demo: optcarrot
29
class Optcarrot::APU
...
@pulse_1 : Optcarrot::APU::Pulse
@pulse_0 : Optcarrot::APU::Pulse
@triangle : Optcarrot::APU::Triangle
@noise : Optcarrot::APU::Noise
...
end
Audio processor unit has
four wave generators
Demo: optcarrot
• TP just showed shallow analysis result
• Due to lack of knowledge about many builtin
classes/methods (such as Fiber, etc.)
• Still, it looks useful to create a prototype of
signatures
• TP is never perfect, but I believe it is promising
30
A Key Idea of Type Profiler
Runs a Ruby code in "type-level"
Traditional interpreter
def foo(n)
n.to_s
end
foo(42)
Calls w/
42
Returns
"42"
Type Profiler
def foo(n)
n.to_s
end
foo(42)
Calls w/
Integer
Returns
String
Object#foo ::
(Integer) -> String
31
Type Profiler and Branch
"Forks" the execution
def foo(n)
if n < 10
n
else
"error"
end
end
foo(42)
Fork!
Now here
We cannot tell
if n<10 or not
Object#foo ::
(Integer) ->
(Integer | String)
32
Returns
String
Returns
Integer
Difficulties of Type Profiler
• Requires a starting point
• Needs to be integrated with a test framework
• Cannot analyze some language features
• Is still very preliminary
34
A staring point is required
• TP cannot infer untested methods
35
def inc(n)
n
end
inc(42)
infer
def inc: (Integer) -> Integer
A test code
def inc(n)
n
end
Untested
infer
def inc: (any) -> any
Test framework integration is needed
• Some test may lead to a wrong guess
36
def foo(n)
n+1
end
assert_raise { foo("s") }
infer
def foo: (String) -> any
A test excepts an exception
def bar(n)
n
end
foo(MockObject.new) A test passes a mock object
infer
def bar: (MockObject) -> ...
Difficult features to analyze
• Typically, TP cannot trace Object#send
• Singleton methods, Object#eval, binding, etc...
• You need manually write RBS in this case
37
def inc(n)
n
end
send("inc".to_sym, 42)
infer
def inc: (any) -> any
The Symbol cannot be
determined in type-level
TP is still Preliminary
• Designing TP is harder than MRI-compatible
normal interpreter
• Many features are not supported yet
• Notable unsupported-yet features: Module, and Exception
• The analysis performance must be improved
38
It is developed in one person-year 😖
Help, advice, and contribution are welcome!!!
Related Work
• mruby-meta-circular (Hideki Miura)
• Type Profiler has been inspired by it
• Type Analysis for JavaScript (S. H. Jensen, et al.)
• RDL infer (Jeff Foster et al.)
• An alternative approach to infer types of non-annotated
Ruby code
• Based on traditional type inference with some heuristics
39
Acknowledgement
• Hideki Miura
• Ruby committers: matz, akr, ko1, soutaro
• Katsuhiro Ueno & Eijiro Sumii
• Stripe team & Shopify team & Jeff Foster
40
Conclusion
• Explained Matz's plan for Ruby 3 static analysis
• Introduced Type Profiler
• A type analyzer for Ruby 3
applicable to a non-annotated Ruby code
• Based on abstract interpretation technique
• Little change for Ruby programming experience
• Any comments and/or contribution are welcome!
• https://github.com/mame/ruby-type-profiler
41

A Static Type Analyzer of Untyped Ruby Code for Ruby 3

  • 1.
    A Static TypeAnalyzer of Untyped Ruby Code for Ruby 3 Yusuke Endoh RubyConf 2019 19th Nov. 2019 1
  • 2.
    Yusuke Endoh (@mametter) •A Ruby committer: • Keyword argument design and implementation (2.0) • Optcarrot: a benchmark for Ruby 3x3 • Ruby 2.0 release manager • Working at Cookpad Inc. w/ @ko1 2
  • 3.
    PR: Cookpad Inc. •Mission: "Make Everyday Cooking Fun" • cookpad.com: A recipe sharing service • Monthly Average Users: 93 million 3
  • 4.
    PR: Cookpad Inc. 4 We'rehiring! HQ is in Bristol, UK Aim to be No.1 in 100 countries (Now in 30 Languages and 73 Countries)
  • 5.
    This Talk: Typesin Ruby 3 • Matz's plan for Types in Ruby 3 Multiple type checkers in one blueprint • Ruby Signature The standard type signature format for stdlib and gems • Type Profiler A type analysis for non-annotated Ruby code 5
  • 6.
    This Talk: Typesin Ruby 3 •➔ Matz's plan for Ruby 3 Types  • Ruby Signature • Type Profiler 6
  • 7.
    The Objective ofRuby 3 types Point out possible bugs without execution • To improve development experience (In other words, a few wrong alerts are acceptable) 7 def increment(n) n.timees { } n + "STRING" end increment(42) NoMethodError? TypeError?
  • 8.
    Question 🙋 Do youwant to write a type annotation? 8 extend T::Sig sig {params(n:Integer).returns(Integer)} def increment(n) n + 1 end increment(42) type annotation (in Sorbet style) source code
  • 9.
    Thank you. Justas I expected • In Ruby 3, you can write annotations if you like • You will gain relatively strong type checking • Moreover, in Ruby 3, • You don't have to write annotations manually • You can even check the code with no annotations! (if my project succeeds ☺) 9
  • 10.
    Thank you. Thatwas unexpected • In Ruby 3, you can write annotations if you like • You will gain relatively strong type checking • Moreover, in Ruby 3, • You don't have to write annotations manually • You can even check the code with no annotations! (if my project succeeds ☺) 10
  • 11.
    Ruby 3 willhave three items 1. Ruby Signature (RBS) language 2. Type inference for non-annotated code (Type Profiler) 3. Type checking for annotated code (Sorbet, RDL, Steep, etc.) I explain 1 and 2 in this talk... 11
  • 12.
    This Talk: Ruby3 types • Matz's plan for Ruby 3 Types •➔Ruby Signature • What it is / Examples / What Ruby 3 will ship • Type Profiler 12
  • 13.
    Soutaro Matsumoto (@soutaro) •Leads the design and implementation of RBS • https://github.com/ruby/ruby-signature • Develops own static type checker "Steep" • [Correction] No session about Steep this year! • Working for Square 13
  • 14.
    1. Ruby signaturelanguage (RBS) • The standard language to describe types of Ruby programs • Different syntax: to keep Ruby code unannotated • Ruby3 will ship with signatures for stdlib and a library for RBS 14 class Inc def increment(n) n + 1 end end inc.rbs inc.rb class Inc def increment: (Integer) -> Integer end separated files
  • 15.
    15 class Array[T] def []:(Integer) -> T | (Integer, Integer) -> Array[T] def first: () -> T? def each: () { (T) -> void } -> Array[T] include Enumerable[T, Array[T]] end Generics Optional Types Overloading Block Mixin interface _Duck def quack: () -> void end Duck typing
  • 16.
    Using RBS • Fortype-checking • Static type checking needs signatures of libraries • Steep uses RBS to define the signature of your Ruby applications • For documentation • RBS explains the API of a gem 16
  • 17.
    Ship your gemswith RBS • Scaffold from Ruby code / Sorbet RBI • Generate using Type Profiler • [WIP] A tool to test RBS definitions by dynamic type checking 17 $ rbs scaffold rb # from unannotated Ruby code $ rbs scaffold rbi # from Sorbet annotated code
  • 18.
    This Talk: Ruby3 types • Matz's plan for Ruby 3 Types • Ruby Signature •➔Type Profiler • Demo • Approach • Problems 18
  • 19.
    Type Profiler A kindof "type inference" for non-annotated code def increment(n) n + 1 end increment(42) 19 github.com/mame/ruby-type-profiler infer def increment: (Integer) -> Integer
  • 20.
    Type Profiler Serves asa (weak) type checker def increment(n) n.timees { } n + "STRING" end increment(42) 20 [error] undefined method: Integer#timees check [error] failed to resolve overload: Integer#+(String) check
  • 21.
  • 22.
    Demo: ao.rb • A3D rendering program • ~300 lines of code • Written by Hideki Miura • Original version (js) was created by Syoyo Fujita https://code.google.com/archive/p/aobench/ • Analysis time < 1sec. 22
  • 23.
    Demo: ao.rb 23 class Vec @x: Complex | Float | any @y : Complex | Float | any @z : Complex | Float | any initialize : (Complex, Complex, Complex) -> Complex | (Complex, Complex, Float) -> Float … vnormalize : () -> Vec vlength : () -> any vdot : (Vec) -> (Complex | Float) x : () -> (Complex | Float) x= : (Complex) -> Complex | (Float) -> Float A class signature for Vec (3D vector) vector operations "any" should be fixed manually Three instance variables
  • 24.
    Demo: ao.rb 24 class Scene @spheres: [] @plane : Plane initialize : () -> Plane render : (Integer, Integer, Integer) -> Integer ambient_occlusion : (Isect) -> Vec end class Plane @p : Vec @n : Vec initialize : (Vec, Vec) -> Vec intersect : (Ray, Isect) -> (NilClass | Vec)
  • 25.
    Demo: ao.rb 25 class Ray @org: Vec @dir : Vec initialize : (Vec, Vec) -> Vec dir : () -> Vec org : () -> Vec end class Isect @t : Complex | Float | any @hit : Boolean @pl : Vec @n : Vec
  • 26.
    Demo: ao.rb • TPgenerates a good prototype of signatures • Can be used as a signature with some fixes • May be also useful for program understanding • There are some wrong / incomplete guesses • Due to lack of knowledge of methods, analysis limitation, etc. • Some of them can be fixed by TP improvement 26
  • 27.
    Demo: optcarrot • 8-bitmachine emulator • Circuit emulation program • 5000 LOC • Author: me ☺ • Analysis time ~ 20 sec. 27 https://eregon.me/blog/2016/11/28/optcarrot.html
  • 28.
    Demo: optcarrot 28 class Optcarrot::NES @conf: Optcarrot::Config @video : any @audio : any @input : any @cpu : Optcarrot::CPU @apu : Optcarrot::APU @ppu : Optcarrot::PPU initialize : () -> None end Three circuit modules: CPU, APU (Audio), PPU (Graphics) Failed to detect other methods
  • 29.
    Demo: optcarrot 29 class Optcarrot::APU ... @pulse_1: Optcarrot::APU::Pulse @pulse_0 : Optcarrot::APU::Pulse @triangle : Optcarrot::APU::Triangle @noise : Optcarrot::APU::Noise ... end Audio processor unit has four wave generators
  • 30.
    Demo: optcarrot • TPjust showed shallow analysis result • Due to lack of knowledge about many builtin classes/methods (such as Fiber, etc.) • Still, it looks useful to create a prototype of signatures • TP is never perfect, but I believe it is promising 30
  • 31.
    A Key Ideaof Type Profiler Runs a Ruby code in "type-level" Traditional interpreter def foo(n) n.to_s end foo(42) Calls w/ 42 Returns "42" Type Profiler def foo(n) n.to_s end foo(42) Calls w/ Integer Returns String Object#foo :: (Integer) -> String 31
  • 32.
    Type Profiler andBranch "Forks" the execution def foo(n) if n < 10 n else "error" end end foo(42) Fork! Now here We cannot tell if n<10 or not Object#foo :: (Integer) -> (Integer | String) 32 Returns String Returns Integer
  • 33.
    Difficulties of TypeProfiler • Requires a starting point • Needs to be integrated with a test framework • Cannot analyze some language features • Is still very preliminary 34
  • 34.
    A staring pointis required • TP cannot infer untested methods 35 def inc(n) n end inc(42) infer def inc: (Integer) -> Integer A test code def inc(n) n end Untested infer def inc: (any) -> any
  • 35.
    Test framework integrationis needed • Some test may lead to a wrong guess 36 def foo(n) n+1 end assert_raise { foo("s") } infer def foo: (String) -> any A test excepts an exception def bar(n) n end foo(MockObject.new) A test passes a mock object infer def bar: (MockObject) -> ...
  • 36.
    Difficult features toanalyze • Typically, TP cannot trace Object#send • Singleton methods, Object#eval, binding, etc... • You need manually write RBS in this case 37 def inc(n) n end send("inc".to_sym, 42) infer def inc: (any) -> any The Symbol cannot be determined in type-level
  • 37.
    TP is stillPreliminary • Designing TP is harder than MRI-compatible normal interpreter • Many features are not supported yet • Notable unsupported-yet features: Module, and Exception • The analysis performance must be improved 38 It is developed in one person-year 😖 Help, advice, and contribution are welcome!!!
  • 38.
    Related Work • mruby-meta-circular(Hideki Miura) • Type Profiler has been inspired by it • Type Analysis for JavaScript (S. H. Jensen, et al.) • RDL infer (Jeff Foster et al.) • An alternative approach to infer types of non-annotated Ruby code • Based on traditional type inference with some heuristics 39
  • 39.
    Acknowledgement • Hideki Miura •Ruby committers: matz, akr, ko1, soutaro • Katsuhiro Ueno & Eijiro Sumii • Stripe team & Shopify team & Jeff Foster 40
  • 40.
    Conclusion • Explained Matz'splan for Ruby 3 static analysis • Introduced Type Profiler • A type analyzer for Ruby 3 applicable to a non-annotated Ruby code • Based on abstract interpretation technique • Little change for Ruby programming experience • Any comments and/or contribution are welcome! • https://github.com/mame/ruby-type-profiler 41