Note: This article is part of a series exploring how Laser, my Ruby static analysis tool, can help improve the quality of Ruby code. I'm presenting on this at RubyConf 2011 about it, and you should come!
Multiple Assignment
Ruby has a simple construct which is very simple to get wrong: multiple assignment. A full explanation of the multiple assignment construct can be found at the Read Ruby 1.9 project. For those familiar with multiple assignment, the problem is that it is completely unchecked. There are two types of errors with multiple-assignment: static errors, and dynamic errors.
Static Errors
For example, the following code:
a, b, c = 1, 2
is perfectly valid, and Ruby will not warn against it. It will simply assign nil to c. Similarly, the following code discards the extra value on the right-hand side:
a, b = 1, 2, 3
These are dead-simple errors to detect, as the error is a part of the AST. This isn't a runtime issue, it's just wrong. One similar construct that Laser warns about is this:
a, *arr, b = 1, 2
In that code, arr is just []. You should write arr = [] if you really want that. This is also dead-simple to detect. Laser will warn about these; here's a simple example session:
$ cat temp.rb class Hello def foo(x, y, z) a, b = x, y, z c, d, e = x, y j, *arr, k = x, y end end Hello.new.foo(gets, gets, gets) $ bundle exec bin/laser temp.rb (stdin):3 Error (6) - RHS value being discarded (stdin):4 Error (6) - LHS (e) never assigned - defaults to nil (stdin):5 Error (6) - LHS splat (arr) is always empty ([]) - not useful
Three small comments before we continue.
First, these errors are generated when the foo method is compiled to a control-flow graph. That's because, as I mentioned, the errors are built-in to the AST. However, I still had to call the method, or the method never would have been compiled; Laser doesn't do work it doesn't have to.
Secondly, the (stdin) there is just a temporary bug.
Thirdly, there actually were a lot more warnings - Laser warned about all the unused variables in foo. But I cut those out.
Anyway, there's not much to say - here, Laser even gives you the name of the variables that are being assigned default values. Nifty.
Dynamic Errors
That's all well and good, but those errors are dead-easy to find and fix. They're practically greppable. What gets a lot tougher is when you use a variable-sized right-hand-side in assignment, such as:
a, b, c, d = 1, 2, *foo x, y, z = bar
In those two examples, if foo returns 0 or 1 elements, then there will be unassigned LHS variables. Also, if foo returns more than 2 elements, then there will be discarded values. Chances are, that's an error, or at least worth telling the user. Similar issues arise with x, y, z = bar.
Laser uses its type system to figure these errors out. Now, I haven't written much about how Laser handles types, because I didn't want to scare off any Rubyists. You should rarely need to be aware of the type system being used in order to use Laser and get benefits out of it, and there is no reason to consider Laser an affront to dynamic typing and duck typing.
That said, Laser attempts to pick out which array objects in a Ruby program are actually tuples - a sequence of fixed length. Ruby's array literal syntax - [1, 2, 3] - introduces a tuple, for example. Laser sees these literals, and instead of treating them as any old array, treats them as a tuple, tracking the type of each sub-element as well as the length of the array. It also provides special information to the typer so it knows when you add two tuples with +, it can combine them, and when you call some_tuple.size, it returns the exact size, and so on.
What's that mean? It means when Laser sees arrays of known size, and they end up on the right-hand side of an assignment, it knows whether you screwed up:
$ cat temp.rb class Foo def initialize(x, y, z) a, b, c, d = x, y, *foo(z) j, k, l = bar(z) end def foo(z) [z] * 3 end def bar(z) [z, z] end end Foo.new(gets, gets, gets) $ bundle exec bin/laser temp.rb (stdin):3 Error (6) - RHS value being discarded (stdin):4 Error (6) - LHS never assigned - defaults to nil
Cool! Naturally, tuples are everywhere in Ruby, especially when you see a splat. For example, variable argument unpacking might have an error:
$ cat temp.rb class Foo def initialize(x, *args) a, b = args[0..1] end end Foo.new(gets, gets) $ bundle exec bin/laser temp.rb (stdin):3 Error (6) - LHS never assigned - defaults to nil
The above example I particularly like, because Laser actually implements Class#new as the following:
class Class < Module def new(*args, &blk) result = allocate result.send(:initialize, *args, &blk) result end end
So when Laser analyzed the previous case, it:
- Saw
Foo.new(gets, gets) - Packed those arguments up as a 2-tuple
- sent it to
Class#new - Looked at the definition of
Class#new(as Ruby code) and saw a call tosendwith the 2-tuple splatted into the arguments - Looked at how to handle
sendusing some internal machinery - Extracts all but the first argument to
sendand passes that as the arguments toinitialize - Separates out the arguments according to Ruby 1.9's method argument rules
- Observes that the length of
argsis 1, so when it is indexed with0..1, it returns a 1-tuple - Sees that the RHS does not have exactly 2 elements, resulting in an error.
Laser has a lot of work ahead of it, but seeing real errors detected in real Ruby files with the actual binary is pretty damn exciting. Stay tuned!

Enjoy this article? Then feel free to: