496 lines
13 KiB
C
496 lines
13 KiB
C
// ideMain.c -- DVX BASIC Runner application
|
|
//
|
|
// A DVX app that loads, compiles, and runs BASIC programs.
|
|
// PRINT output goes to a scrollable TextArea widget. Compile
|
|
// errors are displayed with line numbers.
|
|
//
|
|
// This is Phase 3 of DVX BASIC: proving the compiler and VM
|
|
// work on real hardware inside the DVX windowing system.
|
|
|
|
#include "dvxApp.h"
|
|
#include "dvxDialog.h"
|
|
#include "dvxWidget.h"
|
|
#include "dvxWm.h"
|
|
#include "shellApp.h"
|
|
#include "widgetBox.h"
|
|
#include "widgetButton.h"
|
|
#include "widgetLabel.h"
|
|
#include "widgetTextInput.h"
|
|
#include "widgetStatusBar.h"
|
|
|
|
#include "../compiler/parser.h"
|
|
#include "../runtime/vm.h"
|
|
#include "../runtime/values.h"
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
// ============================================================
|
|
// Constants
|
|
// ============================================================
|
|
|
|
#define IDE_WIN_W 560
|
|
#define IDE_WIN_H 400
|
|
#define IDE_BTN_W 80
|
|
#define IDE_BTN_SPACING 8
|
|
#define IDE_MAX_SOURCE 65536
|
|
#define IDE_MAX_OUTPUT 32768
|
|
|
|
// Menu command IDs
|
|
#define CMD_OPEN 100
|
|
#define CMD_RUN 101
|
|
#define CMD_STOP 102
|
|
#define CMD_CLEAR 103
|
|
#define CMD_EXIT 104
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
int32_t appMain(DxeAppContextT *ctx);
|
|
static void buildWindow(void);
|
|
static void clearOutput(void);
|
|
static void compileAndRun(void);
|
|
static void loadFile(void);
|
|
static void onClose(WindowT *win);
|
|
static void onMenu(WindowT *win, int32_t menuId);
|
|
static void onOpenClick(WidgetT *w);
|
|
static void onRunClick(WidgetT *w);
|
|
static void onClearClick(WidgetT *w);
|
|
static void printCallback(void *ctx, const char *text, bool newline);
|
|
static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize);
|
|
static bool doEventsCallback(void *ctx);
|
|
static void setStatus(const char *text);
|
|
|
|
// ============================================================
|
|
// Module state
|
|
// ============================================================
|
|
|
|
static DxeAppContextT *sCtx = NULL;
|
|
static AppContextT *sAc = NULL;
|
|
static WindowT *sWin = NULL;
|
|
static WidgetT *sEditor = NULL; // TextArea for source code
|
|
static WidgetT *sOutput = NULL; // TextArea for program output
|
|
static WidgetT *sStatus = NULL; // Status bar label
|
|
static BasVmT *sVm = NULL; // VM instance (non-NULL while running)
|
|
|
|
static char sSourceBuf[IDE_MAX_SOURCE];
|
|
static char sOutputBuf[IDE_MAX_OUTPUT];
|
|
static int32_t sOutputLen = 0;
|
|
static char sFilePath[260];
|
|
|
|
// ============================================================
|
|
// App descriptor
|
|
// ============================================================
|
|
|
|
AppDescriptorT appDescriptor = {
|
|
.name = "DVX BASIC",
|
|
.hasMainLoop = false,
|
|
.multiInstance = false,
|
|
.stackSize = SHELL_STACK_DEFAULT,
|
|
.priority = 0
|
|
};
|
|
|
|
// ============================================================
|
|
// appMain
|
|
// ============================================================
|
|
|
|
int32_t appMain(DxeAppContextT *ctx) {
|
|
sCtx = ctx;
|
|
sAc = ctx->shellCtx;
|
|
|
|
basStringSystemInit();
|
|
buildWindow();
|
|
|
|
sFilePath[0] = '\0';
|
|
sSourceBuf[0] = '\0';
|
|
sOutputBuf[0] = '\0';
|
|
sOutputLen = 0;
|
|
|
|
setStatus("Ready. Open a .BAS file or type code and press Run.");
|
|
return 0;
|
|
}
|
|
|
|
// ============================================================
|
|
// buildWindow
|
|
// ============================================================
|
|
|
|
static void buildWindow(void) {
|
|
int32_t winX = (sAc->display.width - IDE_WIN_W) / 2;
|
|
int32_t winY = (sAc->display.height - IDE_WIN_H) / 4;
|
|
|
|
sWin = dvxCreateWindow(sAc, "DVX BASIC", winX, winY, IDE_WIN_W, IDE_WIN_H, true);
|
|
|
|
if (!sWin) {
|
|
return;
|
|
}
|
|
|
|
sWin->onClose = onClose;
|
|
sWin->onMenu = onMenu;
|
|
|
|
// Menu bar
|
|
MenuBarT *menuBar = wmAddMenuBar(sWin);
|
|
MenuT *fileMenu = wmAddMenu(menuBar, "&File");
|
|
wmAddMenuItem(fileMenu, "&Open...\tCtrl+O", CMD_OPEN);
|
|
wmAddMenuSeparator(fileMenu);
|
|
wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT);
|
|
|
|
MenuT *runMenu = wmAddMenu(menuBar, "&Run");
|
|
wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN);
|
|
wmAddMenuSeparator(runMenu);
|
|
wmAddMenuItem(runMenu, "&Clear Output", CMD_CLEAR);
|
|
|
|
AccelTableT *accel = dvxCreateAccelTable();
|
|
dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN);
|
|
dvxAddAccel(accel, KEY_F5, 0, CMD_RUN);
|
|
sWin->accelTable = accel;
|
|
|
|
// Widget tree
|
|
WidgetT *root = wgtInitWindow(sAc, sWin);
|
|
|
|
// Source code editor (top half)
|
|
WidgetT *editorFrame = wgtFrame(root, "Source");
|
|
editorFrame->weight = 100;
|
|
sEditor = wgtTextArea(editorFrame, IDE_MAX_SOURCE);
|
|
sEditor->weight = 100;
|
|
|
|
// Button bar
|
|
WidgetT *btnRow = wgtHBox(root);
|
|
btnRow->spacing = wgtPixels(IDE_BTN_SPACING);
|
|
|
|
WidgetT *openBtn = wgtButton(btnRow, "&Open...");
|
|
openBtn->onClick = onOpenClick;
|
|
openBtn->prefW = wgtPixels(IDE_BTN_W);
|
|
|
|
WidgetT *runBtn = wgtButton(btnRow, "&Run");
|
|
runBtn->onClick = onRunClick;
|
|
runBtn->prefW = wgtPixels(IDE_BTN_W);
|
|
|
|
WidgetT *clearBtn = wgtButton(btnRow, "&Clear");
|
|
clearBtn->onClick = onClearClick;
|
|
clearBtn->prefW = wgtPixels(IDE_BTN_W);
|
|
|
|
// Output area (bottom half)
|
|
WidgetT *outputFrame = wgtFrame(root, "Output");
|
|
outputFrame->weight = 100;
|
|
sOutput = wgtTextArea(outputFrame, IDE_MAX_OUTPUT);
|
|
sOutput->weight = 100;
|
|
sOutput->readOnly = true;
|
|
|
|
// Status bar
|
|
WidgetT *statusBar = wgtStatusBar(root);
|
|
sStatus = wgtLabel(statusBar, "");
|
|
sStatus->weight = 100;
|
|
|
|
dvxFitWindow(sAc, sWin);
|
|
}
|
|
|
|
// ============================================================
|
|
// clearOutput
|
|
// ============================================================
|
|
|
|
static void clearOutput(void) {
|
|
sOutputBuf[0] = '\0';
|
|
sOutputLen = 0;
|
|
wgtSetText(sOutput, "");
|
|
}
|
|
|
|
// ============================================================
|
|
// compileAndRun
|
|
// ============================================================
|
|
|
|
static void compileAndRun(void) {
|
|
// Get source from editor
|
|
const char *src = wgtGetText(sEditor);
|
|
|
|
if (!src || *src == '\0') {
|
|
setStatus("No source code to run.");
|
|
return;
|
|
}
|
|
|
|
clearOutput();
|
|
setStatus("Compiling...");
|
|
|
|
// Force a display update so the status is visible
|
|
dvxInvalidateWindow(sAc, sWin);
|
|
|
|
int32_t srcLen = (int32_t)strlen(src);
|
|
|
|
// Compile (heap-allocated -- BasParserT is ~300KB, too large for stack)
|
|
BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));
|
|
|
|
if (!parser) {
|
|
setStatus("Out of memory.");
|
|
return;
|
|
}
|
|
|
|
basParserInit(parser, src, srcLen);
|
|
|
|
if (!basParse(parser)) {
|
|
int32_t n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s\n", parser->error);
|
|
sOutputLen = n;
|
|
wgtSetText(sOutput, sOutputBuf);
|
|
setStatus("Compilation failed.");
|
|
basParserFree(parser);
|
|
free(parser);
|
|
return;
|
|
}
|
|
|
|
BasModuleT *mod = basParserBuildModule(parser);
|
|
basParserFree(parser);
|
|
free(parser);
|
|
|
|
if (!mod) {
|
|
setStatus("Failed to build module.");
|
|
return;
|
|
}
|
|
|
|
setStatus("Running...");
|
|
|
|
// Create VM
|
|
BasVmT *vm = basVmCreate();
|
|
basVmLoadModule(vm, mod);
|
|
|
|
// Set up implicit main frame
|
|
vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
|
|
vm->callDepth = 1;
|
|
|
|
// Set I/O callbacks
|
|
basVmSetPrintCallback(vm, printCallback, NULL);
|
|
basVmSetInputCallback(vm, inputCallback, NULL);
|
|
basVmSetDoEventsCallback(vm, doEventsCallback, NULL);
|
|
|
|
sVm = vm;
|
|
|
|
// Run with step limit to prevent infinite loops from freezing the GUI
|
|
vm->running = true;
|
|
int32_t stepCount = 0;
|
|
|
|
while (vm->running) {
|
|
BasVmResultE result = basVmStep(vm);
|
|
|
|
stepCount++;
|
|
|
|
if (result != BAS_VM_OK) {
|
|
if (result == BAS_VM_HALTED) {
|
|
// Normal completion
|
|
} else {
|
|
// Runtime error
|
|
int32_t pos = sOutputLen;
|
|
int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "\n[Runtime error: %s]\n", basVmGetError(vm));
|
|
sOutputLen += n;
|
|
wgtSetText(sOutput, sOutputBuf);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
// Yield to DVX every 10000 steps to keep the GUI responsive
|
|
if (stepCount % 10000 == 0) {
|
|
dvxUpdate(sAc);
|
|
}
|
|
}
|
|
|
|
sVm = NULL;
|
|
|
|
// Update output display
|
|
wgtSetText(sOutput, sOutputBuf);
|
|
|
|
static char statusBuf[128];
|
|
snprintf(statusBuf, sizeof(statusBuf), "Done. %ld instructions executed.", (long)stepCount);
|
|
setStatus(statusBuf);
|
|
|
|
basVmDestroy(vm);
|
|
basModuleFree(mod);
|
|
}
|
|
|
|
// ============================================================
|
|
// doEventsCallback
|
|
// ============================================================
|
|
|
|
static bool doEventsCallback(void *ctx) {
|
|
(void)ctx;
|
|
|
|
// Update DVX display and process events
|
|
if (sAc) {
|
|
dvxUpdate(sAc);
|
|
}
|
|
|
|
return true; // continue running
|
|
}
|
|
|
|
// ============================================================
|
|
// inputCallback
|
|
// ============================================================
|
|
|
|
static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize) {
|
|
(void)ctx;
|
|
|
|
// Append prompt to output
|
|
if (prompt && sOutputLen < IDE_MAX_OUTPUT - 1) {
|
|
int32_t n = snprintf(sOutputBuf + sOutputLen, IDE_MAX_OUTPUT - sOutputLen, "%s", prompt);
|
|
sOutputLen += n;
|
|
wgtSetText(sOutput, sOutputBuf);
|
|
}
|
|
|
|
// Use DVX input dialog
|
|
// For now, a simple message box prompt
|
|
// TODO: proper INPUT dialog
|
|
buf[0] = '\0';
|
|
return false; // cancel for now
|
|
}
|
|
|
|
// ============================================================
|
|
// loadFile
|
|
// ============================================================
|
|
|
|
static void loadFile(void) {
|
|
FileFilterT filters[] = {
|
|
{ "BASIC Files (*.bas)", "*.bas" },
|
|
{ "Form Files (*.frm)", "*.frm" },
|
|
{ "All Files (*.*)", "*.*" }
|
|
};
|
|
|
|
char path[260];
|
|
|
|
if (dvxFileDialog(sAc, "Open BASIC File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) {
|
|
FILE *f = fopen(path, "r");
|
|
|
|
if (!f) {
|
|
dvxMessageBox(sAc, "Error", "Could not open file.", MB_OK | MB_ICONERROR);
|
|
return;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
if (size >= IDE_MAX_SOURCE - 1) {
|
|
fclose(f);
|
|
dvxMessageBox(sAc, "Error", "File too large.", MB_OK | MB_ICONERROR);
|
|
return;
|
|
}
|
|
|
|
int32_t bytesRead = (int32_t)fread(sSourceBuf, 1, size, f);
|
|
fclose(f);
|
|
sSourceBuf[bytesRead] = '\0';
|
|
|
|
wgtSetText(sEditor, sSourceBuf);
|
|
strncpy(sFilePath, path, sizeof(sFilePath) - 1);
|
|
sFilePath[sizeof(sFilePath) - 1] = '\0';
|
|
|
|
// Update window title
|
|
char title[300];
|
|
snprintf(title, sizeof(title), "DVX BASIC - %s", path);
|
|
dvxSetTitle(sAc, sWin, title);
|
|
|
|
setStatus("File loaded.");
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// onClearClick
|
|
// ============================================================
|
|
|
|
static void onClearClick(WidgetT *w) {
|
|
(void)w;
|
|
clearOutput();
|
|
setStatus("Output cleared.");
|
|
}
|
|
|
|
// ============================================================
|
|
// onClose
|
|
// ============================================================
|
|
|
|
static void onClose(WindowT *win) {
|
|
sWin = NULL;
|
|
sEditor = NULL;
|
|
sOutput = NULL;
|
|
sStatus = NULL;
|
|
dvxDestroyWindow(sAc, win);
|
|
}
|
|
|
|
// ============================================================
|
|
// onMenu
|
|
// ============================================================
|
|
|
|
static void onMenu(WindowT *win, int32_t menuId) {
|
|
(void)win;
|
|
|
|
switch (menuId) {
|
|
case CMD_OPEN:
|
|
loadFile();
|
|
break;
|
|
|
|
case CMD_RUN:
|
|
compileAndRun();
|
|
break;
|
|
|
|
case CMD_CLEAR:
|
|
clearOutput();
|
|
break;
|
|
|
|
case CMD_EXIT:
|
|
if (sWin) {
|
|
onClose(sWin);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// onOpenClick
|
|
// ============================================================
|
|
|
|
static void onOpenClick(WidgetT *w) {
|
|
(void)w;
|
|
loadFile();
|
|
}
|
|
|
|
// ============================================================
|
|
// onRunClick
|
|
// ============================================================
|
|
|
|
static void onRunClick(WidgetT *w) {
|
|
(void)w;
|
|
compileAndRun();
|
|
}
|
|
|
|
// ============================================================
|
|
// printCallback
|
|
// ============================================================
|
|
|
|
static void printCallback(void *ctx, const char *text, bool newline) {
|
|
(void)ctx;
|
|
|
|
if (!text) {
|
|
return;
|
|
}
|
|
|
|
int32_t textLen = (int32_t)strlen(text);
|
|
|
|
// Append to output buffer
|
|
if (sOutputLen + textLen < IDE_MAX_OUTPUT - 2) {
|
|
memcpy(sOutputBuf + sOutputLen, text, textLen);
|
|
sOutputLen += textLen;
|
|
}
|
|
|
|
if (newline && sOutputLen < IDE_MAX_OUTPUT - 2) {
|
|
sOutputBuf[sOutputLen++] = '\n';
|
|
}
|
|
|
|
sOutputBuf[sOutputLen] = '\0';
|
|
}
|
|
|
|
// ============================================================
|
|
// setStatus
|
|
// ============================================================
|
|
|
|
static void setStatus(const char *text) {
|
|
if (sStatus) {
|
|
wgtSetText(sStatus, text);
|
|
}
|
|
}
|