DeyjaScript 12 : Closures

posted in Jemgine
Published October 30, 2011
Advertisement
The next step after lambda functions is obviously closures. Without closures, I couldn't write code like this.


var view = new View(world, new Rectangle(0,0,800,600));
bindKey("W", [void v] { view.panCamera(0.0, 1.0, 5.0); }, true);


Closures allow lambdas to use variables local to the function they are declared in. To implement this, first I need to detect when variables that need to be closed over are used. The Scope abstraction already performs variable name resolution. I hijack that, and when the scope in question is a lambda scope, and the variable found is from a higher scope, I wrap the variable up. I also prevent it from wrapping the same variable twice.


public UsableVariable resolveVariableName(bool searchUp, String name)
{
foreach (var node in variables) if (node.Name == name) return node;
if (baseScope != null)
{
var R = baseScope.resolveVariableName(false, name);
if (R != null) return R;
}

if (isLambdaFunctionScope) foreach (var node in lambdaReferencedVariables) if (node.Name == name) return node;

if (searchUp && parent != null)
{
var R = parent.resolveVariableName(true, name);
if (R != null && !(R is MemberVariable) && isLambdaFunctionScope)
{
var newLambdaVar = new LambdaVariableWrapper(R);
newLambdaVar.variableIndex = lambdaReferencedVariables.Count;
lambdaReferencedVariables.Add(newLambdaVar);
return newLambdaVar;
}
return R;
}
return null;
}


LambdaVariableWrapper disguises the variable wrapped as a member variable to some imaginary type. All of these variable references are detected in the first pass of compilation, so the list is ready to go for the second. As far as the code inside the lambda, the LambdaVariableWrapper emits bytecode to fetch the variable from an object passed on the stack. Func handles pushing. More interesting is the code that pushes the Func to the stack in the first place. It first has to create an object to hold all the closed variables, and then it needs to load all of them into this object, and then finally it passes the object to Func's constructor.


bytecode.AddBytecode(Instruction.pushNewImpleObject, (byte)localScope.lambdaReferencedVariables.Count);
bytecode.AddBytecode(Instruction.loadRegisterFromStack, (byte)3);
foreach (var boundVar in localScope.lambdaReferencedVariables)
{
boundVar.originalVariable.emitFetchByteCode(bytecode, context);
bytecode.AddBytecode(Instruction.pushRegisterToStack, (byte)3);
bytecode.AddBytecode(Instruction.loadMemberFromStack, (byte)boundVar.variableIndex);
bytecode.AddBytecode(Instruction.popN, (byte)2);
}


The lambda body will later assume this 'ImpleObject' is on the stack at position -4. It's actually a variable; localScope.addVariable(new LocalVariable("__bind", null, IndexType.Parameter)); But, even though the lambda body can resolve the name, it can't use the variable because it has no type.

This implementation closes over values, not variables. That means that whatever value a variable has when the lambda is declared, that's the value it will have in the body. Supporting closure-over-variables is simple. Notice that in the current implementation, something like


type A { int x; }
var a = new A();
a.x = 5;
var func = [void v] { doSomething(a.x); };
a.x = 2;
func();


closes over the variable x. When I call func() at the end, the value of the closed-over variable, 'a', has not changed. It still refers to the same A. And, even if you do a = new A();, the lambda is still going to use the original A. But, look at x. x has changed. And x wasn't closed over, a was. So when I call func at the end, x has changed, and the lambda sees the new value. This method of implementing closure over variables is so simple that I made it an official part of the language with this generic type.


var refCode = @"type Ref {
T value;
void construct(T value) { this.value = value; }
}";
CompileType(globalScope, refCode);


And then I can write code like this


var ogRef = new Ref();
ogRef.value = new ObjectGrid(new Rectangle(104, 4, 692, 592), [Definition d, void v]
{
ogRef.value.hide();
mousePlaceObject(view, world, d, [Location l, Facing f, void v]
{
addLocalTask(new BuildTask(d,l,f));
});
});


in which the lambda argument to the ObjectGrid constructor references the same ObjectGrid it is being passed to. It (unfortunately) compiles without the use of references, such as


var og = new ObjectGrid(new Rectangle(104, 4, 692, 592), [Definition d, void v]
{
og.hide();
mousePlaceObject(view, world, d, [Location l, Facing f, void v]
{
addLocalTask(new BuildTask(d,l,f));
});
});


but will fail at runtime because, at the time the closed values are gathered, 'og' is null. The lambda body will see null, not the new value of og. The reason the first version works is because ogRef already exists. I change the object ogRef references without creating a new Ref<> object. The lambda sees the original value of ogRef (the Ref<> instance) and gets the new value of Ref<>.value from it.

Closures are damn handy. I also implemented some helper functions to make binding things to the scripting language easier. C# reflection makes automatically wrapping arbitrary C# lambdas possible.


public void wrapSystemFunction(Scope into, String name, Delegate Func)
{
var systemFunction = new CallableFunction(CallingConvention.System, FunctionType.Free);

var invokeMethod = Func.GetType().GetMethod("Invoke");
var Parameters = invokeMethod.GetParameters();

systemFunction.ReturnType = findSystemType(invokeMethod.ReturnType);
systemFunction.ParameterTypes = new List();
foreach (var parm in Parameters) systemFunction.ParameterTypes.Add(findSystemType(parm.ParameterType));

systemFunction.ReturnTypeName = systemFunction.ReturnType.fullTypeName;
systemFunction.Name = name;
systemFunction.DecoratedName = Scope.getDecoratedName(systemFunction);
systemFunction.ParameterCount = systemFunction.ParameterTypes.Count; // parameterTypes.Length;
systemFunction.systemFunc = (vm, parms) =>
{
var arguments = new object[parms.Length];
for (int i = 0; i < parms.Length; ++i)
arguments = parms.getMember(0);
var result = Func.DynamicInvoke(arguments);
var obj = vm.createScriptObject(systemFunction.ReturnType);
obj.setMember(0, result);
return obj;
};
systemFunction.DeclarationScope = into;

into.addStaticFunction(systemFunction);
if (globalScope.IsDuplicateFunction(systemFunction)) throw new CompileError("Duplicate system func");
}


Unfortunately I still have to explicitly declare the types of parameters to the delegate when I call wrapSystemFunction.

scriptEngine.wrapSystemFunction(scriptEngine.getGlobalScope(), "hostPeerToPeerBuildSession", new Action((world, port) =>
{ activeSession = new PeerToPeerBuildSession(world, port, this); }));
0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement