DeyjaScript 11 : Enough blather; time to actually use it.

posted in Jemgine
Published October 18, 2011
Advertisement
I mentioned last time that I might start integrating this scripting language with that other thing, and I did. Nothing evolves a library faster than actually using it. First, I designed the interface I wanted scripts to see. The first thing I want to use scripts for is key binding. I could enumerate all possible actions and end up with something like 'Game:bindKey(Key, Action)', but I really want something more. I want players to be able to bind complex scripts to keys. It complicates the binding of simple things a great deal, but I think it will be worth it. A representative line from the startup script is Game:bindKey("W", [void v] { Game:getActiveCamera().pan(0.0, 1.0, 5.0 * Game:deltaTime()); }, true); Eventually I want to find a way to simplify this, but first, I'll concentrate on just getting this working.

In that line I see a 'Game' type (or variable?) that has the functions bindKey, getActiveCamera, and deltaTime. I can approach this two ways. I can add global objects to the language, create a type, and create a global instance called 'Game'. Or, I can add static or 'free' functions. I decide to go with the latter as it doesn't involve me having to figure out where to store the globals. Each scope can have regular member functions or static functions. You can't actually declare them in script, there is simply no syntax for them, but you can bind them externally by calling ScriptEngine.addSystemFunction as in scriptEngine.addSystemFunction(gameType, "getActiveCamera", "Camera", (vm, parms) => { return Cam; }); Allowing scripts to call these functions is more complicated. There are three different nodes in the grammar that call functions. FunctionCallNode, MethodCallNode, and InvokeOperationNode, and FunctionCallNode is overloaded to behave like MethodCallNode in some cases. Where do I shove StaticCallNode into this? Well, I could, and then overload FunctionCallNode again, but instead I combine all these nodes into a single InvokationNode. It's... well, I don't think Apoch would like it.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Irony.Interpreter.Ast;

