Diamondback Ruby
Quick Start
This page gives a quick overview of some of Diamondback Ruby’s (DRuby’s) features. For more comprehensive documentation, see the manual.
The easiest way to install DRuby is to use the RubyGems package manager. To install DRuby, first download the gems for your platform and operating system from the download page. Then install it using the “gem install” command. For example:
$ gem install diamondback-ruby-0.20090415-powerpc-darwin-9.gem Building native extensions. This could take a while… Successfully installed diamondback-ruby-0.20090415-powerpc-darwin-9 1 gem installed
Type the code into your favorite text editor / IDE and save it as “duck1.rb”.
1 class Duck 2 def quack() 3 "quack!" 4 end 5 end 6 d = Duck.new 7 puts d.quack
Now lets run DRuby on the program.
$ druby duck1.rb DRuby analysis complete.
As expected, DRuby did produce any type errors for this simple program. Lets introduce an error to see what happens. Change the last line of duck1.rb to:
7 puts d.qauck
$ druby duck2.rb [ERROR] instance Duck does not support methods qauck in typing method call d.qauck at ./duck2.rb:7 in typing ::Duck.new at ./duck2.rb:6 DRuby analysis complete.
Note that in the error message, DRuby prints not only the location of the errant method call, but also the location of the instantiation of the Duck class that was stored in the variable d.
In Ruby, when we write the expression d.quack, there is nothing that requires the variable d to be an instance of the Duck class. Instead, it can be an instance of any class so long as that class has a quack method. This style of subtyping is commonly referred to as duck typing. DRuby’s analysis was designed around this philosophy and therefore supports many idioms associated with duck typing.
Lets try a slightly larger example:
1 class Duck 2 def quack() 3 "quack!" 4 end 5 def fly!() 6 @in_flight = true 7 end 8 end 9 class GeneticallyEngineeredCow 10 def quack() 11 "moo-ack!" 12 end 13 def tip!() 14 @tipped = true 15 end 16 end 17 18 ducks = [Duck.new, GeneticallyEngineeredCow.new] 19 ducks.each {|d| d.quack}
Here, we have defined two classes, Duck and GeneticallyEngineeredCow which each respond to the quack method. However only ducks can fly, and only cows may be tipped. On line 18, we create the array ducks which contains an instance of each class. Finally, on line 19, we iterate over the array using each calling quack on each element.
When we run DRuby on this code, it does not produce any type errors. This is because while Ducks and GeneticallyEngineeredCows each have different methods, only the quack method is called and DRuby verifies that both classes stored in the array respond to this method. If the last line where changed to
19 ducks.each {|d| d.fly!}
DRuby would report the error:
[ERROR] instance GeneticallyEngineeredCow does not support methods fly! in typing method call d.fly! at ./duck4.rb:19 in typing method call ducks.each at ./duck4.rb:19 in typing expression [_, _] at ./duck4.rb:18 in typing ::GeneticallyEngineeredCow.new at ./duck4.rb:18 DRuby analysis complete.
While DRuby is able to infer types for many Ruby idioms, it sometimes needs a little help for more advanced features, like higher order polymorphism and modeling runtime type tests. Therefore, DRuby includes an annotation language that was inspired by the RDoc documentation system. Annotations are useful documentation in and of themselves, so any method may be documented using this syntax. Here are a few examples:
1 ##% quack : () -> String 2 def quack() 3 "quack!" 4 end 5 6 ##% eat : Food -> NilClass 7 def eat(food) 8 # nom nom 9 end 10 11 ##% color? : Symbol -> Boolean 12 ##% color? : Fixnum -> Boolean 13 def color?(arg) 14 case arg 15 when Symbol: arg == :brown || arg == :white 16 when Fixnum: arg > 0 && arg < 42 17 end 18 end
Monomorphic annotations such as those on lines 1 and 6 are checked statically by DRuby: if the method does not match the signature, it will produce an error when analyzed.
More complex annotations, such as the intersection type on lines 11-12, are currently checked dynamically: we insert code into the Ruby program to detect a type error when the program is executed. This ensures that no statically analyzed code produces a runtime error if this dynamically checked code turns out to be incorrect. This feature is still a little rough around the edges, and so we don’t have much documentation on using it just yet. Check back soon for more details!
While using dynamic checks to verify ruby code is somewhat unsatisfying, we believe it provides a nice way for a program to incrementally benefit from static typing. We can’t possibly statically verify every (correct) Ruby program (it’s undecidable), so this technique provides a back door to force DRuby to accept code it is otherwise unable to analyze, without sacrificing the safety of the rest of its analysis.
We are working on improving DRuby’s analysis to statically check these annotations against their method bodies by recognizing some Ruby constructs specially. For instance, in the above example, DRuby would need to precisely model the case statement on lines 14-17 to ensure the code properly used the argument after testing its type. Unfortunately, there are a myriad of ways to perform such a test in Ruby, and we are working on striking the proper balance between modeling the kind of code programmers tend to write without making the analysis too difficult to understand or predict.
Ruby includes several dynamic features, such as the eval method, which are difficult to analyze statically. For example, consider this example which defines get/set methods for “color” and “weight” using eval:
1 class Duck 2 def initialize() 3 @desc = {} 4 end 5 6 Attributes = %w(color weight) 7 Attributes.each do |attrib| 8 eval "def #{attrib}() @#{attrib} end" 9 eval <<-EOF 10 def #{attrib}=(x) 11 @#{attrib} = x 12 @desc[#{attrib.inspect}] = x 13 end 14 EOF 15 end 16 17 def describe() 18 puts "This is a duck" 19 @desc.each {|k,v| puts "Its #{k} is #{v}"} 20 end 21 end 22 23 d = Duck.new 24 d.color = "blue" 25 d.describe 26 27 if ARGV.length == 17 28 d.height = 20 29 end
If we were to run this program using the standard Ruby interpreter, we would get the following output:
$ ruby duck_eval.rb This is a duck Its color is blue
Note that this program contains a type error: the method call on line 28 is incorrect since the Duck class never defines the height= method. When we ran the program, we did not reach that line of code, and so we don’t know that an error is lurking there.
If we run DRuby on this program, it produces several errors:
[ERROR] instance Duck does not support methods color= in typing method call d.color= at ./duck_eval.rb:24 in typing ::Duck.new at ./duck_eval.rb:23 [ERROR] instance Duck does not support methods height= in typing method call d.height= at ./duck_eval.rb:28 in typing ::Duck.new at ./duck_eval.rb:23 DRuby analysis complete.
While DRuby did detect the real error, it also produced an error for the correct call to color= which was defined using eval. A programmer looking at this output may not look at the code closely and just assume both errors are due to DRuby not being able to handle the eval. In order to model such code, we can instruct DRuby to execute the program internally, watching for calls to eval and seeing what they do. Since running the above code will always execute the eval method (it lives at the top level of the class definition), DRuby will learn about the method definitions for color and weight and analyzes the program as if they were present in the code. This is done by using the —dr-profile command line argument:
$ druby —dr-profile duck_eval.rb [ERROR] instance Duck does not support methods height= in typing method call d.height= at /Users/mike/research/projects/ruby/web/examples/duck_eval.rb:28 in typing ::Duck.new at /Users/mike/research/projects/ruby/web/examples/duck_eval.rb:23 DRuby analysis complete.
DRuby no longer complains about the call to color= since it knows that method exists at runtime, and now only complains about the actual error. While somewhat simple, we believe this technique should work well in practice since most uses of dynamic features appear to follow this same pattern: if they are going to perform a side effect like adding a method, they will do so on every execution. We can also use the program test suite to teach DRuby about additional occurrences of dynamic constructs that may not be present on every run. More details of how to use this feature will be available soon (you can also read our tech report for more formal details about this technique).