Microsoft.Dynamic - Getting Started w/ a Language

Want to create a language on top of the DLR?   Well the first thing you need to do is plug into the language side of the hosting APIs.  Once you've done that you'll also discover that you have a bunch of additional functionality which enables you to quickly get started with common tasks such as calling methods, getting members, etc..  Here's just a simple code dump with some additional comments of how to get started.

 First let's go ahead and create our LanguageContext:

public class MyContext : LanguageContext {
    public MyBinder Binder;
    public MyContext(ScriptDomainManager manager, IDictionary<string, object> options) : base(manager) {
        Binder = new MyBinder(manager);
    }
   
    public override ScriptCode CompileSourceCode(Microsoft.Scripting.SourceUnit source, Microsoft.Scripting.CompilerOptions options,Microsoft.Scripting.ErrorSink errorSink) {
        throw new NotImplementedException();
    }
}

For now you're going to have to figure out how to parse and compile code on your own.  But the interesting point I'd like to highlight right now is the MyBinder class which is implemented like this: 

public class MyBinder : DefaultBinder {
    public MyBinder(ScriptDomainManager manager) : base(manager) {
    }
    public override bool CanConvertFrom(Type fromType, Type toType, bool toNotNullable, NarrowingLevel level) {
        return toType.IsAssignableFrom(fromType);
    }
   
    public override Candidate PreferConvert(Type t1, Type t2) {
        return Candidate.Ambiguous;
    }
}

 With this class implemented I can now start doing .NET binding when I'm trying to resolve method calls, member accesses, or other .NET APIs.  But if I'm going to be calling methods (and you'll frequently call methods if you're resolving members - because methods are methods, properties are methods, events are implemented using methods, etc...) then you'll need something which also chooses which overload of a method to call.  For this we have a overload resolver factory and and overload resolver.  You need to give the binder the factory and it'll then create a resolver for a set of methods which need to be resolved:

internal sealed class DefaultOverloadResolverFactory : OverloadResolverFactory {
    private readonly DefaultBinder _binder;

    public DefaultOverloadResolverFactory(DefaultBinder binder) {
        _binder = binder;
    }

    public override DefaultOverloadResolver CreateOverloadResolver(IList<DynamicMetaObject> args, CallSignature signature, CallTypes callType) {
        return new DefaultOverloadResolver(_binder, args, signature, callType);
    }
}

Ok, now we've got a language, a binder, and an overload resolver.  So at this point we're ready to implement a normal DLR binder.  There's a bunch of base classes which can be derived from for the various protocols which are supported by the DLR.   Here I'm going to implement a get member and invoke member binder to give a quick demonstration of how you interact with a DefaultBinder subclass. 

 

public class MyGetMemberBinder : GetMemberBinder {
    private readonly MyBinder _binder;
    public MyGetMemberBinder(MyBinder binder, string name) : base(name, false) {
        _binder = binder;
    }
   
    public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion) {
        var resolver = new DefaultOverloadResolverFactory(_binder);
        var res = _binder.GetMember(Name, target, resolver);
        if (res.Expression.Type.IsValueType) {
            res = new DynamicMetaObject(
                Expression.Convert(res.Expression, typeof(object)),
                res.Restrictions
            );
        }
        return res;
    }
}

public class MyInvokeBinder : InvokeBinder {
    private readonly MyBinder _binder;
   
    public MyInvokeBinder(MyBinder binder, CallInfo callInfo) : base(callInfo) {
        _binder = binder;
    }
   
    public override DynamicMetaObject FallbackInvoke(DynamicMetaObject target, DynamicMetaObject[] args, DynamicMetaObject errorSuggestion) {
        var resolver = new DefaultOverloadResolverFactory(_binder);
        return _binder.Call(new CallSignature(CallInfo.ArgumentCount), resolver, target, args);
    }
}

 

Finally we're ready to put it all together.  We can create a ScriptRuntime which has our language setup, we can get our language context, and then create a few CallSites which will use our newly created binders:

public class Test {
    public static void Main() {
       ScriptRuntimeSetup setup = new ScriptRuntimeSetup();
       setup.LanguageSetups.Add(CreateLanguageSetup());
       var sr = new ScriptRuntime(setup);
       var engine = sr.GetEngine("test");
       var myCtx = (MyContext)HostingHelpers.GetLanguageContext(engine);
       var binder = myCtx.Binder;
      
       CallSite<Func<CallSite, object, object>> getMemSite = CallSite<Func<CallSite, object, object>>.Create(new MyGetMemberBinder(binder, "Length"));
       Console.WriteLine(getMemSite.Target(getMemSite, new[] { 1, 2, 3}));
      
       CallSite<Func<CallSite, object, object, object, object>> invokeSite = CallSite<Func<CallSite, object, object, object, object>>.Create(new MyInvokeBinder(binder, new CallInfo(2)));
       Console.WriteLine(invokeSite.Target(invokeSite, new ParamsDelegate(ParamsMethod), 1, 2));
      
    }
   
    public delegate object ParamsDelegate(params object[] args);
    public static object ParamsMethod(params object[] args) {
        foreach(object o in args) {
            Console.WriteLine(o);
        }
        return 42;
    }
   
    public static LanguageSetup/*!*/ CreateLanguageSetup() {
            var setup = new LanguageSetup(
                typeof(MyContext).AssemblyQualifiedName,
                "test",
                new[] { "test"},
                new[] { ".tst" }
            );

            return setup;
        }
}
 

Viola, we've created the start of a language as far as the DLR is concerned. We don't yet support parsing and compiling code but we can interact with .NET objects.  There's still a bunch of things we could customize on both the DefaultBinder and OverloadResolver to customize how the binding and calls occur.  But that'll have to wait until a future blog.