namespace DeyjaScript
{
class FunctionCallExNode : CompilableNode
{
CallableFunction function;
UsableVariable invokeOn;
CompilableNode emitThisFirst;
bool pushImplicitThis = false;

public override void Init(Irony.Parsing.ParsingContext context, Irony.Parsing.ParseTreeNode treeNode)
{
base.Init(context, treeNode);
AddChild("Expresion", treeNode.ChildNodes[0].FirstChild);
if (treeNode.ChildNodes.Count == 2)
foreach (var parameter in treeNode.ChildNodes[1].ChildNodes)
AddChild("Parameter", parameter);
this.AsString = "Function Invokation";
}

internal void lookupFunction(ScopeStack context)
{
if (function != null) return;

var argumentTypes = CompilableNode.getArgumentTypes(ChildNodes, context, 1);

if (ChildNodes[0] is MemberAccessNode)
{
//method call or member functor; This is a shortcut to implement methods more effeciently.
var memberAccessExpression = ChildNodes[0].ChildNodes[0];
var memberAccessIdentifier = ChildNodes[0].AsString;

var invokeOnType = (memberAccessExpression as CompilableNode).getResultType(context);
invokeOn = invokeOnType.resolveVariableName(false, memberAccessIdentifier);
if (invokeOn != null)
function = invokeOn.Type.resolveFunctionOverload(false, "__invoke", argumentTypes);
else
function = invokeOnType.resolveFunctionOverload(false, memberAccessIdentifier, argumentTypes);
emitThisFirst = memberAccessExpression as CompilableNode;
}
else if (ChildNodes[0] is TypenameNode)
{
var funcName = (ChildNodes[0] as TypenameNode).removeLastToken();
if (funcName == null) throw new CompileError("How did this match if it's empty?");
var invokeOnType = (ChildNodes[0] as TypenameNode).findType(context);
if (invokeOnType == null)
{
invokeOn = context.topScope.resolveVariableName(true, funcName.identifier);
if (invokeOn != null) //Functor
{
function = invokeOn.Type.resolveFunctionOverload(false, "__invoke", argumentTypes);
if (function == null) throw new CompileError("Cannot invoke this type.");
pushImplicitThis = true;
}
else
{
function = context.topScope.resolveFunctionOverload(true, funcName.identifier, argumentTypes);
if (function != null) pushImplicitThis = true; //bare function call
if (function == null)
{
function = context.topScope.resolveStaticFunctionOverload(true, funcName.identifier, argumentTypes);
if (function == null) throw new CompileError("Could not find function " + funcName.identifier);
}
}
}
else
{
function = invokeOnType.resolveStaticFunctionOverload(false, funcName.identifier, argumentTypes);
if (function == null) throw new CompileError("Could not find function " + funcName.identifier);
}
}
else if (ChildNodes[0] is CompilableNode)
{
//Expression - must be a functor of some kind.
var invokeOnType = (ChildNodes[0] as CompilableNode).getResultType(context);
function = invokeOnType.resolveFunctionOverload(true, "__invoke", argumentTypes);
emitThisFirst = ChildNodes[0] as CompilableNode;
}
}

internal override Scope getResultType(ScopeStack context)
{
lookupFunction(context);
if (function == null) throw new CompileError("Function not found (GRT)");
return function.ReturnType;
}

internal override void gatherInformation(ScopeStack context)
{
lookupFunction(context);
if (function == null) throw new CompileError("Function not found (GI)");

if (emitThisFirst != null) emitThisFirst.gatherInformation(context);
for (int i = 1; i < ChildNodes.Count; ++i)
(ChildNodes as CompilableNode).gatherInformation(context);
}

internal override void emitByteCode(List bytecode, ScopeStack context, bool placeResultOnStack)
{
for (int i = 1; i < ChildNodes.Count; ++i)
(ChildNodes as CompilableNode).emitByteCode(bytecode, context, true);

//Can't just emit the leading expression since member access nodes are short circuited.
if (emitThisFirst != null) emitThisFirst.emitByteCode(bytecode, context, true);
if (invokeOn != null)
{
if (invokeOn is MemberVariable && pushImplicitThis)
bytecode.AddBytecode(Instruction.pushVariableToStack, (byte)(125));
invokeOn.emitFetchByteCode(bytecode, context);
function.EmitCallBytecode(bytecode, context);
}
else
{
if (function.type == FunctionType.Method && pushImplicitThis)
bytecode.AddBytecode(Instruction.pushVariableToStack, (byte)(125));
function.EmitCallBytecode(bytecode, context);
}
bytecode.AddBytecode(Instruction.popN, (byte)function.ParameterCount);
if (placeResultOnStack) bytecode.AddBytecode(Instruction.pushRegisterToStack, (byte)Register.returnValue);
}
}


}


It's big, it's ugly, and it handles every possible way of invoking a function.

Oh, I also added While loops and a floating-point type. It's called 'float' in script, it's implemented as a C# double.

Up until this point, I've had a 'test script' that I run to make sure everything works. But it's gotten too long, and I need to cut out old stuff to debug the new stuff. Suddenly I have no way to know if I've broken old functionality. Also, bits of the library are leaking out to support the testing. Bits that really shouldn't be public. So I move regression testing into the library itself, with a suite of tests. It is woefully incomplete, but as I discover bugs, I'll add tests to cover them.


