Implementing a Language in C# - Part 6: Runtime Intorduction

Implementing a Language in C# - Part 6: Runtime Intorduction

This post is part of a series entitled Implementing a Language in C#. Click here to view the first post which covers some of the preliminary information on creating a language. Click here to view the last post in the series, which covers edge cases of the parser. You can also view all of the posts in the series by clicking here.

We have now arrived at the point everyone has been waiting for. This is the point where everything from the lexer and the parser is turned into actual, meaningful code. This post covers some of the preliminary information relating to the runtime. This post will be light on code and heavy on conceptual information as the runtime is not yet ready. Currently, the only features working are CLR interop and expressions.

The first thing that must be done is to create a place to house all of the important runtime logic. This, in the case of GlassScript, will be called the GlassScriptEnvironment. This object will act as the central place for interpreting and any runtime helpers that may need to be defined.

The goal of building this runtime is to create a REPL. The frontend code for this REPL is actually quite simple:

while (true)
{
    Console.Write("GlassScript> ");
    var program = Console.ReadLine();
    Console.WriteLine(GlassScript.Execute(program));
}

A little bit more code to setup a REPL library:

GlassScript.GlobalEnvironment.SetValue("console", typeof(Console));
GlassScript.GlobalEnvironment.SetValue("exit", new Action<int>(Environment.Exit));

while (true)
...

This makes our REPL program a whole 8 lines of code! You'll notice the call to SetValue takes a variable name and an object. If you pass a System.Type, the runtime will use static methods; a Delegate, the runtime will infer a method; a GlassScript object causes the runtime to just assign the value; and anything else, the runtime will wrap the object in a proxy.

Another important point is to decide how your script code will interact with itself and the outside CLR world. I decided that I wanted to use the CLR's type system as this would save me tonnes of time writing wrapper utilities like I did in PineTree. This means that a GlassScript int is the same as the CLR's int. All calls on a CLR object are passed through the GlassScriptClrProxy class which implements the contract of any other GlassScript object and sits in between the GS and CLR worlds.


At this point, I feel that Microsoft might run me over with a bus if I don't at least mention the Dynamic Language Runtime. The DLR was a project born from a similar attempt at implementing a dynamic scripting language on the CLR: IronPython. The DLR provides most of the backend of the interpreter minus some runtime helpers and an implementation of the DynamicObject. I specifically chose not to implement GlassScript on the DLR because I do not yet know enough about its APIs to correctly implement a language using it. I have, however, followed the same general naming scheme so that it would be simpler to port GlassScript to the DLR if I decide. We will be making use of the DLR's Dynamic Dispatch capabilities to massively save on interop code.

Here is an example:

private dynamic DoDynamicOperation(BinaryOperator op, dynamic a, dynamic b)
{
    try
    {
        switch (op)
        {
            case BinaryOperator.Add: return a + b;
            case BinaryOperator.BitwiseAnd: return a & b;
            case BinaryOperator.BitwiseOr: return a | b;
            case BinaryOperator.BitwiseXor: return a ^ b;
            case BinaryOperator.Div: return a / b;
            case BinaryOperator.Equal: return a == b;
            case BinaryOperator.GreaterThan: return a > b;
            case BinaryOperator.GreaterThanOrEqual: return a >= b;
            case BinaryOperator.LeftShift: return a << b;
            case BinaryOperator.LessThan: return a < b;
            case BinaryOperator.LessThanOrEqual: return a <= b;
            case BinaryOperator.LogicalAnd: return a && b;
            case BinaryOperator.LogicalOr: return a || b;
            case BinaryOperator.Mod: return a % b;
            case BinaryOperator.Mul: return a * b;
            case BinaryOperator.NotEqual: return a != b;
            case BinaryOperator.RightShift: return a >> b;
            case BinaryOperator.Sub: return a - b;
        }
    }
    catch (Exception e)
    {
        throw new RuntimeException(e.Message, e);
    }
    return null;
}

Core Concepts

Scope

GlassScript needs something to hold its scope-level variables. This will be expressed as an abstract ExecutionContext class. This class serves as the base for LocalScopeContext and MethodCallContext. The reason for having two contexts is the fact that LocalScopes can access parent variables while MethodCalls cannot.

Lambda

Lambda expressions are inherently weird. They are allowed access to a scope that may not exist anymore when they are called. For this reason, the local scope is copied and passed to the method whenever it is invoked. Sadly, this makes lambdas much heavier while they are in scope because they hold a reference to all values in their original context. An example test from the REPL:

GlassScript> a = 100
100
GlassScript> b = () => a
LambdaMethod__0<>
GlassScript> b()
100
GlassScript> a = "Hello, World!"
Hello, World!
GlassScript> b()
Hello, World!

The real fun comes when you make a lambda with the same name as another variable. Here is the output from the REPL:

GlassScript> a = 100
100
GlassScript> a = () => a
LambdaMethod__0<>
GlassScript> a()
100

Exposing APIs

The final thing I will talk about is how to expose APIs to GlassScript. Exposing CLR types and objects should be easy. The same should be true for returning objects from GlassScript. This is why there are two main Interop classes: GlassScriptClrProxy, which serves as a base type for interacting with the CLR world; and GlassScriptDynamicObject, which serves as a window into the GlassScript world.

Conclusion

That's all for this post. I'm sorry for the long delay since last time, but that will be nothing compared to coming up. For the months of June and July, I will be gone.