Parscript In-Depth
Parscript is a single header compiled programming language. My main goal with this project was to build a programming language I could use to manipulate large amounts of structured information in a similar way to how a Vertex Shader or a Pixel Shader manipulate vertices and colour.
The Compiler parses in and compiles the script into a custom specification of assembly that is run by the Parscript Virtual Machine.
The compiler follows these steps to assemble the program:
- Code Sanitization
- Finding Scopes
- Extracting Instructions
- Assembling
Code Sanitization
The code sanitization removes all comments and whitespace from the input code.
Finding Scopes
For the Virtual Machine to know where to store or take information from we need to know our scopes and what is in them. There are three types of scopes:
- Global Scope
- Work Scope
- Local Scope
The Global Scope is shared by all work units, good for program constants such as Delta Time or gravity as well as counters. The Work Scope is the bit of information you are currently processing. In the case of the example above, a particle. The Local Scope is a bit of memory the program can use to store temporary information. It gets zeroed after each work unit is done(unless specified not to do so).
Extracting instructions
This step goes over the Worker function and extracts all the single line function calls.
Assembling
During this step, the assembler converts all the extracted instructions in to the Virtual Machine Code.
// --------------------- Compiler Resolvers ---------------------
#define CompilerResolver(OP_CODE, ASSIGNS, OPERAND_COUNT)\
[&SOResolver, &program](const std::string& function) {\
program.push_back(OP_CODE);\
if constexpr (ASSIGNS) { AssignmentPush() }\
if constexpr (OPERAND_COUNT > 0) { for(int idx = 0; idx < OPERAND_COUNT; ++idx) { OperandPush(idx) } }\
}\
resolvers["Float"]["::++"] = CompilerResolver(1, false, 1); // INC_FLOAT [&Scope + Offset]
resolvers["Float"]["::--"] = CompilerResolver(2, false, 1); // DEC_FLOAT [&Scope + Offset]
resolvers["Float"]["::+"] = CompilerResolver(3, true, 2); // ADD_FLOAT [&Scope + Offset], [&Scope + Offset], [&Scope + Offset]
resolvers["Float"]["::-"] = CompilerResolver(4, true, 2); // SUB_FLOAT [&Scope + Offset], [&Scope + Offset], [&Scope + Offset]
resolvers["Float"]["::*"] = CompilerResolver(5, true, 2); // MUL_FLOAT [&Scope + Offset], [&Scope + Offset], [&Scope + Offset]
resolvers["Float"]["::>"] = CompilerResolver(14, true, 2); // BIGGER_THAN_FLOAT [&Scope + Offset], [&Scope + Offset], [&Scope + Offset]
resolvers["Float"]["::<"] = CompilerResolver(15, true, 2); // SMALLER_THAN_FLOAT [&Scope + Offset], [&Scope + Offset], [&Scope + Offset]
// Integer arithmetic /////////////////////////////////////////
resolvers["Int"]["::++"] = CompilerResolver(6, false, 1); // INC_INT [&Scope + Offset]
resolvers["Int"]["::--"] = CompilerResolver(7, false, 1); // DEC_INT [&Scope + Offset]
resolvers["Int"]["::+"] = CompilerResolver(10, true, 2); // ADD_INT [&Scope + Offset], [&Scope + Offset], [&Scope + Offset]
resolvers["Int"]["::-"] = CompilerResolver(11, true, 2); // SUB_INT [&Scope + Offset], [&Scope + Offset], [&Scope + Offset]
resolvers["Int"]["::*"] = CompilerResolver(12, true, 2); // MUL_INT [&Scope + Offset], [&Scope + Offset], [&Scope + Offset]
// Vm Instructions ////////////////////////////////////////////
resolvers["VM"]["::Halt"] = CompilerResolver(0, false, 0); // PAR_HALT
resolvers["VM"]["::HaltConditional"] = CompilerResolver(13, false, 1); // PAR_HALT_CONDITIONAL [&Scope + Offset]
Parscript Virtual Machine
The Parscript has the following define Instruction Set making use of computed Gotos to run the program.
////////////////////////////////////////////////////////////////
// VM Instructions
DeclareOp(PAR_HALT, 1, // PAR_HALT
++workUnitIdx;
if(workUnitIdx >= workScopeCount) {
// Work is done, cleanup memory and exit VM
delete pScopes[LOCAL_SCOPE];
return;
}
else {
// Reset the PC to 0 and advance work unit
pProgram->programCounter = 0U;
pScopes[WORK_SCOPE] = static_cast<uint8_t*>(pWorkScopes) + (workUnitIdx * workScopeSize);
// Zero local scope if needed
if(zeroLocalScope)
std::memset(pScopes[LOCAL_SCOPE], 0, LOCAL_SCOPE_SIZE);
// Start VM on the new local scope work unit, this skips the Step op
goto *opLut[pProgram->pCode[pProgram->programCounter]];
});
DeclareOp(PAR_HALT_CONDITIONAL, 3, { auto* l = (bool*)(pScopes[Operand(0U)] + Operand(1U)); if(*l) goto *opLut[0]; });
////////////////////////////////////////////////////////////////
// Floating point arithmetic instructions
DeclareOp(INC_FLOAT, 3,{ auto* l = (float*)(pScopes[Operand(0U)] + Operand(1U)); *l += 1.f; }); // INC_FLOAT_CONTENT &Scope + offset
DeclareOp(DEC_FLOAT, 3,{ auto* l = (float*)(pScopes[Operand(0U)] + Operand(1U)); *l -= 1.f; }); // DEC_FLOAT_CONTENT &Scope + offset
DeclareOp(ADD_FLOAT, 7,{ auto* l = (float*)(pScopes[Operand(0U)] + Operand(1U)); *l = *((float*)((pScopes[Operand(2U)] + Operand(3U)))) + *((float*)((pScopes[Operand(4U)] + Operand(5U)))); }); // ADD_FLOAT &Scope + offset, &Scope + offset
DeclareOp(SUB_FLOAT, 7,{ auto* l = (float*)(pScopes[Operand(0U)] + Operand(1U)); *l = *((float*)((pScopes[Operand(2U)] + Operand(3U)))) - *((float*)((pScopes[Operand(4U)] + Operand(5U)))); }); // SUB_FLOAT &Scope + offset, &Scope + offset
DeclareOp(MUL_FLOAT, 7,{ auto* l = (float*)(pScopes[Operand(0U)] + Operand(1U)); *l = *((float*)((pScopes[Operand(2U)] + Operand(3U)))) * *((float*)((pScopes[Operand(4U)] + Operand(5U)))); }); // MUL_FLOAT &Scope + offset, &Scope + offset, &Scope + offset
DeclareOp(BIGGER_THAN_FLOAT, 7,{ auto* l = (bool*)(pScopes[Operand(0U)] + Operand(1U)); *l = *((float*)((pScopes[Operand(2U)] + Operand(3U)))) > *((float*)((pScopes[Operand(4U)] + Operand(5U)))); });
DeclareOp(SMALLER_THAN_FLOAT, 7,{ auto* l = (bool*)(pScopes[Operand(0U)] + Operand(1U)); *l = *((float*)((pScopes[Operand(2U)] + Operand(3U)))) < *((float*)((pScopes[Operand(4U)] + Operand(5U)))); });
////////////////////////////////////////////////////////////////
// Integer arithmetic instructions
DeclareOp(INC_INT, 3,{ auto* l = (int*)(pScopes[Operand(0U)] + Operand(1U)); *l += 1; }); // INC_INT_CONTENT &Scope + offset
DeclareOp(DEC_INT, 3,{ auto* l = (int*)(pScopes[Operand(0U)] + Operand(1U)); *l -= 1; }); // DEC_INT_CONTENT &Scope + offset
DeclareOp(INC_UINT, 3,{ auto* l = (unsigned int*)(pScopes[Operand(0U)] + Operand(1U)); *l += 1U; }); // INC_UINT_CONTENT &Scope + offset
DeclareOp(DEC_UINT, 3,{ auto* l = (unsigned int*)(pScopes[Operand(0U)] + Operand(1U)); *l -= 1U; }); // DEC_UINT_CONTENT &Scope + offset
DeclareOp(ADD_INT, 7,{ auto* l = (int*)(pScopes[Operand(0U)] + Operand(1U)); *l = *((int*)((pScopes[Operand(2U)] + Operand(3U)))) + *((int*)((pScopes[Operand(4U)] + Operand(5U)))); }); // ADD_INT &Scope + offset, &Scope + offset
DeclareOp(SUB_INT, 7,{ auto* l = (int*)(pScopes[Operand(0U)] + Operand(1U)); *l = *((int*)((pScopes[Operand(2U)] + Operand(3U)))) - *((int*)((pScopes[Operand(4U)] + Operand(5U)))); }); // SUB_INT &Scope + offset, &Scope + offset
DeclareOp(MUL_INT, 7,{ auto* l = (int*)(pScopes[Operand(0U)] + Operand(1U)); *l = *((int*)((pScopes[Operand(2U)] + Operand(3U)))) * *((int*)((pScopes[Operand(4U)] + Operand(5U)))); }); // MUL_FLOAT &Scope + offset, &Scope + offset, &Scope + offset