How does the programming language express dynamic behaviors in the DLR? So far most of the trees we saw were statically and strongly typed. For example, by constructing tree such as:

Ast.Add(
    Ast.Constant(1),
    Ast.Constant(2)
);

we are expressing fully fully static behavior - adding two integer constants with the result also being an integer. What about adding arbitrary objects, just like in the ToyScript function from the end of the last post:

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

... a function that adds any two things we'll pass to it. If you read the last post or experimented with ToyScript or any other DLR language, then you probably know that ToyScript will generate an ActionExpression in the place of the addition. We'll get to that soon enough. For now let's explore other means we have for expressing runtime dynamic behaviors, something we have tools for already: calling a helper method.

The compiler could turn the addition (or any other dynamic operation for that matter) into a method call. Since we don't know the types of the arguments or the result ahead of time, the helper method ToyHelpers.Add can simply take and return object types:

public static object Add(object a, object b) {
    throw new NotImplementedException();
}

And in the Binary.Generate let's single out the addition to implement it as a call to the ToyHelpers.Add method:

if (_op == Operator.Add) {
    return Ast.Call(
        typeof(ToyHelpers).GetMethod("Add"),
        Ast.ConvertHelper(left, typeof(object)),
        Ast.ConvertHelper(right, typeof(object))
    );
}

The generated tree for the function "add" above will of course look as follows (running "ts.exe -X:ShowASTs add.ts"):

//
// CODE BLOCK: add (1)
//

.codeblock Object add ()(
    .arg Object a (Parameter,InParameterArray)
    .arg Object b (Parameter,InParameterArray)
) { 
    {
        .return (ToyHelpers.Add)(
            (.bound a),
            (.bound b),
        );
    }
}

If we actually call the function "add" within the ToyScript program, we'll encounter the exception thrown from within the helper, no surprise here, but we can quickly get a simple program running. Here's our simple program:

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

print add(1, 2)
print add("Hello ", "ToyScript")

Let's place breakpoint inside the ToyHelper.Add because that's where we place need to implement the actual addition:

image

ToyScript is really simple language so all numeric constant simply become doubles. We can now implement the addition for doubles, and add strings while we are at it:

if (a is double && b is double) {
   
return (double)a + (double)b;
}
if (a is string && b is string) {
    return (string)a + (string)b;
}

throw new NotImplementedException();

This makes our program work:

C:\ToyScript\Bin\Debug > ts.exe add.ts
3
Hello ToyScript

But only for numbers and strings. Other objects cannot be added by our implementation. We could pick few others and implement them in the spirit of double and string, but there will be still plenty to go around. Here's an idea ... we could detect an overloaded operator addition on the .NET objects and call that if we saw one.

The code to add into our Add helper could look something like the following:

if (a != null && b != null) {
    Type aType = a.GetType();
    Type bType = b.GetType();

   
MethodInfo mi = aType.GetMethod(
        "op_Addition",
        new Type[] { aType, bType }
    );

   
if (mi != null) {
       
return mi.Invoke(null, new object[] { a, b });
    }
}

We made few simplifications, of course. We are looking only for "exact match" operators, whose signature is exactly matching the arguments' types and we only examine the left argument for overloaded operator, but you get the idea.

With our new code we can add instances of System.Decimal if we append the following code to our "add.ts" source:

import System

ten = new System.Decimal(10)
twenty = new System.Decimal(20)

print add(ten, twenty)

And running the whole script will produce:

C:\ToyScript\Bin\Debug> ts.exe add.ts
3
Hello ToyScript
30

We could have the script print the type of the result to see that the "30" at the end is indeed an instance of System.Decimal.

With little effort we taught the ToyScript to add by simply calling and implementing a runtime helper method ToyHelpers.Add, and it works quite well. Unfortunately, there are two disadvantages to our approach:

First, we can only single out a finite number of types to handle the way we handled double and string. We could add int, long, char, lists, tuples (if ToyScript supported them) and the sequence of "if" statements in our helper would grow and so would the number of possible type tests we'd need to perform before finding the right one.

This is actually what IronPython did/does in versions 1.x. If program uses mostly types which are at the bottom of our test, the helper will waste a lot of time just testing for types.

Second disadvantage is that the final attempt - finding the overloaded operator "op_Addition" is implemented as a reflected call, which is rather slow.

It won't be until the next post where we'll finally explore how DLR addresses both of these issues at the same time and where we'll see ActionExpressions finally coming to the picture. I hope you can wait until tomorrow for that.