Getting now to the second question that Ales asked:

" ... As an example let's take Print function .. where I want to print different data types - integers, strings, decimals etc and each type should be printed differently. Instead of doing the same as you did in previous article, having one method and testing if-elseif-elseif..else  for all the types, is it possible to use ActionExpression for calling a method instead of Ast.Call()? I hope ActionExpression could give better performance and caching for this, but I didn't figure out how to do this..."

Good news is that the answer is yes (you can take advantage of dynamic caching even in situations such as this one), but there's a bit of a bad news, which is that while there are few good ways to do it, the perfect way that we envision is not yet implemented, but hopefully will be soon.

Problem One

The first problem is representing the dynamic operation as an Action, and that is where the answer is not quite perfect yet.

Action in DLR represents encoding of the dynamic operation. You can think of it as a "dynamic instruction" of sorts. And as of right now, DLR only recognizes fixed set of Actions. The main kinds are (look into DynamicAction.cs for the enum and the DynamicAction base class):

  • DoOperation
  • ConvertTo
  • GetMember
  • SetMember
  • DeleteMember
  • InvokeMember
  • Call
  • CreateInstance

Each enum value above then has corresponding class which contains the encoding of the given dynamic operation. For DoOperations, the encoding of the operation is as simple as another enum value (from Operators.cs). There are quite a few of those, including unary and binary operators, slicing, indexing (Get/Set -Item) and many more. The general rule here is that the enum is enough to encode the operation. For our problem, there's one that could somewhat fit, but the fit may not be perfect: CodeRepresentation (inspired by Python's __repr__).

The other kinds of actions are represented via their own classes, depending what data is part of the action's encoding. For example, all *Member actions must contain the name of the member in question, but there is no deep need for actually having 3 classes for Get/Set/Delete, anyway, that's area we are still actively working on and it'll get better.

Invoke/Call/Create instance are little more complicated because they also encode information about the call signature, whether the call includes a dictionary contents of which are mapped to callee's parameters etc. Invoke is combination of "GetMember" and Call:

a.b(c,d,e)

but done all at once. Python, for example, cannot use Invoke because in Python the above must be exactly equivalent to:

temp = a.b
temp(c,d,e)

The Python rules dictate that a.b be evaluated before any of the arguments. Invoke combines these two operations for languages whose semantic allows this.

... But that was a little digression ...

One option we have to encode our "format for printing" dynamic operation is via the DoOperation(CodeRepresentation), another would be to encode it as a call to a method which is to be found on the object (or its type) which will do the encoding. For example Call("FormatForPrinting").

Using DoOperation(CodeRepresentation)

Let's actually try to implement this. ToyScript has a keyword print which we can use to try this approach. Currently the print is transformed into DLR tree as:

Ast.Statement(
    Span,
    Ast.Call(
        typeof(ToyHelpers).GetMethod("Print"),
        Ast.CodeContext(),
        Ast.ConvertHelper(
            _expression.Generate(tg),
            typeof(object)
        )
    )
);

In other words, as a call to ToyHelpers.Print. Instead, we could change that to the following:

Ast.Statement(
    Span,
    Ast.Call(
        typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }),
        Ast.Action.Operator(Operators.CodeRepresentation,
            typeof(string),
            _expression.Generate(tg)
        )
    )
);

where we take the argument to the print keyword (_expression), perform dynamic operation "CodeRepresentation" on it and simply pass the result to Console.WriteLine(string). Notice that the dynamic operation itself is typed as string which means it will always produce string, allowing for tighter code generation and faster code especially if value types are involved as it can save boxing/unboxing.

Of course, this is the easy part. If we try to run the ToyScript code:

import System

def f(x) {
    print x
}

f(1)
f("Hello")
f(new System.Decimal(10))

we'll get an exception:

image

The reason is that we haven't yet provided instructions to the DLR how to perform the operation. Obviously, the default DLR attempts to do something about what ToyScript requested have failed and the DLR dynamic binder pretty much produced tree which just throws exception:

//
// AST Rule.Test
//

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

//
// AST Rule.Target
//

.throw ((RuntimeHelpers.BadArgumentsForOperation)(
    (Operators)CodeRepresentation,
    .new Object[] = {
        (.bound $arg0),
    },
))

We have to go to ToyScript's dynamic binder and give some instructions as to what it means to perform CodeRepresentation. Before we do that, let's actually implement a really simple formatter. A class with bunch of static methods that do the formatting for the types we want to handle explicitly. This may be a trivial first attempt:

public static class Formatter {

    public static string Format(double v) {
        return "DOUBLE: " + v;
    }

    public static string Format(string s) {
        return "STRING: " + s;
    }

    public static string Format(decimal d) {
        return "DECIMAL: " + d;
    }

}

Really simple. Then, in the ToyBinder, we implement the rules for formatting. I put this code into MakeDoRule<T> right after the string addition rule:

if (action.Operation == Operators.CodeRepresentation) {
    Type argumentType = args[0].GetType();

    StandardRule<T> rule = new StandardRule<T>();

    // Create test: arg0 is argumentType
    rule.Test = Ast.TypeIs(
        rule.Parameters[0],
        argumentType
    );

 

    // Create the target:

    // 1. Look for the "Format" method with perfectly matching signature
    MethodInfo mi = typeof(Formatter).GetMethod("Format", new Type[] { argumentType });

    Expression target;

    if (mi != null) {
        // Call the custom formatter
        target = Ast.Call(
            mi,
            Ast.Convert(rule.Parameters[0], argumentType)
        );
    } else {
        // Call the ((object)arg0).ToString()
        target = Ast.Call(
            Ast.Convert(rule.Parameters[0], typeof(object)),
            typeof(object).GetMethod("ToString")
        );
    }

    // Finish the rule and return it
    rule.Target = rule.MakeReturn(this, target);
    return rule;

}

Pretty much what we do here is look into our Formatter class for a perfectly matching overload of "Format". If we find one, good, call that, otherwise fallback to Object.ToString(). Now we can run our code and see output:

image

If we pass something else to our formatting function "f", our binder falls back to Object.ToString. Suppose we add this to our ToyScript script and run again:

f(new System.Collections.ArrayList())

image

And it worked again :) Really simple example, but if we dig into the saved assembly (must run with
-D -X:SaveAssemblies -X:StaticMethods) we'll find the generated code:

image

Note here that I had to play the same trick I played in one of the first posts where if the dynamic site sees a type once and never again, it won't actually generate the more complicated "if" statement. To work around this optimization, I ran the four lines of code twice in a row to get this behavior. In reality, this is really what we don't want programs to do :)

So this approach does work. In the next post we'll explore a second option how to handle the first problem - trying to encode the string formatting as an invocation of a method which each object should have. Of course, we'll hit a problem that not every object has the method we want to invoke. There is no "OurCustomFormat" on Int32 or Double, is there? But as we'll see, DLR has a way to help us with that also.

Then we will need to address the second problem that arises from the question, which we haven't even identified so stay tuned, we'll get to it all.

Until then, happy hacking.