Initial commit

This commit is contained in:
Scott Duensing 2020-05-13 19:08:25 -05:00
commit 57f209e18c
10 changed files with 3031 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
build-*
*.bin
*.lst
*~
*.user

16
compiler/compiler.pro Normal file
View file

@ -0,0 +1,16 @@
TEMPLATE = app
include(/home/scott/joey/dist/joey.pri)
HEADERS += \
utlist.h
SOURCES += \
main.c
OTHER_FILES += \
../notes.txt
DISTFILES += \
../test.dbl \
../test.lst

1088
compiler/main.c Normal file

File diff suppressed because it is too large Load diff

1073
compiler/utlist.h Normal file

File diff suppressed because it is too large Load diff

69
notes.txt Normal file
View file

@ -0,0 +1,69 @@
DBLTW ("DoubleThrow") - Don't Build Languages This Way
Kinda stack based. Stack entries are "variables". A variable is anything.
Mainly using a stack based VM to make dealing with variables easier.
The compiler is going to be odd. It needs to know about EVERY variable used
in EVERY script. Variables are persistent. Setting one in one script makes
it available in other scripts. The game engine needs to be able to set and
read VM variables somehow. Useful game data such as current map and player
position should be in variables for the scripts to use.
- We say the VM is "kinda stack based" because we skip all the pushing/popping
for operations where it's not needed. This may go away.
- Variables are assigned "memory addresses" (index into an array) by the
compiler. This prevents the need to look up where a variable is stored.
- Exported variables are regular variables that additionally get added to
a lookup array so the game engine can find their "memory address" by name.
- The map compiler needs to generate a script that associates map names with
map numbers so authors can use map names in scripts.
- Stupidly simple pre-processor for constants. It just replaces
tokens/variable names with another value.
- All strings must be in quotes. This allows us to easily determine if
text encountered in a script should be treated as a variable name or not.
- To use a variable in a text string, prefix it with $$.
- Do we want to load all script data for a map at once? Or individual scripts?
- The compiler should produce statistics on memory requirements that we can
send to the game at startup to configure the VM and not waste resources.
---
Preprocessor:
define <thisThing> <thatThing>
include <otherFile>
Keywords:
export <variable> <value>
set <variable> <value>
add <variable> <value>
subtract <variable> <value>
if <value> [<,>,!=,==] <value> <label>
goto <label>
call <label>
return
end
do <function> <arg0> <arg1> <...>
---
Opcodes:
add (pulls two from stack, pushes one)
puc <constant>
puv <variable>
pop <variable>
jmp (will pull arg from stack)
boc (will pull four args from stack)
exe (will pull however many args from stack)
hlt
---

16
test.dbl Normal file
View file

@ -0,0 +1,16 @@
; Function constants (provided by RPG Engine developer)
define print "do 0"
print "Count is: "
export count 5
goto loop
display:
print count " "
return
>loop:
subtract count 1
call display
if count > 0 loop
end

52
vm/main.c Normal file
View file

@ -0,0 +1,52 @@
#include <stdio.h>
#include "vm.h"
// /home/scott/joey/rpgengine/dbltw/test.bin
void func_print(VMContextT *context, juint16 args) {
int x;
for (x=0; x<args; x++) {
vmPop(context, &context->working[0]);
if (vmIsItemNumber(&context->working[0])) {
printf("%d", context->working[0].value);
} else {
printf("%s", context->working[0].text);
}
}
}
int main(int argc, char *argv[]) {
char *programFile;
VMContextT context;
// Get arguments
if (argc != 2) {
printf("Usage: %s binary.bin\n", argv[0]);
return 1;
}
programFile = argv[1];
jlUtilStartup("VM Test");
if (!vmCreate(&context)) {
printf("Unable to initialize VM.\n");
} else {
vmAddFunction(&context, func_print);
if (!vmLoad(&context, programFile)) {
printf("Unable to load '%s'.\n", programFile);
} else {
vmRun(&context);
} // vmLoad
} // vmCreate
vmDestroy(&context);
printf("\nJlStaT = %lu\n", sizeof(jlStaT));
jlUtilShutdown();
}

