In the last installment we discussed how language can express dynamic behavior using ActionExpression and how DLR treats such cases. In short summary, DLR will turn the ActionExpression into sophisticated cache which keeps updating itself as new runtime conditions occur.

We left off at the point where DLR will cry for help when faced with a new runtime condition that it hasn't seen before and doesn't know how to handle. We even saw the actual code that does the crying (the call to "site1.UpdateBindingAndInvoke"), but beyond that all we know that the response comes in some form and that DLR uses the response to learn - to update the code of the delegate handling the dynamic behavior.

Using a little trick that I revealed at the very end of the post, I showed you the actual code of the delegate and we saw that it is very similar to our original attempt to handle dynamic behavior using runtime helper methods.

It is worth reiterating that the key is in the question that DLR asks: "Tell me how to perform the operation!" It is the format of the question which allows DLR to learn - to cache the answers and only ask for help when it has no appropriate cached recipe for handling runtime behaviors.

Rules

In DLR we call these recipes Rules. When DLR asks the question how to perform a given dynamic behavior, it expects a rule back. It is the rule which in the generated code looks like:

//
// Adding strings
//
if (((obj1 != null) && (obj1.GetType() == typeof(string))) &&
    ((obj2 != null) && (obj2.GetType() == typeof(string)))) {

    return StringOps.Add((string)obj1, (string)obj2);

}

The rule consists of a test and a target. Test is a condition which examines the arguments and target is an operation to be performed if the test succeeds. In the code snippet above, the test is the detection whether obj1 and obj2 are both strings and the target is a return of a result of the call to StringOps.Add.

And to close the loop, the rule is expressed as the DLR Tree. When the DLR asks for help determining the exact operation for a given dynamic behavior at runtime, the response comes in the form of a rule which encapsulates two trees. One for the test, the other for the target.

By passing "-X:ShowRules" command line parameter we can have DLR print a trace of all rules created during the program's execution so let's run small ToyScript code:

def add(a, b) {
    return a + b
}

print add("Hello", "ToyScript")

with "-X:ShowRules" and see that for the addition we get following rule as an output:

//
// AST Rule.Test
//

((.bound $arg0) .is String && (.bound $arg1) .is String)

//
// AST Rule.Target
//

.return .comma {
    (.bound $retVal) = (String.Concat)(
        (String)(.bound $arg0),
        (String)(.bound $arg1),
    )
    (Object)(.bound $retVal)
}
;

If you put the test and target together, you can reconstruct the rule's behavior in the C#-like format:

if (arg0 is string && arg1 is string) {

    return String.Concat((string)arg0, (string)arg1);

}

which is exactly what you would expect for string concatenation. Notice that the generated code for the rule that was created when Python executed string addition is slightly different than the rule created for ToyScript addition. If we look at the rules generated when executing Python code, the rule for string addition will trace as:

//
// AST Rule.Test
//

((((.bound $arg0) != .null) && (((.bound $arg0)).(Object.GetType)() == ((Type)String))) &&
(((.bound $arg1) != .null) && (((.bound $arg1)).(Object.GetType)() == ((Type)String))))


//
// AST Rule.Target
//

.return (StringOps.Add)(
    (String)(.bound $arg0),
    (String)(.bound $arg1),
);

yielding exactly the code we saw at the beginning of this post. The difference that may strike you right away is in the rule's test. When we ran ToyScript. the test came out as: "arg0 is string" and with Python, the corresponding part of the test is more complicated: "arg0 != null && arg0.GetType() == typeof(string)". Why the difference, or better yet, why the latter version?

It turns out that when we experimented with different ways to implement the test we realized that JIT (the .NET just-in-time compiler) has an optimization for the latter case and while it looks longer in the source code and in the IL, it will amount to almost nothing in the final executable code. In the case of strings, value types, and generally all sealed types, the two are exactly identical, however the latter - longer - version will check for exact type identity, but "is" operator will succeed if the left operand is a subclass of the right, so there's a subtle difference here also.

The last question we'll ask today is: "Who makes the rules?"

Binders

When DLR requests a rule to be made for a given action which is being invoked with given arguments, it will first call "UpdateBindingAndInvoke" on the dynamic site. These method are in DynamicSite.Generated.cs. At the end of each method is a call to "context.LanguageContext.Binder.UpdateSiteAndExecute". The binder is what the individual language implements to define semantic for the dynamic behaviors determined at runtime.

Not all answers do come from the language binder itself. The binder can simply decide that it doesn't know what to do with given operation on given arguments and pass the task up to the binder base class (ActionBinder) which implements large set of default behaviors, for example behaviors on 'standard' .NET objects such as what it means to get a member "Count" from an instance of System.Collections.Hashtable. It would be silly to expect each language implementer to provide the rules for all possible situations and that's why DLR provides the large default behaviors.

The rule for string addition that we tried with ToyScript is actually implemented in ToyScript, but simply because the DLR default behaviors don't include string addition (in my opinion it is a bug and we'll fix it, but we haven't got to it yet)

The implementation for the string addition in ToyScript is in ToyBinder.MakeRule<T>. You can see that the dynamic action "DoOperation" is singled out and passed to MakeDoRule<T> where addition is singled out and implemented as a construction of the tree that we saw above.

When the language binder receives the request from the DLR to create the rule, it will look at what action is being performed (operation - add, subtract, ..., get member, call, ...) and then examine the arguments being passed in. With all that knowledge the language binder will construct the resulting tree that captures the semantic of the operation. Sometimes the rule is simple (such as our addition above), sometimes it can get pretty complicated. The DLR will cache the rule and only call back again if new circumstances arise.