DeyjaScript 5 : Objects and methods

posted in Jemgine
Published September 18, 2011
Advertisement
I have to say, after getting to the fifth installment in about a week, that I like this style of 'journaling' 'tutorials'. I'm not writing what you should do to create a scripting language, instead I'm just saying what I have done. It also forces me to find a single isolated feature and implement it. I did something similar once before, except then I wrote what I was going to do, and then did it, rather than the other way around. It seems to work well either way. Code gets written, information gets dispensed, certain people hate it.

The goal all along has been to create an object-oriented language. Last time, we ended with a list of functions that could all call each other. There was a lot crammed into that last very short installment, more than I realized. The basic function calling mechanism is in place. Now I transition from a list of functions to an object with methods, and then I go immediately to a list of these objects. The grammar is simple.


objectDeclaration.Rule = identifier + "{" + memberList + "}";
objectList.Rule = MakeStarRule(objectList, objectDeclaration);
newStatement.Rule = ToTerm("new") + identifier;
methodCall.Rule = expression + "." + identifier + "(" + parameterList + ")";


Oh yeah, and new statements and method calls. Notice that objects just have a name, there's no way to pass arguments when calling new, etc. Those are all features I'll add in time. I was a little worried that the methodCall rule would cause a conflict, but Irony hasn't complained and seems to be parsing it correctly.

I create an object list node that acts as the new root node of the program, and allows me to find types by name. Now the execution context (perhaps I should separate execution and compilation contexts?) has three scopes, global, object, and function. I need to change this to some sort of scope stack. That would help me resolve the identifier leaking problem, too. Excuse me for rambling, but take a look at this -


int foo(int x)
{
if (x == 5)
{
int bar = 6;
}
return bar;
}


That compiles. And runs. And if x happens to be 5, it actually works. But if x isn't five, it will return an uninitialized value, which usually results in the VM dieing suddenly and violently.

The object list node is not a compilable node. That abstraction leaked far enough. Instead, it supplies a single 'compile' function, since it has (almost) all the information it needs to compile already. This compile function first gathers up the names of all the objects and their methods, then it allows all it's children CompilableNodes to gather the information they need to compile, and then it allows them all to emit bytecode. The list to emit the bytecode into isn't provided to the function. Instead, each object gets it's own bytecode (methods are still all smashed into one big buffer). I made this change so I could more easily support modules later.

It means a major change in the VM, though. Instead of just a set of bytecode and an instruction pointer, it needs multiple sets of bytecode and instruction pointers. Since the stack is a stack of Objects, when I call a function, instead of pushing just the return address, I push the bytecode/return address pair. The callMethod instruction works basically the same as callFunction.


case Instruction.callMethod:
{
var stackTopPointer = getTopPointer();
var @this = stack.fetch(stackTopPointer - 1) as ObjectDeclarationNode;
stack.store(stackTopPointer, codePointer);
setTopPointer(stackTopPointer + 1);
var methodIndex = BitConverter.ToInt16(codePointer.bytecode, codePointer.index + 1);
codePointer.index += 3;

codePointer = new CodePointer
{
bytecode = @this.bytecode,
index = @this.functions[methodIndex].firstInstruction
};
}
break;


Special thanks to MikeP for teaching me about @ and identifiers in C#, even if he did describe this project as a complete waste of everyone's time. I'll abuse @ in his honor.

Now I have to implement MethodCallNode, which will be an almost direct copy of FunctionCallNode. Combining them would be difficult enough that I didn't. First, I make changes to FunctionCallNode. Since everything is a method, if the function called isn't a system function, it needs an implicit this parameter. Thankfully, I already added a 'this' parameter to every function declaration automatically, and it's always the last parameter, so it's always at index -3. (-2 is the return point, -1 is the old framepointer, 0 is the first variable declared in the function) So when the function being called isn't a system function, FunctionCallNode pushes this and treats it like a method call.

MethodCallNode requires an expression first to tell which object to call the function on. The callMethod instruction assumes that the object the method is being invoked on is at the top of the stack, so first I gather all the parameters, and then I evaluate the leading expression. There is, of course, static type checking when I lookup the function.


internal FunctionDeclarationNode lookupFunction(ExecutionContext context)
{
var invokeOn = (ChildNodes[0] as CompilableNode).getResultType(context);
var objectDeclaration = context.currentGlobalScope.findObject(invokeOn);
if (objectDeclaration == null) throw new CompileError("Can't invoke methods on this type.");

var argumentTypes = new String[ChildNodes.Count - 1];
for (int i = 1; i < ChildNodes.Count; ++i)
argumentTypes[i - 1] = (ChildNodes as CompilableNode).getResultType(context);

var func = objectDeclaration.findFunction(AsString, argumentTypes);
return func;
}

internal override string getResultType(ExecutionContext context)
{
var func = lookupFunction(context);
if (func == null) throw new CompileError("Function not found (GRT)");
return func.getResultType(context);
}

public override void gatherInformation(ExecutionContext context)
{
var func = lookupFunction(context);
if (func == null) throw new CompileError("Function not found (GI)");
function = func;
foreach (var child in ChildNodes)
(child as CompilableNode).gatherInformation(context);
}

public override void emitByteCode(List bytecode, ExecutionContext context, bool placeResultOnStack)
{
for (int i = 1; i < ChildNodes.Count; ++i)
(ChildNodes as CompilableNode).emitByteCode(bytecode, context, true);
(ChildNodes[0] as CompilableNode).emitByteCode(bytecode, context, true);
function.EmitCallBytecode(bytecode);
bytecode.AddBytecode(Instruction.popN, (byte)ChildNodes.Count); //pop parameters
if (placeResultOnStack) bytecode.AddBytecode(Instruction.pushRegisterToStack, (byte)Register.returnValue);
}


An aside, apparently C# supports zero-length arrays.

To actually test this, I need objects to invoke methods on. So I create a new instruction. For now, it just shoves the object declaration onto the stack, effectively making every object static. The next step is probably to have actual instances. Afterwards, I will have to eventually support static method invocation.

For testing, I write a quick way to invoke methods on a script object.


public void callFunction(VirtualMachine VM, ExecutionContext context, int func)
{
var wrapperCode = new List();
wrapperCode.AddBytecode(Instruction.callMethod, BitConverter.GetBytes((Int16)func));
wrapperCode.AddBytecode(Instruction.exit);
VM.registers[(int)Register.stackTopPointer] = 1;
VM.registers[(int)Register.framePointer] = 0;
VM.stack.store(0, this);
VM.execute(wrapperCode.ToArray(), context, 0, true);
}


It takes advantage of the change to multiple bytecode buffers. That last argument to execute tells the VM not to setup the stack or registers. One last thing, I get rid of the callFunction instruction and the function table at the start of the bytecode entirely, as they are no longer used.
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