|
| 1 | +# TypeProf: A type analysis tool for Ruby code based on abstract interpretation |
| 2 | + |
| 3 | +## How to use TypeProf as a CLI tool |
| 4 | + |
| 5 | +Analyze app.rb: |
| 6 | + |
| 7 | +``` |
| 8 | +$ typeprof app.rb |
| 9 | +``` |
| 10 | + |
| 11 | +Analyze app.rb with sig/app.rbs that specifies some method types: |
| 12 | + |
| 13 | +``` |
| 14 | +$ typeprof sig/app.rbs app.rb |
| 15 | +``` |
| 16 | + |
| 17 | +Here is a typical use case: |
| 18 | + |
| 19 | +``` |
| 20 | +$ typeprof sig/app.rbs app.rb -o sig/app.gen.rbs |
| 21 | +``` |
| 22 | + |
| 23 | +## How to use TypeProf as a Language Server |
| 24 | + |
| 25 | +See [the slide deck of my talk in RubyKaigi 2024](https://speakerdeck.com/mame/good-first-issues-of-typeprof) for now. |
| 26 | + |
| 27 | +## What is a TypeProf? |
| 28 | + |
| 29 | +TypeProf is a Ruby interpreter that *abstractly* executes Ruby programs at the type level. |
| 30 | +It executes a given program and observes what types are passed to and returned from methods and what types are assigned to instance variables. |
| 31 | +All values are, in principle, abstracted to the class to which the object belongs, not the object itself (detailed in the next section). |
| 32 | + |
| 33 | +Here is an example of a method call. |
| 34 | + |
| 35 | +``` |
| 36 | +def foo(n) |
| 37 | + p n #=> Integer |
| 38 | + n.to_s |
| 39 | +end |
| 40 | +
|
| 41 | +p foo(42) #=> String |
| 42 | +``` |
| 43 | + |
| 44 | +The analysis results of TypeProf are as follows. |
| 45 | + |
| 46 | +``` |
| 47 | +$ ruby exe/typeprof test.rb |
| 48 | +# Revealed types |
| 49 | +# test.rb:2 #=> Integer |
| 50 | +# test.rb:6 #=> String |
| 51 | +
|
| 52 | +# Classes |
| 53 | +class Object |
| 54 | + def foo : (Integer) -> String |
| 55 | +end |
| 56 | +``` |
| 57 | + |
| 58 | +When the method call `foo(42)` is executed, the type (abstract value) "`Integer`" is passed instead of the `Integer` object 42. |
| 59 | +The method `foo` executes `n.to_s`. |
| 60 | +Then, the built-in method `Integer#to_s` is called and you get the type "`String`", which the method `foo` returns. |
| 61 | +Collecting observations of these execution results, TypeProf outputs, "the method `foo` receives `Integer` and returns `String`" in the RBS format. |
| 62 | +Also, the argument of `p` is output in the `Revealed types` section. |
| 63 | + |
| 64 | +Instance variables are stored in each object in Ruby, but are aggregated in class units in TypeProf. |
| 65 | + |
| 66 | +``` |
| 67 | +class Foo |
| 68 | + def initialize |
| 69 | + @a = 42 |
| 70 | + end |
| 71 | +
|
| 72 | + attr_accessor :a |
| 73 | +end |
| 74 | +
|
| 75 | +Foo.new.a = "str" |
| 76 | +
|
| 77 | +p Foo.new.a #=> Integer | String |
| 78 | +``` |
| 79 | + |
| 80 | +``` |
| 81 | +$ ruby exe/typeprof test.rb |
| 82 | +# Revealed types |
| 83 | +# test.rb:11 #=> Integer | String |
| 84 | +
|
| 85 | +# Classes |
| 86 | +class Foo |
| 87 | + attr_accessor a : Integer | String |
| 88 | + def initialize : -> Integer |
| 89 | +end |
| 90 | +``` |
| 91 | + |
| 92 | + |
| 93 | +## Abstract values |
| 94 | + |
| 95 | +As mentioned above, TypeProf abstracts almost all Ruby values to the type level, with some exceptions like class objects. |
| 96 | +To avoid confusion with normal Ruby values, we use the word "abstract value" to refer the values that TypeProf handles. |
| 97 | + |
| 98 | +TypeProf handles the following abstract values. |
| 99 | + |
| 100 | +* Instance of a class |
| 101 | +* Class object |
| 102 | +* Symbol |
| 103 | +* `untyped` |
| 104 | +* Union of abstract values |
| 105 | +* Instance of a container class |
| 106 | +* Proc object |
| 107 | + |
| 108 | +Instances of classes are the most common values. |
| 109 | +A Ruby code `Foo.new` returns an instance of the class `Foo`. |
| 110 | +This abstract value is represented as `Foo` in the RBS format, though it is a bit confusing. |
| 111 | +The integer literal `42` generates an instance of `Integer` and the string literal `"str"` generates an instance of `String`. |
| 112 | + |
| 113 | +A class object is a value that represents the class itself. |
| 114 | +For example, the constants `Integer` and `String` has class objects. |
| 115 | +In Ruby semantics, a class object is an instance of the class `Class`, but it is not abstracted into `Class` in TypeProf. |
| 116 | +This is because, if it is abstracted, TypeProf cannot handle constant references and class methods correctly. |
| 117 | + |
| 118 | +A symbol is an abstract value returned by Symbol literals like `:foo`. |
| 119 | +A symbol object is not abstracted to an instance of the class `Symbol` because its concrete value is often required in many cases, such as keyword arguments, JSON data keys, the argument of `Module#attr_reader`, etc. |
| 120 | +Note that some Symbol objects are handled as instances of the class `Symbol`, for example, the return value of `String#to_sym` and Symbol literals that contains interpolation like `:"foo_#{ x }"`. |
| 121 | + |
| 122 | +`untyped` is an abstract value generated when TypeProf fails to trace values due to analysis limits or restrictions. |
| 123 | +Any operations and method calls on `untyped` are ignored, and the evaluation result is also `untyped`. |
| 124 | + |
| 125 | +A union of abstract values is a value that represents multiple possibilities., |
| 126 | +For (a bit artificial) example, the result of `rand < 0.5 ? 42 : "str"` is a union, `Integer | String`. |
| 127 | + |
| 128 | +An instance of a container class, such as Array and Hash, is an object that contains other abstract values as elements. |
| 129 | +At present, only Array, Enumerator and Hash are supported. |
| 130 | +Details will be described later. |
| 131 | + |
| 132 | +A Proc object is a closure produced by lambda expressions (`-> {... }`) and block parameters (`&blk`). |
| 133 | +During the interpretation, these objects are not abstracted but treated as concrete values associated with a piece of code. |
| 134 | +In the RBS result, they are represented by using anonymous proc type, whose types they accepted and returned. |
| 135 | + |
| 136 | +TODO: write more |
0 commit comments