Share a context/global object throughout an expression tree

Jan 13, 2010 at 1:34 PM
Edited Jan 13, 2010 at 1:34 PM

Things have been coming along nicely with my language implementation, got most of the stuff I want in - but now I've encountered a bit of a problem: I have a context object that holds stuff like built in objects, pre-defined functions, classes, etc. These can be changed out during runtime (the context object is not static, it can be changed by the code that is currently executing). It's also used to expose parts of the BCL to my language (sort of what SymPL does with import, etc - but a bit more involved).

Now the thing is that this object is the only and only argument to the lambda compiled for the expression tree, like this:

            return Et.Lambda<Context>(
                BodyExpr,
                ContextParamExpr
            ).Compile();

Still nothing weird going on, when the compiled lambda is called from C# to execute the expression tree you just pass in a Context object (which you can setup yourself before hand, to choose what you want to expose to the language, what built in objects should be available, etc.) - this is exactly what SymPL does for its globals object, etc.

The problem here is that sometimes I need access this context object from within my binders (especially my ConvertBinder, but that's not really the point) - to access some functionality one of the built in native objects, or when an object is automatically created during runtime and it needs to inherit from the base 'MyLangObject'-base class.

I've tried pushing the ContextParamExpr (since it's at the top level) on at the beginning on all the arguments on the Invoke and InvokeMember binders, and then pull it of the args[] array passed into the binder. Still doesn't solve the problem with the SetMember, GetMember and Convert binders - and it's feels like a really ugly hack.

I've also tried passing a context object into the compilation itself, passing at as an argument to a overloaded constructor on the binders, but this ties a compilation of a specific source code to this exact context object - so I need to compile the source code for each context object instead of using the same compiled lambda and passing in a different context object instead.

My third solution is to instead of passing in the context object to the binder constructors directly I pass in a "ContextProvider" object which just has one field called "Context" which allows me to switch out the context between different executions of the same code unit (and the binders access the current context through contextProvider.Context). But this means I can't execute a compiled unit in any form of parallel fashion because I change the ContextProvider.Context field to the Context for the currently executing environment, but if I started a second execution of the same compiled unit it would change the contextProvider.Context object for that compilation unit and the previous, already ongoing, execution would have it's contextProvider.Context object changed mid-runtime.

Does anyone have a good solution to this? Do I need to drill down even further then just creating my own binders to achieve this?

Jan 13, 2010 at 5:39 PM

This is a common pattern for language runtimes - they execute code within some context that describes the world. For C# and VB.NET this context is AppDomain. For DLR based dynamic languages this context is usually a ScriptRuntime. Executions against different runtimes access different global variables, dynamic class hierarchies, etc. DLR Hosting API expects your language to subclass LanguageContext and associates its instance with an instance of ScriptRuntime. Both IronPython and IronRuby have their contexts: PythonContext and RubyContext, respectively.

We do compile a specialized code for each context. The call site binders are cached on the context and call site rules are mostly context bound. The advantage is that the rules can assume the current context and don't need to emit restrictions for it. We can also use other context bound live objects in Expression.Constant. The disadvantage is that we need to emit more code. Any code that uses context-bound call-site needs to be either duplicated for each context or parameterized over context bound constants. But we expect the number of contexts (runtimes) to be small.