executeTest("Empty Test", "pass();", Engine, VM);
executeTest("Global System Call", "pass();", Engine, VM);
executeTest("System Method Call", "var S = \"123\"; if (S.length() == 3) pass();", Engine, VM);
executeTest("Free Function Call", "void function() { pass(); } void TEST() { function(); }", Engine, VM, false);
executeTest("Method Call", "type A { void method() { pass(); }}; var a = new A(); a.method();", Engine, VM);
executeTest("Lambda Call", "var lambda = [void v] { pass(); }; lambda();", Engine, VM);
executeTest("Lambda Method", "type A { void method() { pass(); }}; var a = new A(); var lambda = a.method; lambda();", Engine, VM);
executeTest("Integer Ops", "int x = 4 * 3; int y = 16 / 8; if (x + y == 14) pass();", Engine, VM);
executeTest("Float Ops", "float x = 4.0 * 3.0; float y = 16.0 / 8.0; if (x + y == 14.0) pass();", Engine, VM);
executeTest("Order of Ops", "int x = 4 * 3 + 10 / 5 - (4 + 2) / 3; if (x == 12) pass();", Engine, VM);
executeTest("Chain Invoke", "RegressionTest:getRegTest().test();", Engine, VM);
executeTest("Expression Invoke A", "Func A() { return RegressionTest:getRegTest().test; } void TEST() { A()(); }", Engine, VM, false);
executeTest("Expression Invoke B", "Func A() { return [void v] { RegressionTest:getRegTest().test(); }; } void TEST() { A()(); }", Engine, VM, false);
executeTest("Negative Numbers", "int x = -5; if (x == -5) pass();", Engine, VM);
executeTest("Subtraction & Negate", "if (5 - 7 == 2 + -4) pass();", Engine, VM);


Most of these tests trivially test trivial bits of the language. Several of them declare variables or invoke other parts of the language they aren't directly testing. At least one of them is designed to fail - Expression Invoke A results in the compile error, 'Cannot take pointer to system function'. This represents a flaw I'll have to address at some point. You can see in the last two tests how parsing negative numbers broke everything.

It's time now to seriously consider the interface the scripting language will present to the world. So far I've just sort of written code. A lot of the types are public for no good reason. Some, like CodePointer, the outside world has no business knowing about ever. I hide them, and I resolve the inconsistent accessibility errors (An error that took me a long time to understand when I first started using C#. Now I wonder, why doesn't C++ have this same error?), and then I go through every class that's still public and make sure what's public actually should be, and everything else is internal or private. I'm left with a few core types. The engine, the virtual machine, Scope, ScriptObject, Function. The engine was pretty well buttoned up. Now Scope has public functions for looking up functions, and you can walk the owner scope tree but only up. Function exists only so you can call it. The important data is exposed, but immutable.

I decided that the most useful function the virtual machine could have is a 'run string' function. This will compile a module, execute it, and then discard it. The engine doesn't actually support discarding modules, but that's fine, I'm the library author. I can just hack it.


var wrappedCode = "void RUNSTRING(){" + code + "}";
var currentTypeCount = context.globalScope.SubordinateScopes.Count;
var module = context.CompileModule("RUNSTRING", new List { wrappedCode });
var func = module.findStaticFunctionBySimpleName("RUNSTRING");

callFreeFunction(func, context);

context.globalScope.SubordinateScopes.RemoveRange(currentTypeCount, context.globalScope.SubordinateScopes.Count - currentTypeCount);


So I'm finally using the scripting language in my game, and I want to bind a function that takes a Func as an argument. Except, I can't. Why not? Because addSystemFunction doesn't actually parse the argument types, it just looks for a type with that name. Fine, Func is named Func; except that it doesn't exist yet, because it hasn't be instantiated. This is a terrible flaw. AddSystemFunction should parse that typename and instantiate the generic. In the mean time, I provide a terrible work around, invoked like so


var FuncVoidType = scriptEngine.InstanceGenericType(scriptEngine.getGlobalScope().findGenericType("Func", false, 1),
new List { scriptEngine.getGlobalScope().findLocalType("void", false) });


I use it, it works. When I press W, scripts get run, the camera pans. The game does absolutely nothing it didn't do before, but it does it differently, and that makes all the difference.

Next time, I think I'll talk about type deduction. Not the sort the compiler uses now, which I'll term 'backward type deduction'. That is fairly trivial. Given A(B);, if I know the type of B, I can find A. Everything in the compiler currently uses backward type deduction. Instead, I'll talk about something more complicated, forward type deduction. That is, given something like A( { B.foo(); });, if we know the type of A, we can deduce the type of B. I call this 'forward' because we move forward from the deduced type, rather than backward from it. Implementing a method of forward type deduction will allow me to dispense with type arguments to Lambdas, including their return type; it's also necessary to properly support the 'null' token. See if you can figure out why.
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