/* * JoeyDev * Copyright (C) 2018-2023 Scott Duensing * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors be held liable for any damages * arising from the use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software * in a product, an acknowledgment in the product documentation would be * appreciated but is not required. * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * 3. This notice may not be removed or altered from any source distribution. */ #pragma clang diagnostic push #pragma ide diagnostic ignored "EmptyDeclOrStmt" #include #include "common.h" #include "vecparse.h" #include "utils.h" #define IS_NUMBER(n) (n & 0x8000 ? FALSE : TRUE) typedef struct KeywordsS { char *command; ParserKeywordT id; } KeywordsT; typedef struct OperatorsS { char *operator; ParserMathT id; } OperatorsT; typedef struct LabelS { char *key; int value; int line; } LabelT; static int labelGetValue(int lineNumber, VecByteCodeT *bytecode, LabelT *labels, LabelT ***unresolved, char *label); static void outputByte(VecByteCodeT *bytecode, unsigned short word); static void outputWord(VecByteCodeT *bytecode, unsigned short word); static char *parserGetNextLine(char *str, char **savePtr); static gboolean parserGetNextValue(char *token, char **valueEnd, char ***variables, int *x); static int parserGetWord(char *wordList, char **tokenEnd); static gboolean parserGetX(char **tokenEnd, char ***variables, int *x); static gboolean parserGetXY(char **tokenEnd, char ***variables, int *x, int *y); static gboolean parserGetXYZ(char **tokenEnd, char ***variables, int *x, int *y, int *z); static gboolean variableCollect(char *value, char ***variables, int *result); static int labelGetValue(int lineNumber, VecByteCodeT *bytecode, LabelT *labels, LabelT ***unresolved, char *label) { int index; int found = -1; LabelT *temp; // Do we know this label? We search ourselves so it's case-insensitive. for (index = 0; index < hmlen(labels); index++) { if (strcasecmp(label, labels[index].key) == 0) { found = index; break; } } if (found < 0) { // We don't yet know this label, so it's a forward reference. // Record it with a negative location, so we can resolve it later. temp = NEW(LabelT); temp->key = strdup(label); temp->value = bytecode->length; temp->line = lineNumber; arrput(*unresolved, temp); index = 0 - ((int)arrlen(*unresolved) + 1); } else { index = labels[index].value; } return index; } static void outputByte(VecByteCodeT *bytecode, unsigned short word) { unsigned char byte = (unsigned char)word; utilEnsureBufferSize(&bytecode->bytes, &bytecode->bufferSize, bytecode->length + 1); // If the word passed in is a variable, set the MSb in the byte as well. if (word & 0x8000) { byte |= 0x80; } bytecode->bytes[bytecode->length++] = byte; } static void outputWord(VecByteCodeT *bytecode, unsigned short word) { utilEnsureBufferSize(&bytecode->bytes, &bytecode->bufferSize, bytecode->length + 2); bytecode->bytes[bytecode->length++] = (word & 0xFF00) >> 8; bytecode->bytes[bytecode->length++] = word & 0x00FF; } // Custom strtok_r because: // A sequence of two or more contiguous delimiter bytes in the // parsed string is considered to be a single delimiter. // Which is exactly what we don't want for the line parser. static char *parserGetNextLine(char *str, char **savePtr) { char *token = NULL; if (str) { *savePtr = str; } if (!*savePtr) { return NULL; } token = *savePtr; *savePtr = strpbrk(*savePtr, "\n"); if (*savePtr) { **savePtr = 0; *savePtr = *savePtr + 1; } while (token != NULL && (token[0] == ' ' || token[0] == '\t')) token++; return token; } static gboolean parserGetNextValue(char *token, char **valueEnd, char ***variables, int *x) { char *value; // Return next value in a comma separated list. value = strtok_r(token, ",", valueEnd); if (value == NULL) return FALSE; return variableCollect(value, variables, x);; } static int parserGetWord(char *wordList, char **tokenEnd) { char *token; char *word; char *wordEnd = NULL; char *mutable; int index = -1; // Returns -1 if the word is not found in the wordlist. // Returns a zero-based index of the position in the list // if one of the words is found. token = strtok_r(NULL, " ", tokenEnd); if (token == NULL) return index; mutable = strdup(wordList); word = strtok_r(mutable, " ", &wordEnd); while (word != NULL) { index++; if (strcasecmp(token, word) == 0) break; word = strtok_r(NULL, " ", &wordEnd); } DEL(mutable); return index; } static gboolean parserGetX(char **tokenEnd, char ***variables, int *x) { char *value; // Return single value. value = strtok_r(NULL, " ", tokenEnd); if (value == NULL) return FALSE; return variableCollect(value, variables, x); } static gboolean parserGetXY(char **tokenEnd, char ***variables, int *x, int *y) { char *token; char *valueEnd; // Return values of X,Y pair. token = strtok_r(NULL, " ", tokenEnd); if (token == NULL) return FALSE; if (!parserGetNextValue(token, &valueEnd, variables, x)) return FALSE; if (!parserGetNextValue(NULL, &valueEnd, variables, y)) return FALSE; return TRUE; } static gboolean parserGetXYZ(char **tokenEnd, char ***variables, int *x, int *y, int *z) { char *token; char *valueEnd; // Return values of X,Y pair. token = strtok_r(NULL, " ", tokenEnd); if (token == NULL) return FALSE; if (!parserGetNextValue(token, &valueEnd, variables, x)) return FALSE; if (!parserGetNextValue(NULL, &valueEnd, variables, y)) return FALSE; if (!parserGetNextValue(NULL, &valueEnd, variables, z)) return FALSE; return TRUE; } static gboolean variableCollect(char *value, char ***variables, int *result) { int index; int found; char *endPtr = NULL; char **vars = *variables; // Is it a variable or number? if (strlen(value) > 1 && value[0] == '%') { // It's a variable. Do we know it? found = -1; for (index = 0; index < arrlen(vars); index++) { if (strcasecmp(vars[index], value) == 0) { found = index; // Found. break; } } if (found < 0) { // Variable is not yet known. Add it to list. found = arrlen(vars); arrput(vars, value); } // Set MSb to indicate it's a variable. found |= 0x8000; } else { // It's a number. errno = 0; endPtr = NULL; found = (int)strtol(value, &endPtr, 10); if (errno != 0 || *endPtr != 0) return FALSE; // Is it negative? If so, we need it in our format. if (found < 0) { found = -found; found |= 0x4000; } // Ensure MSb is cleared, so we know it's a number. found &= 0x7fff; } *result = found; return TRUE; } int vecparser(char *programIn, VecByteCodeT *bytecode) { int keyword; char *line; char *lineEnd; gboolean lineOkay; gboolean isOkay; char *token; char *tokenEnd; int lineNumber; int x1; int y1; int x2; int y2; PointT p1; PointT p2; PointT *points = NULL; // Used to collect points for LINE char **variables = NULL; // Array of known variables and their IDs LabelT *labels = NULL; // Known labels and their byte offsets LabelT *label = NULL; // Temp label for array management. LabelT **unresolved = NULL; // List of unresolved label uses and their byte offsets int result = -1; // Returns -1 on success or line number of first error. KeywordsT commands[] = { { "BOX", PARSE_BOX }, { "CALL", PARSE_CALL }, { "CIRCLE", PARSE_CIRCLE }, { "CLEAR", PARSE_CLEAR }, { "COLOR", PARSE_COLOR }, { "//", PARSE_COMMENT }, { "ELLIPSE", PARSE_ELLIPSE }, { "GOTO", PARSE_GOTO }, { "IF", PARSE_IF }, { "FILL", PARSE_FILL }, { "LINE", PARSE_LINE }, { "PALETTE", PARSE_PALETTE }, { "PLOT", PARSE_PLOT }, { "RECTANGLE", PARSE_RECTANGLE }, { "RETURN", PARSE_RETURN }, { NULL, PARSE_NONE } }; OperatorsT math[] = { { "=", MATH_ASSIGN }, { "+", MATH_ADD }, { "-", MATH_SUBTRACT }, { "*", MATH_MULTIPLY }, { "/", MATH_DIVIDE }, { "RND", MATH_RND }, { "MOD", MATH_MOD }, { "POW", MATH_POW }, { "SQRT", MATH_SQRT }, { "ABS", MATH_ABS }, { "COS", MATH_COS }, { "SIN", MATH_SIN }, { "TAN", MATH_TAN }, { NULL, MATH_NONE } }; // Parse code. lineNumber = 0; line = parserGetNextLine(programIn, &lineEnd); while (line != NULL) { // Get the first token on the line. token = strtok_r(line, " ", &tokenEnd); // Is this something we care about? It'll be math, a label, or a keyword. lineOkay = FALSE; // Is this a blank line with no tokens? if (token == NULL) { // Yep - just move on. lineOkay = TRUE; } else { // blank line debug("[%s]\n", token); // Is it math? if (strlen(token) > 1 && token[0] == '%') { // We're doing some kind of math. // Look up variable index in table, or add it if needed. // Variable index is stored in y1 for later. y1 = -1; for (y2 = 0; y2 < arrlen(variables); y2++) { if (strcasecmp(variables[y2], token) == 0) { y1 = y2; // Found. break; } } if (y1 < 0) { // Variable is not yet known. Add it to list. y1 = arrlen(variables); arrput(variables, token); } // Mark as variable. y1 |= 0x80; // Find the operator. token = strtok_r(NULL, " ", &tokenEnd); if (token != NULL) { keyword = 0; while (math[keyword].operator) { if (strcasecmp(math[keyword].operator, token) == 0) { // Yep! Gather arguments and generate bytecode. if (!parserGetX(&tokenEnd, &variables, &x1)) break; outputByte(bytecode, PARSE_MATH); outputByte(bytecode, y1); outputByte(bytecode, math[keyword].id); outputWord(bytecode, x1); lineOkay = TRUE; break; } keyword++; } } } else { // doing math // Is it a label? if (strlen(token) > 1 && token[strlen(token) - 1] == ':') { // It's a label. // Remove trailing colon. token[strlen(token) - 1] = 0; // Have we already used this label? x2 = -1; // We search ourselves so it's case-insensitive. for (x1 = 0; x1 < shlen(labels); x1++) { if (strcasecmp(token, labels[x1].key) == 0) { x2 = x1; break; } } if (x2 < 0) { // New label. Add to hashmap. shput(labels, token, bytecode->length); lineOkay = TRUE; } } else { // doing a label // It's a keyword. keyword = 0; while (commands[keyword].command) { if (strcasecmp(commands[keyword].command, token) == 0) { // Yep! Gather arguments and generate bytecode. switch (commands[keyword].id) { case PARSE_NONE: // Won't happen, but silences an error. break; case PARSE_BOX: // Box (value),(value) to (value),(value) if (!parserGetXY(&tokenEnd, &variables, &x1, &y1)) break; if (parserGetWord("TO", &tokenEnd) < 0) break; if (!parserGetXY(&tokenEnd, &variables, &x2, &y2)) break; if (IS_NUMBER(x1) && IS_NUMBER(x2)) if (x2 < x1) break; if (IS_NUMBER(x1)) if (x1 < 0 || x1 > 319) break; if (IS_NUMBER(x2)) if (x2 < 0 || x2 > 319) break; if (IS_NUMBER(y1) && IS_NUMBER(y2)) if (y2 < y1) break; if (IS_NUMBER(y1)) if (y1 < 0 || y1 > 199) break; if (IS_NUMBER(y2)) if (y2 < 0 || y2 > 199) break; outputByte(bytecode, PARSE_BOX); outputWord(bytecode, x1); outputWord(bytecode, y1); outputWord(bytecode, x2); outputWord(bytecode, y2); lineOkay = TRUE; break; case PARSE_CALL: // Call (label) if (tokenEnd == NULL) break; outputByte(bytecode, PARSE_CALL); x1 = labelGetValue(lineNumber, bytecode, labels, &unresolved, tokenEnd); outputWord(bytecode, x1); lineOkay = TRUE; break; case PARSE_CIRCLE: // Circle (value),(value) radius (value) if (!parserGetXY(&tokenEnd, &variables, &x1, &y1)) break; if (parserGetWord("RADIUS", &tokenEnd) < 0) break; if (!parserGetX(&tokenEnd, &variables, &y2)) break; if (IS_NUMBER(x1)) if (x1 < 0 || x1 > 319) break; if (IS_NUMBER(y1)) if (y1 < 0 || y1 > 199) break; outputByte(bytecode, PARSE_CIRCLE); outputWord(bytecode, y2); outputWord(bytecode, x1); outputWord(bytecode, y1); lineOkay = TRUE; break; case PARSE_CLEAR: outputByte(bytecode, PARSE_CLEAR); lineOkay = TRUE; break; case PARSE_COLOR: // Color (short) if (!parserGetX(&tokenEnd, &variables, &x1)) break; if (IS_NUMBER(x1)) if (x1 < 0 || x1 > 15) break; outputByte(bytecode, PARSE_COLOR); outputByte(bytecode, x1); lineOkay = TRUE; break; case PARSE_COMMENT: // Eat the rest of the line. while (token != NULL) { token = strtok_r(NULL, " ", &tokenEnd); } lineOkay = TRUE; break; case PARSE_ELLIPSE: // Ellipse (value),(value) to (value),(value) if (!parserGetXY(&tokenEnd, &variables, &x1, &y1)) break; if (parserGetWord("TO", &tokenEnd) < 0) break; if (!parserGetXY(&tokenEnd, &variables, &x2, &y2)) break; if (IS_NUMBER(x1) && IS_NUMBER(x2)) if (x2 < x1) break; if (IS_NUMBER(x1)) if (x1 < 0 || x1 > 319) break; if (IS_NUMBER(x2)) if (x2 < 0 || x2 > 319) break; if (IS_NUMBER(y1) && IS_NUMBER(y2)) if (y2 < y1) break; if (IS_NUMBER(y1)) if (y1 < 0 || y1 > 199) break; if (IS_NUMBER(y2)) if (y2 < 0 || y2 > 199) break; outputByte(bytecode, PARSE_ELLIPSE); outputWord(bytecode, x1); outputWord(bytecode, y1); outputWord(bytecode, x2); outputWord(bytecode, y2); lineOkay = TRUE; break; case PARSE_FILL: // Fill (value),(value) {to (value} if (!parserGetXY(&tokenEnd, &variables, &x1, &y1)) break; if (IS_NUMBER(x1)) if (x1 < 0 || x1 > 319) break; if (IS_NUMBER(y1)) if (y1 < 0 || y1 > 199) break; // Do they want to fill to a certain color? Or over the current color? x2 = 16; // 16 == Fill, otherwise FillTo if (parserGetWord("TO", &tokenEnd) >= 0) { if (!parserGetX(&tokenEnd, &variables, &x2)) break; if (IS_NUMBER(x2)) if (x2 < 0 || x2 > 15) break; } outputByte(bytecode, PARSE_FILL); outputWord(bytecode, x1); outputWord(bytecode, y1); outputByte(bytecode, x2); lineOkay = TRUE; break; case PARSE_GOTO: // Goto (label) if (tokenEnd == NULL) break; outputByte(bytecode, PARSE_GOTO); x1 = labelGetValue(lineNumber, bytecode, labels, &unresolved, tokenEnd); outputWord(bytecode, x1); lineOkay = TRUE; break; case PARSE_IF: // If (value) (compare) (value) (goto|call) (label) if (!parserGetX(&tokenEnd, &variables, &x1)) break; y1 = parserGetWord("== != < > <= >=", &tokenEnd); if (y1 < 0) break; if (!parserGetX(&tokenEnd, &variables, &x2)) break; y2 = parserGetWord("goto call", &tokenEnd); if (y2 < 0) break; if (tokenEnd == NULL) break; outputByte(bytecode, PARSE_IF); outputWord(bytecode, x1); outputByte(bytecode, y1); outputWord(bytecode, x2); outputByte(bytecode, y2); x1 = labelGetValue(lineNumber, bytecode, labels, &unresolved, tokenEnd); outputWord(bytecode, x1); lineOkay = TRUE; break; case PARSE_LABEL: // Won't happen. break; case PARSE_LINE: // Line (value),(value) to (value),(value) [to ...] points = NULL; if (!parserGetXY(&tokenEnd, &variables, &p1.x, &p1.y)) break; if (parserGetWord("TO", &tokenEnd) < 0) break; if (!parserGetXY(&tokenEnd, &variables, &p2.x, &p2.y)) break; if (IS_NUMBER(p1.x)) if (p1.x < 0 || p1.x > 319) break; if (IS_NUMBER(p1.y)) if (p1.y < 0 || p1.y > 199) break; if (IS_NUMBER(p2.x)) if (p2.x < 0 || p2.x > 319) break; if (IS_NUMBER(p2.y)) if (p2.y < 0 || p2.y > 199) break; arrput(points, p1); arrput(points, p2); isOkay = TRUE; while (parserGetWord("TO", &tokenEnd) >= 0) { if (!parserGetXY(&tokenEnd, &variables, &p1.x, &p1.y)) { // Error. isOkay = FALSE; break; } if (IS_NUMBER(p1.x)) if (p1.x < 0 || p1.x > 319) { isOkay = FALSE; break; } if (IS_NUMBER(p1.y)) if (p1.y < 0 || p1.y > 199) { isOkay = FALSE; break; } arrput(points, p1); } if (isOkay) { outputByte(bytecode, PARSE_LINE); outputWord(bytecode, arrlen(points)); for (x1 = 0; x1 < arrlen(points); x1++) { outputWord(bytecode, points[x1].x); outputWord(bytecode, points[x1].y); } lineOkay = TRUE; } break; case PARSE_MATH: // Won't happen. break; case PARSE_PALETTE: // Palette (short) AS (short),(short),(short) if (!parserGetX(&tokenEnd, &variables, &x1)) break; if (parserGetWord("AS", &tokenEnd) < 0) break; if (!parserGetXYZ(&tokenEnd, &variables, &x2, &y1, &y2)) break; if (IS_NUMBER(x1)) if (x1 < 0 || x1 > 15) break; if (IS_NUMBER(x2)) if (x2 < 0 || x2 > 15) break; if (IS_NUMBER(y1)) if (y1 < 0 || y1 > 15) break; if (IS_NUMBER(y2)) if (y2 < 0 || y2 > 15) break; outputByte(bytecode, PARSE_PALETTE); outputByte(bytecode, x1); outputByte(bytecode, x2); outputByte(bytecode, y1); outputByte(bytecode, y2); lineOkay = TRUE; break; case PARSE_PLOT: // Plot (value),(value) if (!parserGetXY(&tokenEnd, &variables, &x1, &y1)) break; if (IS_NUMBER(x1)) if (x1 < 0 || x1 > 319) break; if (IS_NUMBER(y1)) if (y1 < 0 || y1 > 199) break; outputByte(bytecode, PARSE_PLOT); outputWord(bytecode, x1); outputWord(bytecode, y1); lineOkay = TRUE; break; case PARSE_RECTANGLE: // Rectangle (value),(value) to (value),(value) if (!parserGetXY(&tokenEnd, &variables, &x1, &y1)) break; if (parserGetWord("TO", &tokenEnd) < 0) break; if (!parserGetXY(&tokenEnd, &variables, &x2, &y2)) break; if (IS_NUMBER(x1) && IS_NUMBER(x2)) if (x2 < x1) break; if (IS_NUMBER(x1)) if (x1 < 0 || x1 > 319) break; if (IS_NUMBER(x2)) if (x2 < 0 || x2 > 319) break; if (IS_NUMBER(y1) && IS_NUMBER(y2)) if (y2 < y1) break; if (IS_NUMBER(y1)) if (y1 < 0 || y1 > 199) break; if (IS_NUMBER(y2)) if (y2 < 0 || y2 > 199) break; outputByte(bytecode, PARSE_RECTANGLE); outputWord(bytecode, x1); outputWord(bytecode, y1); outputWord(bytecode, x2); outputWord(bytecode, y2); lineOkay = TRUE; break; case PARSE_RETURN: // Return outputByte(bytecode, PARSE_RETURN); lineOkay = TRUE; break; } // switch // Unwind point array if needed. if (points != NULL) { while (arrlen(points) > 0) { arrdel(points, 0); } } // Stop looking for this keyword - we handled it. break; } else { // Keep looking until we find this keyword or run out of commands. keyword++; } // keyword match } // loop over commands } // label } // math } // blank line // Is everything still okay? if (!lineOkay) { // Nope - error. result = lineNumber; break; } // Move to next line. line = parserGetNextLine(NULL, &lineEnd); lineNumber++; } // read program line // Resolve forward label declarations and patch bytecode. if (lineOkay) { #ifdef DEBUG_MODE for (y1 = 0; y1 < shlen(labels); y1++) { debug("Resolved - %s\n", labels[y1].key); } for (y1 = 0; y1 < arrlen(unresolved); y1++) { debug("Unresolved - %s\n", unresolved[y1]->key); } #endif for (y1 = 0; y1 < arrlen(unresolved); y1++) { // Find offset of this unresolved label. We search ourselves so it's case-insensitive. x2 = -1; for (x1 = 0; x1 < shlen(labels); x1++) { debug("Checking label %d of %d - %s == %s\n", y1, (int)arrlen(unresolved), unresolved[y1]->key, labels[x1].key); if (strcasecmp(unresolved[y1]->key, labels[x1].key) == 0) { x2 = x1; break; } } if (x2 < 0) { // Label not found! Error! result = unresolved[y1]->line; break; } else { // Write this label offset into the unresolved offset. bytecode->bytes[unresolved[y1]->value++] = (labels[x2].value & 0xFF00) >> 8; bytecode->bytes[unresolved[y1]->value] = labels[x2].value & 0x00FF; } } } // Unwind variables array if needed. ARRFREE(variables); // Unwind unresolved array if needed. if (unresolved != NULL) { while (arrlen(unresolved) > 0) { label = unresolved[0]; DEL(label->key); DEL(label); arrdel(unresolved, 0); } } // Unwind labels hashmap if needed. if (labels != NULL) { while (shlen(labels) > 0) { shdel(labels, labels[0].key); } shfree(labels); } /* * (value) is a 16-bit integer. Since we only need a fraction of the * possible values provided by this, we steal a couple bits for our * own use. All values are stored without messing with 2's complement. * * Type Negative Value * \ /__________/_ * \ // \ * tnvvvvvvvvvvvvvv * * So with this scheme we can store values from -16383 to 16383 (yes, * zero is represented twice). * * The Type bit determines if the value stored is a literal value or a * reference to a variable in the variable table. * * (short) is a simplified version used for colors. It is always positive * and has a range from 0 to 127 with Type being the MSb. This effectively * limits the number of available variables to 128. * */ return result; } #pragma clang diagnostic pop