I thought I'd use a few posts to talk a bit about some interesting ways that IronRuby does things. To do this effectively, we need to have a way to look at the code that IronRuby generates, as just browsing the code isn't going to do much good if you don't know where to start!
Fortunately, the DLR has our backs covered! As I've mentioned in the past, the DLR allows as to see the ASTs used for code generation as well as the Rules used to execute actions, and this is an invaluable tool both to debug our own language implementations, as well as to see what other implementations are doing.
Looking at both ASTs and Rules is easy if you're using the DLR Console either to execute a script in a file or in REPL mode, using the /X:ShowASTs and /X:ShowRules switches, like this:
Creating .NET Objects
One of the strengths of IronRuby is that it allows us to work against not only all the Ruby goodness, but also access a lot of the functionality already available in the .NET Framework [1].
For example, I could create an instance of System.Text.UTF8Encoding like this:
So what happens when we do this? The current implementation of IronRuby will generate an AST that looks like this:
(RubyOps.GetConstantExplicit)(
(RubyOps.GetConstantExplicit)(
(RubyOps.GetGlobalConstant)(
.context,
(SymbolId)System,
),
.context,
(SymbolId)Text,
),
.context,
(SymbolId)UTF8Encoding,
)
(Boolean)False
);
There are several interesting things about this tree. As you can see, the qualified class name is translated to an object through a series of calls to methods in the RubyOps class. The first two calls return RubyModule objects corresponding to the "System" and "System::Text" namespace, while the last one returns a RubyClass object representing the type for the UTF8Encoding class. Then an InvokeMember action is built to call the "new" method in this RubyClass object, which will return the new instance of the UTF8Encoding class.
However, if you go and look at the code for the RubyClass and RubyModule classes, you won't find a "new" method anywhere. What gives?
The trick is, of course, in the IDynamicObject implementation of RubyClass/RubyModule, which receives the InvokeMember action and generates the right set of rules for this case. The method doesn't need to exists, as long as we can resolve the call and build a StandardRule whose target involves the right action/call being executed at runtime. In our simple example, this should mean creating a new instance of UTF8Encoding, which we can see
// AST Rule.Test
//
((((.bound $arg0) == (RubyClass)Ruby.Builtins.RubyClass) && ((RubyClass)Ruby.Builtins.RubyClass.Version ==
15111)) && (Boolean)True)
//
// AST Rule.Target
//
.return .comma {
(.bound val) = .new UTF8Encoding(
(.bound $arg1),
)
(.bound val)
}
The process through which IronRuby builds new instances (or rather, derives the right set of rules for a new expression) is actually fairly complex; even for this simple case. However, in the end, you can think of this process in a simplified way:
- The GetRule() implementation resolves the "new" method call in the RubyClass object by based on a fairly complicated set of lookups around the underlying .NET type and its superclasses and their corresponding RubyClass objects. What matters to us is that, at some point, IronRuby realizes that it really means "create a new instance",
- RubyClass.SetRuleForCreateInstance() is invoked, which will configure the rule correctly for this case.
- Within this, IronRuby notes that UTF8Encoding isn't a class defined in Ruby, but actually a CLR type.
- The set of candidate constructors of the CLR type is evaluated to find the right constructor to use.
- A New Expression (Tree.Ast.New()) is provided as the target of the rule.
So this is how our original InvokeMember action is converted into a new expression! Actually, this is a pretty simple case as far as IronRuby goes, because UTF8Encoding doesn't something corresponding in the Ruby language. To see a different scenario try creating an ArrayList (or other object that implements IList) and you will see a lot more interesting things going on to make the object and its class act ruby-like.
What about my language?
Does this mean you have to do it the same way when implementing your own language on the DLR? Certainly not. There are many ways you can choose to implement it and the DLR actually provides a few ways for that.
For example, in this particular scenario IronRuby is relying on an InvokeMember action, but the DLR also has a Create action which provides another way to represent your object construction. You still need to provide a callable object (IDynamicObject) to provide the rule, but it might not necessarily be very complicated once you resolve the typename to a CLR type.
Of course, the trick is finding
out the right constructor to invoke for a given expression, and fortunately, the DLR provides some help to do this with the MethodBinder class. Here's a pretty minimal (not particularly efficient) implementation you could start with:
rule.AddTest(
Tree.Ast.Equal(rule.Parameters[0], Tree.Ast.Constant(this))
);
Type[] argTypes = new Type[args.Length-1];
for ( int i=0; i < argTypes.Length; i++ ) {
argTypes[i] = CompilerHelpers.GetType(args[i+1]);
}
ConstructorInfo[] all = ClrType.GetConstructors();
MethodBinder methodBinder = MethodBinder.MakeBinder(binder, ".ctor", all);
BindingTarget target = methodBinder.MakeBindingTarget(CallType.None, argTypes);
if ( target.Success ) {
Tree.Expression[] actualArgs = ArrayUtils.RemoveFirst(rule.Parameters);
rule.Target = rule.MakeReturn(
binder,
target.MakeExpression(rule, actualArgs)
);
} else {
rule.Target = rule.MakeError(
Tree.Ast.Call(
GetType().GetMethod("InvalidArgs")
)
);
}
}
[1] The examples in this post assume you're 'require'-ing mscorlib.dll.