613
vm/vm.c Normal file
View file

@ -0,0 +1,613 @@
#include "vm.h"
#include "stdio.h"
#include "stdarg.h"
#include "string.h"
// Number of scratch items for opcodes to use as working space.
// Depends on VM implementation so it's declared here.
#define WORKING_SIZE 4
typedef void (*VMOpCodeT)(VMContextT *context);
void copyItem(PairT *item, PairT *source);
void op_add(VMContextT *context);
void op_boc(VMContextT *context);
void op_exe(VMContextT *context);
void op_jmp(VMContextT *context);
void op_hlt(VMContextT *context);
void op_pop(VMContextT *context);
void op_puc(VMContextT *context);
void op_puv(VMContextT *context);
void op_sub(VMContextT *context);
void readItemFromPC(VMContextT *context, PairT *item);
void setStringFormat(PairT *item, char *format, ...);
static VMOpCodeT opcodes[OPCODE_LAST];
void copyItem(PairT *item, PairT *source) {
vmSetItem(item, source->text, &source->value);
}
void op_add(VMContextT *context) {
bool isString0;
bool isString1;
vmPop(context, &context->working[0]);
vmPop(context, &context->working[1]);
isString0 = !vmIsItemNumber(&context->working[0]);
isString1 = !vmIsItemNumber(&context->working[1]);
// Both operands need to be numbers to result in a number.
if (isString0 || isString1) {
// Are both strings?
if (isString0 && isString1) {
setStringFormat(&context->working[2], "%s%s", context->working[0].text, context->working[1].text);
} else {
// Is the first a string?
if (isString0) {
setStringFormat(&context->working[2], "%s%d", context->working[0].text, context->working[1].value);
} else {
setStringFormat(&context->working[2], "%d%s", context->working[0].value, context->working[1].text);
}
}
context->working[2].value = (jint16)strlen(context->working[2].text);
} else {
// Number
context->working[2].value = context->working[0].value + context->working[1].value;
}
vmPush(context, &context->working[2]);
}
void op_boc(VMContextT *context) {
bool isNumber0;
bool isNumber2;
int result;
vmPop(context, &context->working[0]); // arg1
vmPop(context, &context->working[1]); // compairson
vmPop(context, &context->working[2]); // arg2
vmPop(context, &context->working[3]); // target pc
isNumber0 = vmIsItemNumber(&context->working[0]);
isNumber2 = vmIsItemNumber(&context->working[2]);
// Both Numbers?
if (isNumber0 && isNumber2) {
switch (context->working[1].value) {
case 0: // =
if (context->working[0].value == context->working[2].value) {
context->private.pc = (juint16)context->working[3].value;
}
break;
case 1: // !
if (context->working[0].value != context->working[2].value) {
context->private.pc = (juint16)context->working[3].value;
}
break;
case 2: // <
if (context->working[0].value < context->working[2].value) {
context->private.pc = (juint16)context->working[3].value;
}
break;
case 3: // >
if (context->working[0].value > context->working[2].value) {
context->private.pc = (juint16)context->working[3].value;
}
break;
}
} else {
// Which side is a number?
if (isNumber0) {
setStringFormat(&context->working[2], "%d", context->working[2].value);
} else {
setStringFormat(&context->working[0], "%d", context->working[0].value);
}
result = strcmp(context->working[0].text, context->working[2].text);
switch (context->working[1].value) {
case 0: // =
if (result == 0) {
context->private.pc = (juint16)context->working[3].value;
}
break;
case 1: // !
if (result != 0) {
context->private.pc = (juint16)context->working[3].value;
}
break;
case 2: // <
if (result < 0) {
context->private.pc = (juint16)context->working[3].value;
}
break;
case 3: // >
if (result > 0) {
context->private.pc = (juint16)context->working[3].value;
}
break;
}
}
}
void op_exe(VMContextT *context) {
vmPop(context, &context->working[0]); // Arg count
vmPop(context, &context->working[1]); // Function number
context->private.functions[context->working[1].value](context, (juint16)(context->working[0].value) - 1);
}
void op_jmp(VMContextT *context) {
vmPop(context, &context->working[0]);
//printf("Jumping to %04x\n", (juint16)context->working[0].value);
context->private.pc = (juint16)context->working[0].value;
}
void op_hlt(VMContextT *context) {
(void)(context);
// We don't actually need to do anything for HLT.
}
void op_pop(VMContextT *context) {
readItemFromPC(context, &context->working[0]);
vmPop(context, &context->working[1]);
copyItem(&context->private.variables[context->working[0].value], &context->working[1]);
}
void op_puc(VMContextT *context) {
readItemFromPC(context, &context->working[0]);
vmPush(context, &context->working[0]);
}
void op_puv(VMContextT *context) {
readItemFromPC(context, &context->working[0]);
vmPush(context, &context->private.variables[context->working[0].value]);
}
void op_sub(VMContextT *context) {
vmPop(context, &context->working[0]);
vmPop(context, &context->working[1]);
// Number
context->working[2].value = context->working[0].value - context->working[1].value;
vmPush(context, &context->working[2]);
}
void readItemFromPC(VMContextT *context, PairT *item) {
byte b1;
byte b2;
int y;
//printf("Reading item at offset %04x - %02x\n", context->private.pc + context->private.headerBytes, context->private.memory[context->private.pc]);
// Get the length/value of this item
b1 = context->private.memory[context->private.pc++];
b2 = context->private.memory[context->private.pc++];
#ifdef JOEY_BIG_ENDIAN
item->value = (jint16)((b1 << 8) + b2);
#else
item->value = (jint16)((b2 << 8) + b1);
#endif
// If this is a zero, it's a number. If it's not, it's a string
if (context->private.memory[context->private.pc] > 0) {
if (item->allocated < item->value + 1) {
item->text = (char *)jlRealloc(item->text, (size_t)(item->value + 1));
item->allocated = item->value + 1;
}
for (y=0; y<item->value; y++) {
item->text[y] = (char)context->private.memory[context->private.pc++];
}
item->text[y] = 0;
} else {
context->private.pc++;
item->text[0] = 0;
}
//printf("Ending read (inclusive) at offset %04x - %02x\n", context->private.pc + context->private.headerBytes - 1, context->private.memory[context->private.pc - 1]);
}
void vmSetItem(PairT *item, char *text, jint16 *value) {
juint16 len;
if ((text != 0) && (text[0] != 0)) {
len = (juint16)strlen(text) + 1;
if (item->allocated < len) {
item->text = (char *)jlRealloc(item->text, len);
item->allocated = (jint16)len;
}
strcpy(item->text, text);
item->value = (jint16)len - 1;
} else {
if (item->text != 0) {
item->text[0] = 0;
}
}
if (value != 0) {
item->value = *value;
}
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
void setStringFormat(PairT *item, char *format, ...) {
int len;
va_list args;
// Do we have a buffer at all?
if (item->allocated == 0) {
item->text = (char *)jlMalloc(16 * sizeof(char));
item->allocated = 16;
}
// Attempt to write formatted string into buffer.
va_start(args, format);
len = vsnprintf(item->text, (unsigned long)item->allocated, format, args);
va_end(args);
// Did it succeed?
if (len >= item->allocated) {
// Nope! Allocate more memory and do it again.
item->text = (char *)jlRealloc(item->text, (unsigned long)len);
item->allocated = (jint16)len;
va_start(args, format);
len = vsnprintf(item->text, (unsigned long)item->allocated, format, args);
va_end(args);
}
}
#pragma GCC diagnostic pop
bool vmAddFunction(VMContextT *context, VMFuncT function) {
context->private.functionCount++;
context->private.functions = (VMFuncT *)jlRealloc(context->private.functions, sizeof(VMFuncT) * context->private.functionCount);
context->private.functions[context->private.functionCount - 1] = function;
return true;
}
bool vmCreate(VMContextT *context) {
juint16 x;
// This can be redundant but it doesn't hurt anything.
opcodes[OPCODE_ADD - 1] = op_add;
opcodes[OPCODE_BOC - 1] = op_boc;
opcodes[OPCODE_EXE - 1] = op_exe;
opcodes[OPCODE_JMP - 1] = op_jmp;
opcodes[OPCODE_HLT - 1] = op_hlt;
opcodes[OPCODE_POP - 1] = op_pop;
opcodes[OPCODE_PUC - 1] = op_puc;
opcodes[OPCODE_PUV - 1] = op_puv;
opcodes[OPCODE_SUB - 1] = op_sub;
context->private.sp = 0;
context->private.pc = 0;
context->private.headerBytes = 0;
// Stack
context->private.stack = 0;
context->private.stackCount = 0;
// All variables
context->private.variables = 0;
context->private.variablesCount = 0;
// Native function pointers
context->private.functions = 0;
context->private.functionCount = 0;
// Program space
context->private.memory = 0;
context->private.programSize = 0;
// Exported Labels
context->private.labels = 0;
context->private.labelCount = 0;
// Exported Variables
context->private.variableTable = 0;
context->private.variableTableCount = 0;
// Working RAM
context->working = (PairT *)jlMalloc(sizeof(PairT) * WORKING_SIZE);
for (x=0; x<WORKING_SIZE; x++) {
context->working[x].text = 0;
context->working[x].allocated = 0;
context->working[x].value = 0;
}
return true;
}
bool vmDestroy(VMContextT *context) {
juint16 x;
// Working RAM
for (x=0; x<WORKING_SIZE; x++) {
jlFree(context->working[x].text);
}
jlFree(context->working);
// Program space
jlFree(context->private.memory);
// Native function pointers
jlFree(context->private.functions);
// Label Jump Table
for (x=0; x<context->private.labelCount; x++) {
jlFree(context->private.labels[x].text);
}
jlFree(context->private.labels);
// Exported Varaibles Index
for (x=0; x<context->private.variableTableCount; x++) {
jlFree(context->private.variableTable[x].text);
}
jlFree(context->private.variableTable);
// All variables
for (x=0; x<context->private.variablesCount; x++) {
jlFree(context->private.variables[x].text);
}
jlFree(context->private.variables);
// Stack
for (x=0; x<context->private.stackCount; x++) {
jlFree(context->private.stack[x].text);
}
jlFree(context->private.stack);
return true;
}
bool vmGetPCIndex(VMContextT *context, char *label, juint16 *pc) {
juint16 x;
bool result = false;
for (x=0; x<context->private.labelCount; x++) {
if (strcmp(context->private.labels[x].text, label) == 0) {
*pc = context->private.labels[x].value;
result = true;
break;
}
}
return result;
}
bool vmGetVariable(VMContextT *context, char *name, PairT *item) {
juint16 x;
item = NULL;
if (vmGetVariableIndex(context, name, &x)) {
*item = context->private.variables[x];
}
return (item != NULL);
}
bool vmGetVariableIndex(VMContextT *context, char *name, juint16 *index) {
juint16 x;
for (x=0; x<context->private.variableTableCount; x++) {
if (strcmp(context->private.variableTable[x].text, name) == 0) {
*index = context->private.variableTable[x].value;
return true;
}
}
return false;
}
bool vmIsItemNumber(PairT *item) {
return (item->text == 0) || (item->text[0] == 0);
}
bool vmLoad(VMContextT *context, char *filename) {
char temp[4];
FILE *binFile;
juint16 tempUnsigned;
juint16 x;
juint16 y;
int length;
binFile = fopen(filename, "rb");
if (!binFile) {
return false;
}
// Get file size for later
fseek(binFile, 0, SEEK_END);
length = (int)ftell(binFile);
fseek(binFile, 0, SEEK_SET);
// Read header & Version Number
fread(temp, 4, 1, binFile);
if ((temp[0] != 'D') || (temp[1] != 'B') || (temp[2] != 'L') || (temp[3] != 0)) {
fclose(binFile);
return false;
}
// Read exported label entries
fread(&tempUnsigned, sizeof(juint16), 1, binFile);
#ifdef JOEY_BIG_ENDIAN
context->private.labelCount = jlByteSwap(tempUnsigned);
#else
context->private.labelCount = tempUnsigned;
#endif
context->private.labels = (UPairT *)jlMalloc(sizeof(UPairT) * context->private.labelCount);
for (x=0; x<context->private.labelCount; x++) {
fread(&tempUnsigned, sizeof(juint16), 1, binFile); // Address of label
#ifdef JOEY_BIG_ENDIAN
context->private.labels[x].value = jlByteSwap(tempUnsigned);
#else
context->private.labels[x].value = tempUnsigned;
#endif
y = (juint16)fgetc(binFile); // Length of label name
// Label Name
context->private.labels[x].text = (char *)jlMalloc((y + 1) * sizeof(char));
fread(context->private.labels[x].text, sizeof(char), y, binFile);
context->private.labels[x].text[y] = 0;
context->private.labels[x].allocated = (jint16)(y + 1);
}
// Read exported variable entries
fread(&tempUnsigned, sizeof(juint16), 1, binFile);
#ifdef JOEY_BIG_ENDIAN
context->private.variableTableCount = jlByteSwap(tempUnsigned);
#else
context->private.variableTableCount = tempUnsigned;
#endif
context->private.variableTable = (UPairT *)jlMalloc(sizeof(UPairT) * context->private.variableTableCount);
for (x=0; x<context->private.variableTableCount; x++) {
fread(&tempUnsigned, sizeof(juint16), 1, binFile);
#ifdef JOEY_BIG_ENDIAN
context->private.variableTable[x].value = jlByteSwap(tempUnsigned);
#else
context->private.variableTable[x].value = tempUnsigned;
#endif
y = (juint16)fgetc(binFile); // Length of variable name
// Variable Name
context->private.variableTable[x].text = (char *)jlMalloc((y + 1) * sizeof(char));
fread(context->private.variableTable[x].text, sizeof(char), y, binFile);
context->private.variableTable[x].text[y] = 0;
context->private.variableTable[x].allocated = (jint16)(y + 1);
}
// Read total variable count
fread(&tempUnsigned, sizeof(juint16), 1, binFile);
#ifdef JOEY_BIG_ENDIAN
context->private.variablesCount = jlByteSwap(tempUnsigned);
#else
context->private.variablesCount = tempUnsigned;
#endif
context->private.variables = (PairT *)jlMalloc(sizeof(PairT) * context->private.variablesCount);
for (x=0; x<context->private.variablesCount; x++) {
context->private.variables->text = 0;
context->private.variables->allocated = 0;
context->private.variables->value = 0;
}
// Record position in file for debugging purposes
context->private.headerBytes = (juint16)ftell(binFile);
//printf("PC offset is %04x\n", context->private.headerBytes);
// Read program byte stream
context->private.programSize = (juint16)(length - ftell(binFile));
context->private.memory = (byte *)jlMalloc(sizeof(byte) * context->private.programSize);
fread(context->private.memory, sizeof(char) * context->private.programSize, 1, binFile);
fclose(binFile);
return true;
}
bool vmPop(VMContextT *context, PairT *item) {
if (context->private.sp > 0) {
context->private.sp--;
copyItem(item, &context->private.stack[context->private.sp]);
return true;
}
return false;
}
bool vmPush(VMContextT *context, PairT *item) {
// Grow stack if needed.
if (context->private.sp + 1 > context->private.stackCount) {
context->private.stackCount = context->private.sp + 1;
context->private.stack = (PairT *)jlRealloc(context->private.stack, sizeof(PairT) * context->private.stackCount);
context->private.stack[context->private.sp].text = 0;
context->private.stack[context->private.sp].allocated = 0;
context->private.stack[context->private.sp].value = 0;
}
copyItem(&context->private.stack[context->private.sp], item);
context->private.sp++;
return true;
}
bool vmSetPC(VMContextT *context, char *label) {
juint16 pc;
bool result = false;
if (label == NULL) {
context->private.pc = 0;
result = true;
} else {
if (vmGetPCIndex(context, label, &pc)) {
context->private.pc = pc;
result = true;
}
}
return result;
}
bool vmSetPCByIndex(VMContextT *context, juint16 index) {
context->private.pc = index; // Currently, the PC is the index we return
return true;
}
bool vmSetVariable(VMContextT *context, char *name, PairT *item) {
PairT *target = NULL;
if (vmGetVariable(context, name, target)) {
copyItem(target, item);
return true;
}
return false;
}
bool vmSetVariableByIndex(VMContextT *context, jint16 index, PairT *item) {
copyItem(&context->private.variables[index], item);
return true;
}
bool vmStep(VMContextT *context) {
byte opcode = context->private.memory[context->private.pc++];
//printf("Fetched opcode at file offset %04x (PC %04x): %02x\n", context->private.pc + context->private.headerBytes - 1, context->private.pc - 1, opcode);
opcodes[opcode - 1](context);
return (opcode != OPCODE_HLT);
}
bool vmRun(VMContextT *context) {
while (vmStep(context))
;
return true;
}

82
vm/vm.h Normal file
View file

@ -0,0 +1,82 @@
#ifndef DBLTW_VM_H
#define DBLTW_VM_H
#include "joey.h"
#define OPCODE_ADD 0x01 // Add
#define OPCODE_BOC 0x02 // Branch On Condition
#define OPCODE_EXE 0x03 // Execute VM Function
#define OPCODE_JMP 0x04 // Jump to Address
#define OPCODE_HLT 0x05 // Halt Execution
#define OPCODE_POP 0x06 // Pop into Variable
#define OPCODE_PUC 0x07 // Push Constant
#define OPCODE_PUV 0x08 // Push Variable
#define OPCODE_SUB 0x09 // Subtract
#define OPCODE_LAST OPCODE_SUB
typedef struct vmContext_ VMContextT;
typedef void (*VMFuncT)(VMContextT *context, juint16 stackCount);
typedef struct {
jint16 value;
jint16 allocated;
char *text;
} PairT; // Signed / string pair.
typedef struct {
juint16 value;
jint16 allocated;
char *text;
} UPairT; // Unsigned / string pair.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpadded"
typedef struct {
PairT *stack; // Stack for computations
PairT *variables; // All variable storage
UPairT *labels; // Lable jump table
UPairT *variableTable; // Exported variable name-to-index table
juint16 variablesCount; // Number of variables in program
juint16 labelCount; // Number of exported labels currently loaded
juint16 variableTableCount; // Number of exported variables currently loaded
juint16 stackCount; // Maximum size stack has been
juint16 sp; // Stack pointer
juint16 pc; // Program counter
byte *memory; // Program byte stream
juint16 programSize; // Number of bytes in program byte stream
VMFuncT *functions; // Native function pointers
juint16 functionCount; // Number of native functions attached
juint16 headerBytes; // Header bytes (used for PC calculations)
} PrivateT;
#pragma GCC diagnostic pop
typedef struct vmContext_ {
PrivateT private;
PairT *working; // Working space for operands
} VMContextT;
bool vmAddFunction(VMContextT *context, VMFuncT function);
bool vmCreate(VMContextT *context);
bool vmDestroy(VMContextT *context);
bool vmGetPCIndex(VMContextT *context, char *label, juint16 *pc);
bool vmGetVariable(VMContextT *context, char *name, PairT *item);
bool vmGetVariableIndex(VMContextT *context, char *name, juint16 *index);
bool vmIsItemNumber(PairT *item);
bool vmLoad(VMContextT *context, char *filename);
bool vmPop(VMContextT *context, PairT *item);
bool vmPush(VMContextT *context, PairT *item);
void vmSetItem(PairT *item, char *text, jint16 *value);
bool vmSetPC(VMContextT *context, char *label);
bool vmSetPCByIndex(VMContextT *context, juint16 index);
bool vmSetVariable(VMContextT *context, char *name, PairT *item);
bool vmSetVariableByIndex(VMContextT *context, jint16 index, PairT *item);
bool vmStep(VMContextT *context);
bool vmRun(VMContextT *context);
#endif

17
vm/vm.pro Normal file
View file

@ -0,0 +1,17 @@
TEMPLATE = app
include(/home/scott/joey/dist/joey.pri)
HEADERS += \
vm.h
SOURCES += \
main.c \
vm.c
OTHER_FILES += \
../notes.txt
DISTFILES += \
../test.dbl \
../test.lst