Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
49 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

Scripted Functions

The first step to supporting scripted functions is to add instructions that make use of the stack's scoping capabilities:

case op_push_scope:
    _stack.PushScope(_instr->Data()[0]);
    ++_instr;
    break;
case op_pop_scope:
    _stack.PopScope();
    ++_instr;
    break;

The instruction to push a new scope onto the stack will require a single instruction parameter to describe the size of the scope being pushed.

So how exactly does this describe a higher-level function? Let's say we have the following useless function in a higher-level language:

void DumbFunc(int param1, int param2)
{
	int first, second, third;
}

And we make a call to it somewhere:

DumbFunc(11, 7);

Such a function requires a stack scope that can support space for both parameters, as well as three additional variables (a total of five variables). The calling code needs to push the values of 11 and 7 onto the stack for use (or non-use, as the case may be) in DumbFunc's code. Our entry into such a function from calling code at this point might look like the following in psuedo-script:

push_const 11
push_const 7
jump DumbFunc
CallingCodeDestination:
...

DumbFunc:
push_scope 5
// DumbFunc does nothing
pop_scope
jump CallingCodeDestination

Before the scope is popped, the stack from left to right should now look like this:

11 7 X X X

Where X is an undefined value. The undefined values are expected because DumbFunc doesn't initialize the values for its other three variables.

Now that the scoping issues are handled, there is another problem to solve. How does the function know where to jump in the calling code? Unless you are inlining, which I will discuss in a later article, you would certainly like to have only one instance of a function's code within your script, rather than a duplicate for every single time you call the code (where each duplicate would have a unique returning jump destination). Duplicates would make your script larger than necessary.

One thing that might come to mind initially is to store another InstrRef which is set to hold the destination before jumping to the function code. The function would then jump to this location. This would work if you never had functions that called more functions, but that is a restriction we can do without.

Again, a stack provides a desirable solution. With a stack, we can trace the path we take like in a manner that is similar to unraveling a string while exploring a maze. As long as we continue to push the desired destination point onto such a stack before entering the function code, the function can then jump back to that destination later. Subsequent function calls will push a new destination onto the stack in a recursive fashion, rather than corrupting its parent's destination.

Here I provide an implementation for such a call stack:

// traces the call path through a script    
class CallStack
{
public:
    CallStack(InstrRef& current) : _instr(current)  {}
    void Call(InstrRef funcBegin)
    {
        _stack.push_back(_instr+1); // set the return point to the next instruction
        _instr = funcBegin;         // set current instruction to the function starting point
    }
    void Return()
    {
        assert(!_stack.empty());
        _instr = _stack.back();     // return to proper instruction in calling code
        _stack.pop_back();
    }
private:
    std::vector<InstrRef>   _stack; // stores function return destinations
    InstrRef&               _instr; // reference to current instruction
};

As you can see, it requires that a reference to the root and current instruction registers be passed to it during creation. The CallStack will be placed in the Execution class, which will construct the CallStack using its instruction pointers:

// construct callstack with current instruction
Execution() : _scriptPtr(0), _instrPtr(0), _instr(0), _callStack(_instr)    {}

InstrRef            _instr;     // current instruction
CallStack           _callStack; // traces call path

In our pseudo-script, our new concept would be expressed like this:

push_const 11
push_const 7
call_func DumbFunc
...

DumbFunc:
push_scope 5
// DumbFunc does nothing
pop_scope
end_func

The call_func instruction would use the CallStack's Call(), while end_func would use its Return() function. A handler for call_func would require a single data parameter that stores the position of the function to be called. So let's create these two new instruction handlers:

case op_call_func:
    _callStack.Call(_instrPtr + _instr->Data()[0]);
    break;
case op_end_func:
    _callStack.Return();
    break;

Simple enough. Note how the instruction offset is added to the root instruction pointer when being passed to Call(). It's very similar to the jump instructions created last time.

Now suppose DumbFunc were to return an int:

int DumbFunc(int param1, int param2)
{
	int first, second, third;
	return 1;
}

With the way we currently have things set up, we will need a way to place the value returned at the top of the stack after returning from the function call. To avoid the use of hacks, a simple solution is to create a general-purpose register that can store the returned value during stack state changes.

In class Execution:

int	_register;  // general purpose register

To make use of it, we simply need instructions to handle access:

// registers
case op_store:
    _register = _stack.Top();
    _stack.Pop();
    ++_instr;
    break;
case op_load:
    _stack.Push(_register);
    ++_instr;
    break;

Currently, we only need to support this single register. Later on, if we find a need for more, we can always adjust these handlers. The op_store instruction will store the value at the top of the stack in a register, and pop it off. Then op_load can be used to push the register's value back onto the stack at a later time.

The new DumbFunc pseudo-script would look like this:

push_const 11
push_const 7
call_func DumbFunc
load	// if we want to use the return value
...

DumbFunc:
push_scope 5
push_const 1	// using a return value of 1
store		// save the return value
pop_scope
end_func

Recursive Test

We now have what it takes to implement a recursive factorial. Not that a recursive factorial algorithm is to be preferred over an iterative version, but such an algorithm can still test that the system can now make recursive function calls. At a high level, our function would look like this:

int Factorial(unsigned int input)
{
	if (input > 1)
		return input * Factorial(input-1);
	return 1;
}

We would like to call this function with a particular input parameter, and then output the result. We have to translate this into something our system will understand. At the outermost scope, our program looks like this:

push_const input
call_func factorial
load
output
end

The input value is pushed onto the stack to be used as a parameter as the factorial function is called. The return value is placed onto the stack, and then shown to us. The tricky part is the factorial function itself. Using the same method for converting from high-level to our machine code, we will come up with something like this:

factorial:
push_scope 1
push_var 0
// if (input > 1)
push_const 1
greater
jump_if_false A
// Factorial(input-1)
push_var 0
push_const 1
subtract
call_func factorial
load
// input *
push_var 0
multiply
jump B
A:
// 1
push_const 1
B:
// return
store
pop_scope
end_func

The factorial of 7 should come out to be 5040. So we will put everything together with 7 as the input parameter, and resolve the label offsets. (Note the numbering of the lines in this example to more clearly show the instruction indices.)

0:	push_const 7
1:	call_func 5
2:	load
3:	output
4:	end
5:	push_scope 1
6:	push_var 0
7:	push_const 1
8:	greater
9:	jump_if_false 18
10:	push_var 0
11:	push_const 1
12:	subtract
13:	call_func 5
14:	load
15:	push_var 0
16:	multiply
17:	jump 19
18:	push_const 1
19:	store
20:	pop_scope
21:	end_func

When we execute this script we are pleased to see that the output is 5040, as expected.



Page 3

Contents
  Introduction
  Page 2
  Page 3
  Page 4

  Source code
  Printable version
  Discuss this article

The Series
  An Introduction
  Data Manipulation
  Dynamic Loading
  The Stack and Program Flow
  Functionality