This article is about the OHRRPGCE FMF project, which is an alternate implementation of the OHRRPGCE for Java mobile phones. Technical implementation details discussed here should not be confused with those of the RPG format
This page describes the workings of the HVM in relation to the bytecodes it processes. For your reference, here is a copy of the data structures diagram:
We will now list some critical operations performed by the HVM and how it uses the data structures to track these operations. Before reading this section, you should probably read the syntax dictionary, so that you have an idea of Henceforth's intended functionality. Otherwise, this section might not make a lot of sense.
For these charts, the green background is default. If the background is red, that means the chart only applies to a full-blown user script. A subroutine will fail, since it doesn't have the requisite data structures allocated. We check if a subroutine is being run by seeing if the PC Stack's size is greater than 1. Checking another stack's size to determine this is non-standard.
For brevity, when we say "advance the PC by one" (or, equivalently, PC += 1), we are referring to a "smart increment". For multi-word bytecodes, this means we have to increment by the length of the bytecode, not simply by 1.
Lifecycle
Trigger
After the HVM initializes, and before it starts running scripts.
Response
Create a new entry in the script cache
New Name = "root"
ID = -1
Pinned = true
In Use = 1
Bytecode = new Array[]
DoTable = new Array()
Optionally, load into "Bytecode" the contents of "global.HF", or read directly from user input.
Note: We do not create an entry in the script name multiplexer; root is effectively read-only.
Make a new Script UDT:
Local Stack = new Stack()
Program Counter Stack = new Stack(0)
Script Cache Pointer Stack = new Stack(Script Cache Reference)
Do/If Stack Stack = new Stack(new Stack())
Local Variable Hash Stack = new Stack(new Hash())
ID = -1
Parent Script Pointer = -1
Local Variable Array = new Array()
String Table = new Array()
Local Function Start Indices = new Array()
Return Address Stack = new Stack()
Note: If the root script attempts to use script-only data structures (e.g., local parameters), the behavior of the HVM is undefined.
Make a new entry in the Running Scripts table:
Script = our new Script UDT
Wait = false
Wait On = 0
Insert this new Running Scripts entry at index zero (or as the head node, if it's a linked list). This spot is reserved for the root script.
Functional Primitives
Trigger
dup
swap
drop
over
rot
add
sub
mult
div
random
b_xor
b_and
eq
lt
not
and
xor
b_not
b_or
or
Response
Pop a pre-defined number of arguments from the local stack.
Perform the desired math/logic
Push the resultant, if applicable, to the local stack.
Numbers
Trigger
The HVM encounters a "short integer" or a "full-width integer"
Response
Push that number to the "local stack" of the currently-running script.
Script-Local Subroutines
Trigger
The HVM encounters a "define local script" with id x.
Response
In the "local function start indices" array, at index x, insert (PC+1)
Advance the PC without executing commands until you reach an "end define".
Advance the PC by one.
Trigger
The HVM encounters a "call local script" with id x.
Response
Push (PC+1) to the "return address stack"
Set the PC to the "local function start indices" array value at index x.
Trigger
The HVM encounters an "end define" and the "return address stack" is not empty.
Response
Pop the "return address stack" and set the PC to this value.
Global Variables
Trigger
The HVM encounters a "retrieve global variable" with id x.
Response
If x is equal to the magic number (0x3FF for 10-bit ids), then pop the stack and store that value in x.
Push the value of that variable from the Global Variable Table.
Trigger
The HVM encounters a "set global variable" with id x.
Response
Pop the stack.
If x is equal to the magic number (0x3FF for 10-bit ids), then pop the stack and store that value in x.
Store the popped value at location x in the Global Variable Table.
Call Subroutines
Trigger
The HVM, while running a script with id y in the Running Scripts Table, encounters a "call user-defined function by id" with id x.
Response
Retrieve a new entry in the script cache
Get entry x from the script lookup table; if "Cache" == -1, then load the source from the appropriate "Chunk" and add a new entry to the script_cache:
New Name = null
ID = x
Pinned = false
In Use = 0
Bytecode = loadFromChunk()
Do Table = new Array[do_udt]
For the appropriate script cache entry, set InUse = InUse + 1
Make a new Script UDT:
Local Stack = new Stack[]
Program Counter Stack = new Stack(0)
Script Cache Pointer = new Stack(id of the script cache entry we retrieved)
Do/If Stack Stack = new Stack(new Stack())
Local Variable Hash Stack = new Stack(new Hash{})
*Note: The HVM can leave "optional" components like this un-initialized until they are actually used.
ID = x
Parent Script Pointer = y
Local Variable Array = new Array[max+1]
String Table = new Array[]*
Local Function Start Indices = new Array[]*
Return Address Stack = new Stack[]
Set Local Variable Array to -1 at index 0
Make a new entry in the Running Scripts table:
Script = our new Script UDT
Wait = false
Wait On = 0
Insert this new Running Scripts entry one step ahead of script Y; this ensures it will be run next.
Set script Y's "Wait" flag to "true".
Trigger
The HVM, while running a script with id y in the Running Scripts Table, encounters a "call named subroutine" with name exp.
Response
Retrieve a new entry in the script cache
Get entry exp from the script name multiplexer
Use exp to get the appropriate script cache entry; set its InUse = InUse + 1
In the current script UDT:
Program Counter Stack.push(0)
Script Cache Pointer.push(id of the script cache entry we retrieved)
Do/If Stack Stack.push(new Stack())
Local Variable Hash Stack.push(new Hash{})
*Note: The HVM can leave "optional" components like this un-initialized until they are actually used.
Trigger
The HVM encounters a "hamsterspeak api call" with id x.
Response
Pop the agreed-upon number of arguments
Call API function x with said arguments
Push the return variable, if applicable
Local Variables
Trigger
The HVM encounters a "push local variable by id" with id x.
Response
Pop the stack
If x is equal to the magic number (0x1FF for 9-bit ids), then pop the stack and store that value in x. Subtract 1 from x.
Store the popped value in script_udt.local_variable_array[x+1]
Trigger
The HVM encounters a "pop local variable by id" with id x.
Response
If x is equal to the magic number (0x1FF for 9-bit ids), then pop the stack and store that value in x. Subtract 1 from x.
Retrieve the value in script_udt.local_variable_array[x+1]
Push the retrieved value to the stack
Trigger
The HVM encounters a "pop local variable by name" with name test.
Response
Pop the stack
Create a key in script_udt.local_variable_hash_stack.top() for "test"
Store the popped value in script_udt.local_variable_hash_stack.top(){"test"}
Trigger
The HVM encounters a "push local variable by name" with name test.
Response
If the local variable hash stack's top entry has no key at "test", push -1 and return
Retrieve the value in script_udt.local_variable_hash_stack.top(){"test"}
Push the retrieved value to the stack
If Statements
Trigger
The HVM encounters an "if_start"
Response
Push a new do_udt with startIndex = PC to the do/if stack stack.top()
Retrieve the script cache entry associated with script_cache_pointer_stack.top(). If this contains a do_udt with the same startIndex, set the elseIndex and endIndex of this do_udt to the values in the cached entry. Else, set them to -1. Do the same for the endIndex.
Pop the stack. If this value is not 0, increment the PC. If 0, then set the PC to the elseIndex. (If the elseIndex is -1, scan & dispose bytecodes until the else_start command is found, and then update the script_cache.) Increment the PC.
Trigger
The HVM encounters an "if_end"
Response
Pop the do/if stack stack.top(). If that entry's endIndex is -1, set endIndex = PC and update the script_cache.
Trigger
The HVM encounters an "else_start" (This will only happen if the else branch is not taken)
Response
Pop the do/if stack stack.top(). If that entry's elseIndex is -1, set elseIndex = PC and update the script_cache.
Set the PC = endIndex. (If endIndex is -1, scan & dispose bytecodes until the if_end command is found, and then update the script cache.) Increment the PC
Loops & Flow Control
Trigger
The HVM encounters a "do_start"
Response
Push a new do_udt with startIndex = PC to the do/if stack stack.top()
If the script_cache entry for this script contains a do_udt with the same startIndex, set the endIndex of this do_udt to that value. Else, set it to -1. Set elseIndex = -2.
Increment the PC
Trigger
The HVM encounters a "do_end"
Response
Pop the do/if stack stack.top(). If that entry's endIndex is -1, set endIndex = PC and update the script_cache.
Trigger
The HVM encounters a "break" or "break_x"
Response
Set x = 1 if this is a "break" command, or x = stack.pop() if this is "break_x"
while x > 0, do the following:
Pop the do/if stack stack.top() until you encounter a do_udt with elseIndex == -2. Pop that as well.
Set PC = endIndex of this do_udt. (If endIndex is -1, scan & discard bytecodes until a do_end is found. Then update the script_cache.) Increment the PC.
Decrement x
Trigger
The HVM encounters a "continue" or "continue_x"
Response
Set x = 1 if this is a "continue" command, or x = stack.pop() if this is "continue_x"
Pop the do/if stack stack.top() until you have removed x do_udts with elseIndex == -2. Push the last entry back to the do/if stack stack.top().
Set PC = do/if_stack_stack.top().top().startIndex + 1
Subroutines & Strings
Trigger
The HVM encounters a "string define" with string test string.
Response
Push test string to the end of the string table
Trigger
The HVM encounters a "begin define" with string my_function.
Response
Look for my_function in the script name multiplexer. If it's not there, add it and set definitions = new Array[int]
Create a new entry in the script_cache:
New Name = "my_function"
ID = -1
Pinned = true
In Use = 0
Do Table = new Array[do_UDT]
Scan all bytecodes up to and including an end_define into the "Bytecode" field for this new script cache entry.
Push into script_name_multiplexer{"my_function"} a reference to the script cache entry you just created.
Trigger
The HVM encounters an "un-define named subroutine" with string my_function.
Response
Pop the last definition from the script_name_multiplexer{"my_function"} into variable x.
If script_name_multiplexer{"my_function"} has no more definitions, remove it.
Set script_cache[x] to be a null row; remove it from the array if you can do so without introducing inconsistencies. At the very least, de-allocate all its data.
Trigger
The HVM encounters an "end_define", or a "return" command in a user-defined script.
Response
If there is a return value in script_udt.local_variable_array[0], push it to the parent script's stack. If not, push the last calculated response ($_)
Set this script's parent's entry in the running scripts table to Wait = false
Remove this script from the running scripts table.
Decrement the in_use field for the script_cache entry for this script_udt
Delete this script_udt
Trigger
The HVM encounters an "end_define" in a subroutine.
Response
Pop the script cache pointer stack, set the the corresponding script cache's entry's In Use to In Use -1