// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // bascomp.c -- DVX BASIC command-line compiler // // Compiles a .dbp project into a standalone .app file. // // Usage: BASCOMP project.dbp [-o output.app] [-release] // // The project file and all referenced source files (.bas, .frm) // are loaded relative to the directory containing the .dbp file. // The stub (basstub.app) is read from the same directory as the // compiler executable. #include "../compiler/compact.h" #include "../compiler/lexer.h" #include "../compiler/obfuscate.h" #include "../compiler/parser.h" #include "../compiler/strip.h" #include "../compiler/symtab.h" #include "../compiler/opcodes.h" #include "../runtime/vm.h" #include "../runtime/values.h" #include "../runtime/serialize.h" #include "../../../../libs/kpunch/libdvx/dvxRes.h" #include "../../../../libs/kpunch/libdvx/dvxPrefs.h" #include "../../../../libs/kpunch/libdvx/dvxTypes.h" #include "../../../../libs/kpunch/libdvx/platform/dvxPlat.h" #include "../basBuild.h" #include "../basRes.h" #include "stb_ds_wrap.h" #include #include #include #include // Function prototypes (alphabetical) static void concatGrow(char **buf, int32_t *cap, int32_t need); static const char *extractFormCode(const char *frmText); int main(int argc, char **argv); static void usage(void); static void concatGrow(char **buf, int32_t *cap, int32_t need) { while (*cap < need) { *cap *= 2; } *buf = (char *)realloc(*buf, *cap); } static const char *extractFormCode(const char *frmText) { if (!frmText) { return NULL; } const char *p = frmText; int32_t depth = 0; bool inForm = false; while (*p) { // Skip leading whitespace p = dvxSkipWs(p); if (strncasecmp(p, "Begin ", 6) == 0) { if (!inForm && strncasecmp(p + 6, "Form ", 5) == 0) { inForm = true; } depth++; } else if (strncasecmp(p, "End", 3) == 0 && (p[3] == '\0' || p[3] == '\r' || p[3] == '\n' || p[3] == ' ')) { depth--; if (depth <= 0 && inForm) { // Skip past this line while (*p && *p != '\n') { p++; } if (*p == '\n') { p++; } return p; } } // Skip to next line while (*p && *p != '\n') { p++; } if (*p == '\n') { p++; } } return NULL; } static void usage(void) { fprintf(stderr, "DVX BASIC Compiler\n\n"); fprintf(stderr, "Usage: BASCOMP project.dbp [-o output.app] [-release]\n\n"); fprintf(stderr, " project.dbp DVX BASIC project file\n"); fprintf(stderr, " -o output.app Output file (default: project name + .app)\n"); fprintf(stderr, " -release Strip debug information\n"); } int main(int argc, char **argv) { if (argc < 2) { usage(); return 1; } const char *dbpPath = NULL; const char *outputPath = NULL; bool release = false; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) { outputPath = argv[++i]; } else if (strcmp(argv[i], "-release") == 0) { release = true; } else if (argv[i][0] != '-') { dbpPath = argv[i]; } else { fprintf(stderr, "Unknown option: %s\n", argv[i]); usage(); return 1; } } if (!dbpPath) { fprintf(stderr, "Error: no project file specified.\n"); usage(); return 1; } // Load the project file PrefsHandleT *prefs = prefsLoad(dbpPath); if (!prefs) { fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath); return 1; } // Derive project directory char projectDir[DVX_MAX_PATH]; snprintf(projectDir, sizeof(projectDir), "%s", dbpPath); char *sep = platformPathDirEnd(projectDir); if (sep) { *sep = '\0'; } else { projectDir[0] = '.'; projectDir[1] = '\0'; } // Read project metadata const char *projName = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, "App"); const char *author = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, ""); const char *publisher = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, ""); const char *version = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, ""); const char *copyright = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, ""); const char *description = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, ""); const char *iconPath = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, ""); const char *helpFile = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, ""); const char *startupForm = prefsGetString(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, ""); (void)startupForm; // used implicitly by stub's basFormRtLoadAllForms // Derive output path char outBuf[DVX_MAX_PATH]; if (!outputPath) { snprintf(outBuf, sizeof(outBuf), "%s/%s.app", projectDir, projName); outputPath = outBuf; } printf("Project: %s\n", projName); printf("Output: %s (%s)\n", outputPath, release ? "release" : "debug"); // Collect source files (dynamic array; grows as needed) typedef struct { char path[DVX_MAX_PATH]; bool isForm; } SrcFileT; SrcFileT *files = NULL; // Modules for (int32_t i = 0; ; i++) { char key[16]; snprintf(key, sizeof(key), "File%d", (int)i); const char *val = prefsGetString(prefs, BAS_INI_SECTION_MODULES, key, NULL); if (!val) { break; } SrcFileT entry; snprintf(entry.path, DVX_MAX_PATH, "%s/%s", projectDir, val); entry.isForm = false; arrput(files, entry); } // Forms for (int32_t i = 0; ; i++) { char key[16]; snprintf(key, sizeof(key), "File%d", (int)i); const char *val = prefsGetString(prefs, BAS_INI_SECTION_FORMS, key, NULL); if (!val) { break; } SrcFileT entry; snprintf(entry.path, DVX_MAX_PATH, "%s/%s", projectDir, val); entry.isForm = true; arrput(files, entry); } int32_t fileCount = (int32_t)arrlen(files); if (fileCount == 0) { fprintf(stderr, "Error: project has no source files.\n"); prefsClose(prefs); return 1; } // Concatenate sources (modules first, then form code) int32_t concatCap = 8192; char *concatBuf = (char *)malloc(concatCap); if (!concatBuf) { fprintf(stderr, "Error: out of memory.\n"); prefsClose(prefs); return 1; } int32_t pos = 0; // Pass 0: .bas modules, Pass 1: .frm code sections for (int32_t pass = 0; pass < 2; pass++) { for (int32_t i = 0; i < fileCount; i++) { if (pass == 0 && files[i].isForm) { continue; } if (pass == 1 && !files[i].isForm) { continue; } int32_t srcLen = 0; char *srcBuf = platformReadFile(files[i].path, &srcLen); if (!srcBuf) { fprintf(stderr, "Error: cannot read %s\n", files[i].path); free(concatBuf); prefsClose(prefs); return 1; } const char *code = srcBuf; if (files[i].isForm) { // Extract form name from "Begin Form " char formName[BAS_MAX_SYMBOL_NAME] = ""; const char *bp = srcBuf; while (*bp) { bp = dvxSkipWs(bp); if (strncasecmp(bp, "Begin Form ", 11) == 0) { bp = dvxSkipWs(bp + 11); int32_t ni = 0; while (*bp && *bp != ' ' && *bp != '\t' && *bp != '\r' && *bp != '\n' && ni < BAS_MAX_SYMBOL_NAME - 1) { formName[ni++] = *bp++; } formName[ni] = '\0'; break; } while (*bp && *bp != '\n') { bp++; } if (*bp == '\n') { bp++; } } code = extractFormCode(srcBuf); if (!code) { code = ""; } int32_t codeLen = (int32_t)strlen(code); concatGrow(&concatBuf, &concatCap, pos + codeLen + 128); // Inject BEGINFORM directive before form code if (formName[0]) { pos += snprintf(concatBuf + pos, concatCap - pos, "BEGINFORM \"%s\"\n", formName); } memcpy(concatBuf + pos, code, codeLen); pos += codeLen; if (pos > 0 && concatBuf[pos - 1] != '\n') { concatBuf[pos++] = '\n'; } // Inject ENDFORM directive after form code if (formName[0]) { pos += snprintf(concatBuf + pos, concatCap - pos, "ENDFORM\n"); } } else { int32_t codeLen = (int32_t)strlen(code); concatGrow(&concatBuf, &concatCap, pos + codeLen + 2); memcpy(concatBuf + pos, code, codeLen); pos += codeLen; if (pos > 0 && concatBuf[pos - 1] != '\n') { concatBuf[pos++] = '\n'; } } free(srcBuf); } } concatBuf[pos] = '\0'; // Compile printf("Compiling %d file(s)...\n", (int)fileCount); BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT)); if (!parser) { fprintf(stderr, "Error: out of memory.\n"); free(concatBuf); prefsClose(prefs); return 1; } basParserInit(parser, concatBuf, pos); if (!basParse(parser)) { fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error); basParserFree(parser); free(parser); free(concatBuf); prefsClose(prefs); return 1; } free(concatBuf); BasModuleT *mod = basParserBuildModule(parser); basParserFree(parser); free(parser); if (!mod) { fprintf(stderr, "Error: failed to build module.\n"); prefsClose(prefs); return 1; } printf(" code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount); // Strip for release if (release) { basStripModule(mod); printf(" stripped debug info\n"); } // Read all .frm texts up front; they're used for obfuscation and // then embedded as FORM0, FORM1, ... resources below. Parallel // dynamic arrays, one entry per form file. char **frmData = NULL; // stb_ds: strdup'd stripped form text int32_t *frmLens = NULL; // stb_ds: length of stripped form text for (int32_t i = 0; i < fileCount; i++) { if (!files[i].isForm) { continue; } int32_t flen = 0; char *fdata = platformReadFile(files[i].path, &flen); if (!fdata) { continue; } // Strip comments from the .frm text unconditionally. Comments // are source-only; they shouldn't ship in the embedded resource // for either debug or release builds. int32_t stripCap = flen + 16; uint8_t *stripped = (uint8_t *)malloc(stripCap); if (!stripped) { free(fdata); continue; } int32_t strippedLen = basStripFrmComments(fdata, flen, stripped, stripCap); free(fdata); arrput(frmData, (char *)stripped); arrput(frmLens, strippedLen); } int32_t frmCount = (int32_t)arrlen(frmData); // Obfuscate form/control names in release mode BasObfFrmT *obfFrms = NULL; for (int32_t i = 0; i < frmCount; i++) { BasObfFrmT empty = { NULL, 0 }; arrput(obfFrms, empty); } if (release && frmCount > 0) { const char **frmTexts = NULL; for (int32_t i = 0; i < frmCount; i++) { arrput(frmTexts, frmData[i]); } basObfuscateNames(mod, frmTexts, frmLens, frmCount, obfFrms); arrfree(frmTexts); printf(" obfuscated %d form(s)\n", (int)frmCount); } // Remove OP_LINE instructions and compact the bytecode. if (release) { int32_t removed = basCompactBytecode(mod); if (removed > 0) { printf(" compacted bytecode (-%d bytes)\n", (int)removed); } } // Serialize module int32_t modLen = 0; uint8_t *modData = basModuleSerialize(mod, &modLen); if (!modData) { fprintf(stderr, "Error: failed to serialize module.\n"); basModuleFree(mod); prefsClose(prefs); return 1; } // Serialize debug info int32_t dbgLen = 0; uint8_t *dbgData = NULL; if (!release) { dbgData = basDebugSerialize(mod, &dbgLen); } basModuleFree(mod); // Read the stub DXE embedded in our own executable as a resource. // The bascomp Makefile appends basstub.app to bascomp post-link // via `dvxres add ... STUB binary @basstub.app`, so BASSTUB.APP no // longer has to sit alongside the compiler. DvxResHandleT *selfRes = dvxResOpen(argv[0]); if (!selfRes) { fprintf(stderr, "Error: cannot open %s to read embedded stub.\n", argv[0]); free(modData); free(dbgData); prefsClose(prefs); return 1; } uint32_t stubLen = 0; void *stubData = dvxResRead(selfRes, BAS_RES_STUB, &stubLen); dvxResClose(selfRes); if (!stubData) { fprintf(stderr, "Error: STUB resource not found in %s.\n", argv[0]); free(modData); free(dbgData); prefsClose(prefs); return 1; } // Write stub to output file FILE *out = fopen(outputPath, "wb"); if (!out) { fprintf(stderr, "Error: cannot create %s\n", outputPath); free(stubData); free(modData); free(dbgData); prefsClose(prefs); return 1; } fwrite(stubData, 1, stubLen, out); fclose(out); free(stubData); // Pick the right form bytes per build mode: release builds use the // obfuscated variant when available, debug builds always use the raw // stripped text. const uint8_t **emitFormData = NULL; int32_t *emitFormLens = NULL; for (int32_t fi = 0; fi < frmCount; fi++) { if (release && obfFrms[fi].data) { arrput(emitFormData, (const uint8_t *)obfFrms[fi].data); arrput(emitFormLens, obfFrms[fi].len); } else { arrput(emitFormData, (const uint8_t *)frmData[fi]); arrput(emitFormLens, frmLens[fi]); } } // Resolve icon disk path (if any) against the project directory. char iconFullPath[DVX_MAX_PATH]; const char *iconDiskPath = NULL; if (iconPath[0]) { snprintf(iconFullPath, sizeof(iconFullPath), "%s/%s", projectDir, iconPath); iconDiskPath = iconFullPath; } // Emit all resources via the shared helper. BasBuildSpecT spec; memset(&spec, 0, sizeof(spec)); spec.projName = projName; spec.author = author; spec.publisher = publisher; spec.version = version; spec.copyright = copyright; spec.description = description; spec.helpFile = helpFile; spec.iconPath = iconDiskPath; spec.moduleData = modData; spec.moduleLen = modLen; spec.debugData = dbgData; spec.debugLen = dbgLen; spec.formCount = frmCount; spec.formData = emitFormData; spec.formLens = emitFormLens; basBuildEmitResources(outputPath, &spec); free(modData); free(dbgData); // Copy help file to output directory (the HELPFILE resource itself was // written by basBuildEmitResources). if (helpFile[0]) { const char *helpBase = platformPathBaseName(helpFile); char helpSrc[DVX_MAX_PATH]; snprintf(helpSrc, sizeof(helpSrc), "%s/%s", projectDir, helpFile); char outDir[DVX_MAX_PATH]; snprintf(outDir, sizeof(outDir), "%s", outputPath); char *outSep = platformPathDirEnd(outDir); if (outSep) { *outSep = '\0'; } else { outDir[0] = '.'; outDir[1] = '\0'; } char helpDst[DVX_MAX_PATH]; snprintf(helpDst, sizeof(helpDst), "%s/%s", outDir, helpBase); int32_t hLen = 0; char *hData = platformReadFile(helpSrc, &hLen); if (hData) { FILE *hf = fopen(helpDst, "wb"); if (hf) { fwrite(hData, 1, hLen, hf); fclose(hf); } free(hData); } } // Free .frm buffers for (int32_t i = 0; i < frmCount; i++) { free(frmData[i]); free(obfFrms[i].data); } arrfree(files); arrfree(frmData); arrfree(frmLens); arrfree(obfFrms); arrfree(emitFormData); arrfree(emitFormLens); prefsClose(prefs); printf("Created %s (%d bytes)\n", outputPath, (int)stubLen + (int)modLen); return 0; }