DVX_GUI/src/apps/kpunch/dvxbasic/stub/bascomp.c

621 lines
18 KiB
C

// 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
// 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 <name>"
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;
}