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

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:

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

Click here for the repository of the project.