This is part of a series I started in March 2008 - you may want to go back and look at older parts if you're new to this series.
Eigenclasses, or meta-classes in Ruby are effectively regular classes that gets "injected" in the inheritance
chain, but hidden when calling #super
and #class
(Ruby is sneaky like that; I'm not convinced
I like that part). For more on eigenclasses there's _why's excellent old article
Fixing this is deceptively easy, at least for the basic case. Lets get this example working first:
class Foo
def self.bar baz
puts baz
end
end
Foo.bar("Hello world")
def self.bar
basically means (using the convention that "#klass" refers to the real class of the
object, rather than whatever #class
returns):
"if self.klass is not an eigenclass, then make it one (create a new subclass of self.klass that
is marked as an eigenclass). Define #bar
as a method on self.klass". Note that "self" in this case
is the Class
object Foo
, so Foo.klass
will be Class
, and the new eigenclass that we define a method on will be a subclass of Class
, not a subclass of Foo
.
Similarly, the alternative syntax (which we'll get back to later) of "class <
(where green represents Class
objects)
This represents this:
class Foo
end
Foo.new
Now we want to do the def self.bar
from above. The resulting objects should look like this:
Consider if we instead did:
class Foo
end
ob = Foo.new
def ob.bar
end
In that case, we'd expect #bar
to get defined on an Eigenclass that pops into the hierarchy like this:
Notice how the Eigenclass for the non-class case gets introduced as the new class of the instance.
The object isn't "really" an instance of Foo
any more, but an instance of the eigenclass, which
again is inherited from Foo
.
Conceptually it's exactly the same. It's just a matter of which object we introduced a new method on, and therefore which object we attached an eigenclass to.
We'll not deal with this case any further in this part. We'll wrap that up later. Especially as instance
specific eigenclasses is rare (because they should be used sparingly: they are expensive, causing a
Class
object per object you attach methods to).
First we get 51c003f out of the way, which simply expands the Scope
class slightly to forward
and provide defaults for more cases. Most of these will be made use of later.
Then let's start to hook in the actual eigenclass treatment. We're now aiming for handle the
def self.foo
case, which the parser will deliver as [:defm, [:self, :foo], ...]
.
All of the following is in 8ad4e6f
def compile_defm(scope, name, args, body)
scope = scope.class_scope
+ if name.is_a?(Array)
+ compile_eigenclass(scope, name[0], [[:defm, name[1], args, body]])
+ return Value.new([:subexpr])
+ end
compile_eigenclass
looks like this:
+ def compile_eigenclass(scope, expr, exps)
+ @e.comment("=== Eigenclass start")
+
+ ob = [:index, expr, 0]
+ ret = compile_eval_arg(scope, [:assign, ob,
+ [:sexp, [:call, :__new_class_object, [scope.klass_size, ob, scope.klass_size]]]
+ ])
+ @e.save_result(ret)
+
+ let(scope,:self) do |lscope|
+ @e.save_to_local_var(:eax, 1)
+
+ # FIXME: This uses lexical scoping, which will be wrong in some contexts.
+ compile_exp(lscope, [:sexp, [:assign, [:index, :self ,2], "<#{scope.local_name.to_s} eigenclass>"]])
+
+ exps.each do |e|
+ compile_do(lscope, e)
+ end
+ @e.load_local_var(1)
+ end
+ @e.comment("=== Eigenclass end")
+
+ return Value.new([:subexpr], :object)
+ end
+
Basically, we create a new class object, save the pointer to it as a local variable temporarily,
and alias self
. We then assign a name to the class, and compile methods in that new context.
This is all fairly similar to compile_class
, and there might be opportunities to combine the
two more later (and there's almost certainly holes/bugs in compile_eigenclass
as it stands.
There's also one big inefficiency: If you define multiple methods, you get multiple eigenclasses chained. That's ok. It's just wasteful, and potentially slow. It's easier to fix that once I get around to adding an easier way of identifying an eigenclass.
But what is the let
method above?
Basically, I needed a simple way of defining a local scope, and allocating stack space for it.
compile_let
did that, but only for code in tree form. So the new let
is a rewrite that
extracts out that part of the code, and leaves compile_let
a tiny little stub:
def let(scope,*varlist)
vars = Hash[*(varlist.zip(1..varlist.size)).flatten]
lscope =LocalVarScope.new(vars, scope)
if varlist.size > 0
@e.evict_regs_for(varlist)
@e.with_local(vars.size) do
yield(lscope)
end
@e.evict_regs_for(varlist)
else
yield(lscope)
end
end
# Compiles a let expression.
# Takes the current scope, a list of variablenames as well as a list of arguments.
def compile_let(scope, varlist, *args)
let(scope, *varlist) do |ls|
compile_do(ls, *args)
end
return Value.new([:subexpr])
end
Before we move on to some minor supporting changes in the runtime library, there was one bug that slowed me down substantially I want to briefly mention:
compile_assign
was the only user of what at some point became a severely broken
mechanism for saving register content. With the proper register allocation there's
no excuse for it any more, and so Emitter#save_register
has to go. The problem
was that if you indicated a register should be saved, it didn't properly mark it
as freed up again, and so we got spurious pushl
's onto the stack in positions
which meant you could push and pop values in the wrong order The replacement in
`compile_assign is sometimes less efficient, but also less broken.
It also cuts some lines out of emitter.rb
The bootstrapping of the object model is still a bit messy. One aspect that has been
there "forever" but which became more obvious when working on this part, is the chicken
and egg problem with Class
and Object
. With the support for re-opening classes,
we have the mechanism for making the situation better.
This change in 1e1ce21 improved by explicitly "manually" link Class
into the list
of subclasses of Object
. This means Class
will properly inherit methods of Object
after all, and let us clean up Class
next.
class Object
+ # At this point we have a "fixup to make as part of bootstrapping:
+ #
+ # Class was created *before* Object existed, which means it is not linked into the
+ # subclasses array. As a result, unless we do this, Class will not inherit methods
+ # that are subsquently added to Object below. This *must* be the first thing to happen
+ # in Object, before defining any methods etc:
+ #
+ %s(assign (index self 4) Class)
+
The above let us strip out the suprfluous Class#==
and depend on Object#==
in 4c0c349.
At the same time (and same commit) we fix Class#to_s
and Class#inspect
to use the ugly Class#name
. We also "manually" set the class of Class
to Class
, and it's name to the raw string "Class"
These changes leave us with what's needed to compile almost all of the tokenizer code pretty much unmodified, and so we're no rapidly closing on the initial goal of compiling the s-exp subset of the parser. We'll look at the next batch of changes for that next.
However I will make a change going forwards. After part 44 or 45, rather than batching up changes and trying to cover specific subjects, I'll be pushing out changes as soon as I have something ready to commit, and will mention the commits I think are worth while discussing in much shorter posts. I'll then follow it up with longer articles covering specific areas of the compiler or larger changes more rarely as I think I have something more worthwhile about a specific component.
The main reason is that it takes a lot of effort to write these larger posts - 2-3 times as long as actually making the changes (including time to restructure some of the commits to make more sense for the articles), and frankly I want to make faster progress on actually being able to use the compiler.
This will also simplify my git handling substantially - currently I manage two repos in addition to my local working copies - one for my drafts, and the public one on Github. Going forwards I'll push changes to the Github repo right away. Branches will also no longer reflect a specific article.
I hope this will lead to more interesting development rather than fewer, though recent parts of this series have been very much a mixed bag of commits that are only related by the what goal I've been working towards rather than what code they've touched anyway.