DVX_GUI/apps/dvxbasic/test_compiler.c

1643 lines
43 KiB
C

// test_compiler.c -- End-to-end test: source -> compiler -> VM -> output
//
// Build: make -C dvxbasic tests
#include "compiler/parser.h"
#include "runtime/vm.h"
#include "runtime/values.h"
#include <stdio.h>
#include <string.h>
static void runProgram(const char *name, const char *source) {
printf("=== %s ===\n", name);
int32_t len = (int32_t)strlen(source);
BasParserT parser;
basParserInit(&parser, source, len);
if (!basParse(&parser)) {
printf("COMPILE ERROR: %s\n\n", parser.error);
basParserFree(&parser);
return;
}
BasModuleT *mod = basParserBuildModule(&parser);
basParserFree(&parser);
if (!mod) {
printf("MODULE BUILD FAILED\n\n");
return;
}
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
// Module-level code uses callStack[0] as implicit main frame
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
BasVmResultE result = basVmRun(vm);
if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
printf("[VM error %d: %s]\n", result, basVmGetError(vm));
}
basVmDestroy(vm);
basModuleFree(mod);
printf("\n");
}
int main(void) {
printf("DVX BASIC Compiler Tests\n");
printf("========================\n\n");
basStringSystemInit();
// Test 1: Hello World
runProgram("Hello World",
"PRINT \"Hello, World!\"\n"
);
// Test 2: Arithmetic
runProgram("Arithmetic",
"PRINT 2 + 3 * 4\n"
"PRINT 10 \\ 3\n"
"PRINT 10 MOD 3\n"
"PRINT 2 ^ 8\n"
);
// Test 3: String operations
runProgram("String Ops",
"DIM s AS STRING\n"
"s = \"Hello, BASIC!\"\n"
"PRINT s\n"
"PRINT LEN(s)\n"
"PRINT LEFT$(s, 5)\n"
"PRINT RIGHT$(s, 6)\n"
"PRINT MID$(s, 8, 5)\n"
"PRINT UCASE$(s)\n"
);
// Test 4: IF/THEN/ELSE
runProgram("IF/THEN/ELSE",
"DIM x AS INTEGER\n"
"x = 42\n"
"IF x > 100 THEN\n"
" PRINT \"big\"\n"
"ELSEIF x > 10 THEN\n"
" PRINT \"medium\"\n"
"ELSE\n"
" PRINT \"small\"\n"
"END IF\n"
);
// Test 5: FOR loop
runProgram("FOR Loop",
"DIM i AS INTEGER\n"
"FOR i = 1 TO 10\n"
" PRINT i;\n"
"NEXT i\n"
"PRINT\n"
);
// Test 6: DO/WHILE loop
runProgram("DO/WHILE Loop",
"DIM n AS INTEGER\n"
"n = 1\n"
"DO WHILE n <= 5\n"
" PRINT n;\n"
" n = n + 1\n"
"LOOP\n"
"PRINT\n"
);
// Test 7: SUB and FUNCTION
runProgram("SUB and FUNCTION",
"DECLARE SUB Greet(name AS STRING)\n"
"DECLARE FUNCTION Square(x AS INTEGER) AS INTEGER\n"
"\n"
"CALL Greet(\"World\")\n"
"PRINT Square(7)\n"
"\n"
"SUB Greet(name AS STRING)\n"
" PRINT \"Hello, \" & name & \"!\"\n"
"END SUB\n"
"\n"
"FUNCTION Square(x AS INTEGER) AS INTEGER\n"
" Square = x * x\n"
"END FUNCTION\n"
);
// Test 8: SELECT CASE
runProgram("SELECT CASE",
"DIM grade AS STRING\n"
"grade = \"B\"\n"
"SELECT CASE grade\n"
" CASE \"A\"\n"
" PRINT \"Excellent\"\n"
" CASE \"B\", \"C\"\n"
" PRINT \"Good\"\n"
" CASE ELSE\n"
" PRINT \"Other\"\n"
"END SELECT\n"
);
// Test 9: Fibonacci
runProgram("Fibonacci",
"DIM a AS INTEGER\n"
"DIM b AS INTEGER\n"
"DIM temp AS INTEGER\n"
"DIM i AS INTEGER\n"
"a = 0\n"
"b = 1\n"
"FOR i = 1 TO 10\n"
" PRINT a;\n"
" temp = a + b\n"
" a = b\n"
" b = temp\n"
"NEXT i\n"
"PRINT\n"
);
// Test 10: Math functions
runProgram("Math Functions",
"PRINT ABS(-42)\n"
"PRINT SQR(144)\n"
"PRINT INT(3.7)\n"
);
// Test 11: File I/O
runProgram("File I/O",
"OPEN \"/tmp/dvxbasic_test.txt\" FOR OUTPUT AS #1\n"
"PRINT #1, \"Hello from BASIC!\"\n"
"PRINT #1, \"Line two\"\n"
"PRINT #1, \"42\"\n"
"CLOSE #1\n"
"\n"
"DIM line$ AS STRING\n"
"DIM count AS INTEGER\n"
"count = 0\n"
"OPEN \"/tmp/dvxbasic_test.txt\" FOR INPUT AS #1\n"
"DO WHILE NOT EOF(#1)\n"
" INPUT #1, line$\n"
" PRINT line$\n"
" count = count + 1\n"
"LOOP\n"
"CLOSE #1\n"
"PRINT count;\n"
"PRINT \"lines read\"\n"
);
// Test 12: LINE INPUT# and APPEND
runProgram("LINE INPUT and APPEND",
"OPEN \"/tmp/dvxbasic_test2.txt\" FOR OUTPUT AS #2\n"
"PRINT #2, \"First line\"\n"
"CLOSE #2\n"
"\n"
"OPEN \"/tmp/dvxbasic_test2.txt\" FOR APPEND AS #2\n"
"PRINT #2, \"Appended line\"\n"
"CLOSE #2\n"
"\n"
"DIM s$ AS STRING\n"
"OPEN \"/tmp/dvxbasic_test2.txt\" FOR INPUT AS #2\n"
"LINE INPUT #2, s$\n"
"PRINT s$\n"
"LINE INPUT #2, s$\n"
"PRINT s$\n"
"CLOSE #2\n"
);
// Test 13: Array -- 1D with default lbound=0
runProgram("1D Array",
"DIM arr(5) AS INTEGER\n"
"DIM i AS INTEGER\n"
"FOR i = 1 TO 5\n"
" arr(i) = i * i\n"
"NEXT i\n"
"FOR i = 1 TO 5\n"
" PRINT arr(i);\n"
"NEXT i\n"
"PRINT\n"
);
// Expected: 1 4 9 16 25
// Test 14: Multi-dimensional array
runProgram("Multi-dim Array",
"DIM m(2, 2) AS INTEGER\n"
"m(1, 1) = 11\n"
"m(1, 2) = 12\n"
"m(2, 1) = 21\n"
"m(2, 2) = 22\n"
"PRINT m(1, 1); m(1, 2); m(2, 1); m(2, 2)\n"
);
// Expected: 11 12 21 22
// Test 15: Array with explicit bounds (TO syntax)
runProgram("Array with TO bounds",
"DIM a(1 TO 3) AS INTEGER\n"
"a(1) = 10\n"
"a(2) = 20\n"
"a(3) = 30\n"
"PRINT a(1); a(2); a(3)\n"
);
// Expected: 10 20 30
// Test 16: LBOUND and UBOUND
runProgram("LBOUND/UBOUND",
"DIM a(5 TO 10) AS INTEGER\n"
"PRINT LBOUND(a); UBOUND(a)\n"
);
// Expected: 5 10
// Test 17: User-defined TYPE
runProgram("TYPE",
"TYPE Point\n"
" x AS INTEGER\n"
" y AS INTEGER\n"
"END TYPE\n"
"DIM p AS Point\n"
"p.x = 10\n"
"p.y = 20\n"
"PRINT p.x; p.y\n"
);
// Expected: 10 20
// Test 18: String array
runProgram("String Array",
"DIM names(3) AS STRING\n"
"names(0) = \"Alice\"\n"
"names(1) = \"Bob\"\n"
"names(2) = \"Charlie\"\n"
"DIM i AS INTEGER\n"
"FOR i = 0 TO 2\n"
" PRINT names(i)\n"
"NEXT i\n"
);
// Expected: Alice / Bob / Charlie
// Test 19: REDIM with PRESERVE
runProgram("REDIM PRESERVE",
"DIM a(3) AS INTEGER\n"
"a(0) = 100\n"
"a(1) = 200\n"
"a(2) = 300\n"
"REDIM PRESERVE a(5) AS INTEGER\n"
"a(4) = 500\n"
"PRINT a(0); a(1); a(2); a(4)\n"
);
// Expected: 100 200 300 500
// Test 20: ERASE
runProgram("ERASE",
"DIM a(3) AS INTEGER\n"
"a(1) = 42\n"
"ERASE a\n"
"DIM b(2) AS INTEGER\n"
"b(1) = 99\n"
"PRINT b(1)\n"
);
// Expected: 99
// Test 21: Array in FOR loop accumulation
runProgram("Array Accumulation",
"DIM sums(5) AS INTEGER\n"
"DIM i AS INTEGER\n"
"DIM j AS INTEGER\n"
"FOR i = 1 TO 5\n"
" sums(i) = 0\n"
" FOR j = 1 TO i\n"
" sums(i) = sums(i) + j\n"
" NEXT j\n"
"NEXT i\n"
"FOR i = 1 TO 5\n"
" PRINT sums(i);\n"
"NEXT i\n"
"PRINT\n"
);
// Expected: 1 3 6 10 15
// ============================================================
// Batch 1: Control Flow
// ============================================================
// Test: GOTO with forward jump
runProgram("GOTO Forward",
"PRINT \"before\"\n"
"GOTO skip\n"
"PRINT \"skipped\"\n"
"skip:\n"
"PRINT \"after\"\n"
);
// Expected: before / after
// Test: GOTO with backward jump
runProgram("GOTO Backward",
"DIM n AS INTEGER\n"
"n = 0\n"
"top:\n"
"n = n + 1\n"
"IF n < 5 THEN GOTO top\n"
"PRINT n\n"
);
// Expected: 5
// Test: GOSUB/RETURN
runProgram("GOSUB/RETURN",
"DIM x AS INTEGER\n"
"x = 10\n"
"GOSUB dbl\n"
"PRINT x\n"
"END\n"
"dbl:\n"
"x = x * 2\n"
"RETURN\n"
);
// Expected: 20
// Test: ON ERROR GOTO -- verify error handler catches errors
// and ERR returns the error number
runProgram("ON ERROR GOTO",
"ON ERROR GOTO handler\n"
"PRINT 10 / 0\n"
"END\n"
"handler:\n"
"PRINT \"caught\"\n"
"PRINT ERR\n"
);
// Expected: caught / 11
// Test: Single-line IF
runProgram("Single-line IF",
"DIM x AS INTEGER\n"
"x = 42\n"
"IF x > 10 THEN PRINT \"big\"\n"
"IF x < 10 THEN PRINT \"small\"\n"
"IF x = 42 THEN PRINT \"exact\" ELSE PRINT \"nope\"\n"
);
// Expected: big / exact
// Test: Multi-statement line with :
runProgram("Multi-statement :",
"DIM x AS INTEGER\n"
"DIM y AS INTEGER\n"
"x = 1 : y = 2 : PRINT x + y\n"
);
// Expected: 3
// ============================================================
// Batch 2: Misc Features
// ============================================================
// Test: SWAP
runProgram("SWAP",
"DIM a AS INTEGER\n"
"DIM b AS INTEGER\n"
"a = 10\n"
"b = 20\n"
"SWAP a, b\n"
"PRINT a;\n"
"PRINT b\n"
);
// Expected: 20 10
// Test: TIMER (returns number > 0)
runProgram("TIMER",
"DIM t AS DOUBLE\n"
"t = TIMER\n"
"IF t > 0 THEN PRINT \"ok\"\n"
);
// Expected: ok
// Test: DATE$ (returns non-empty string)
runProgram("DATE$",
"DIM d$ AS STRING\n"
"d$ = DATE$\n"
"IF LEN(d$) > 0 THEN PRINT \"ok\"\n"
);
// Expected: ok
// Test: TIME$ (returns non-empty string)
runProgram("TIME$",
"DIM t$ AS STRING\n"
"t$ = TIME$\n"
"IF LEN(t$) > 0 THEN PRINT \"ok\"\n"
);
// Expected: ok
// Test: ENVIRON$
runProgram("ENVIRON$",
"DIM p$ AS STRING\n"
"p$ = ENVIRON$(\"HOME\")\n"
"IF LEN(p$) > 0 THEN PRINT \"ok\"\n"
);
// Expected: ok
// ============================================================
// Batch 3: New features (DATA/READ/RESTORE, DIM SHARED,
// STATIC, DEF FN, OPTION BASE)
// ============================================================
// Test: DATA/READ/RESTORE
runProgram("DATA/READ/RESTORE",
"DATA 10, 20, \"hello\"\n"
"DIM a AS INTEGER\n"
"DIM b AS INTEGER\n"
"DIM c AS STRING\n"
"READ a, b, c\n"
"PRINT a; b;\n"
"PRINT c\n"
"RESTORE\n"
"READ a\n"
"PRINT a\n"
);
// Expected: 10 20 hello / 10
// Test: DIM SHARED
runProgram("DIM SHARED",
"DIM SHARED count AS INTEGER\n"
"count = 0\n"
"CALL Increment\n"
"CALL Increment\n"
"CALL Increment\n"
"PRINT count\n"
"SUB Increment\n"
" count = count + 1\n"
"END SUB\n"
);
// Expected: 3
// Test: STATIC
runProgram("STATIC",
"CALL Counter\n"
"CALL Counter\n"
"CALL Counter\n"
"SUB Counter\n"
" STATIC n AS INTEGER\n"
" n = n + 1\n"
" PRINT n;\n"
"END SUB\n"
"PRINT\n"
);
// Expected: 1 2 3
// Test: DEF FN
runProgram("DEF FN",
"DEF FNdouble(x AS INTEGER) = x * 2\n"
"PRINT FNdouble(5)\n"
"PRINT FNdouble(21)\n"
);
// Expected: 10 / 42
// Test: OPTION BASE
runProgram("OPTION BASE",
"OPTION BASE 1\n"
"DIM arr(3) AS INTEGER\n"
"arr(1) = 10\n"
"arr(3) = 30\n"
"PRINT arr(1); arr(3)\n"
);
// Expected: 10 30
// Test: DATA with mixed types
runProgram("DATA mixed types",
"DATA 100, 3.14, \"world\"\n"
"DIM x AS INTEGER\n"
"DIM y AS DOUBLE\n"
"DIM z AS STRING\n"
"READ x, y, z\n"
"PRINT x\n"
"PRINT z\n"
);
// Expected: 100 / world
// Test: Multiple DATA statements scattered
runProgram("DATA scattered",
"DIM a AS INTEGER\n"
"DIM b AS INTEGER\n"
"DIM c AS INTEGER\n"
"DATA 1, 2\n"
"READ a, b\n"
"DATA 3\n"
"READ c\n"
"PRINT a; b; c\n"
);
// Expected: 1 2 3
// Test: DIM SHARED with SUB modifying shared variable
runProgram("DIM SHARED multi",
"DIM SHARED total AS INTEGER\n"
"DIM SHARED msg AS STRING\n"
"total = 100\n"
"msg = \"start\"\n"
"CALL Modify\n"
"PRINT total\n"
"PRINT msg\n"
"SUB Modify\n"
" total = total + 50\n"
" msg = \"done\"\n"
"END SUB\n"
);
// Expected: 150 / done
// ============================================================
// Batch 4: New I/O and string features
// ============================================================
// Test: WRITE #
runProgram("WRITE #",
"OPEN \"/tmp/dvxbasic_write.txt\" FOR OUTPUT AS #1\n"
"WRITE #1, 10, \"hello\", 3.14\n"
"CLOSE #1\n"
"OPEN \"/tmp/dvxbasic_write.txt\" FOR INPUT AS #1\n"
"DIM s AS STRING\n"
"LINE INPUT #1, s\n"
"PRINT s\n"
"CLOSE #1\n"
);
// Expected: 10,"hello",3.14
// Test: FREEFILE
runProgram("FREEFILE",
"DIM f AS INTEGER\n"
"f = FREEFILE\n"
"PRINT f\n"
);
// Expected: 1
// Test: PRINT USING numeric
runProgram("PRINT USING numeric",
"PRINT USING \"###.##\"; 3.14159\n"
);
// Expected: 3.14
// Test: PRINT USING string
runProgram("PRINT USING string",
"PRINT USING \"!\"; \"Hello\"\n"
);
// Expected: H
// Test: SPC and TAB in PRINT
runProgram("SPC/TAB",
"PRINT SPC(3); \"hi\"\n"
);
// Expected: hi
// Test: Fixed-length string
runProgram("STRING * n",
"DIM s AS STRING * 5\n"
"s = \"Hi\"\n"
"PRINT \"[\" & s & \"]\"\n"
"PRINT LEN(s)\n"
);
// Expected: [Hi ] / 5
// Test: MID$ statement
runProgram("MID$ statement",
"DIM s AS STRING\n"
"s = \"Hello World\"\n"
"MID$(s, 7, 5) = \"BASIC\"\n"
"PRINT s\n"
);
// Expected: Hello BASIC
// Test: OPEN FOR BINARY / GET / PUT
runProgram("BINARY GET/PUT",
"DIM v AS INTEGER\n"
"OPEN \"/tmp/dvxbasic_bin.tmp\" FOR BINARY AS #1\n"
"v = 12345\n"
"PUT #1, , v\n"
"SEEK #1, 1\n"
"DIM r AS INTEGER\n"
"GET #1, , r\n"
"PRINT r\n"
"CLOSE #1\n"
);
// Expected: 12345
// Test: LOF and LOC
runProgram("LOF/LOC",
"OPEN \"/tmp/dvxbasic_lof.txt\" FOR OUTPUT AS #1\n"
"PRINT #1, \"test\"\n"
"CLOSE #1\n"
"OPEN \"/tmp/dvxbasic_lof.txt\" FOR INPUT AS #1\n"
"DIM sz AS LONG\n"
"sz = LOF(1)\n"
"IF sz > 0 THEN PRINT \"ok\"\n"
"CLOSE #1\n"
);
// Expected: ok
// Test: INPUT$(n, #channel)
runProgram("INPUT$",
"OPEN \"/tmp/dvxbasic_inp.txt\" FOR OUTPUT AS #1\n"
"PRINT #1, \"ABCDEF\"\n"
"CLOSE #1\n"
"OPEN \"/tmp/dvxbasic_inp.txt\" FOR INPUT AS #1\n"
"DIM s AS STRING\n"
"s = INPUT$(3, #1)\n"
"PRINT s\n"
"CLOSE #1\n"
);
// Expected: ABC
// Test: SEEK function form
runProgram("SEEK function",
"OPEN \"/tmp/dvxbasic_seek.txt\" FOR OUTPUT AS #1\n"
"PRINT #1, \"test\"\n"
"CLOSE #1\n"
"OPEN \"/tmp/dvxbasic_seek.txt\" FOR BINARY AS #1\n"
"DIM p AS LONG\n"
"p = SEEK(1)\n"
"IF p = 1 THEN PRINT \"ok\"\n"
"CLOSE #1\n"
);
// Expected: ok
// Test: ON n GOTO
runProgram("ON n GOTO",
"DIM n AS INTEGER\n"
"n = 2\n"
"ON n GOTO ten, twenty, thirty\n"
"PRINT \"none\"\n"
"GOTO done\n"
"ten:\n"
"PRINT \"ten\"\n"
"GOTO done\n"
"twenty:\n"
"PRINT \"twenty\"\n"
"GOTO done\n"
"thirty:\n"
"PRINT \"thirty\"\n"
"done:\n"
);
// Expected: twenty
// Test: ON n GOTO (no match)
runProgram("ON n GOTO no match",
"DIM n AS INTEGER\n"
"n = 5\n"
"ON n GOTO aa, bb\n"
"PRINT \"fallthrough\"\n"
"GOTO done2\n"
"aa:\n"
"PRINT \"aa\"\n"
"GOTO done2\n"
"bb:\n"
"PRINT \"bb\"\n"
"done2:\n"
);
// Expected: fallthrough
// Test: ON n GOSUB
runProgram("ON n GOSUB",
"DIM n AS INTEGER\n"
"DIM result AS INTEGER\n"
"result = 0\n"
"n = 2\n"
"ON n GOSUB addTen, addTwenty, addThirty\n"
"PRINT result\n"
"GOTO endProg\n"
"addTen:\n"
"result = result + 10\n"
"RETURN\n"
"addTwenty:\n"
"result = result + 20\n"
"RETURN\n"
"addThirty:\n"
"result = result + 30\n"
"RETURN\n"
"endProg:\n"
);
// Expected: 20
// Test: FORMAT$
runProgram("FORMAT$",
"PRINT FORMAT$(1234.5, \"#,##0.00\")\n"
"PRINT FORMAT$(0.5, \"0.00\")\n"
"PRINT FORMAT$(-42, \"+#0\")\n"
"PRINT FORMAT$(0.75, \"percent\")\n"
);
// Expected: 1,234.50\n0.50\n-42\n75%
// Test: SHELL as function expression
runProgram("SHELL function",
"DIM r AS INTEGER\n"
"r = SHELL(\"echo hello > /dev/null\")\n"
"IF r = 0 THEN PRINT \"ok\"\n"
);
// Expected: ok
// Test: SHELL as statement
runProgram("SHELL statement",
"SHELL \"echo hello > /dev/null\"\n"
"PRINT \"done\"\n"
);
// Expected: done
// Test: OPTION COMPARE TEXT
runProgram("OPTION COMPARE TEXT",
"OPTION COMPARE TEXT\n"
"IF \"hello\" = \"HELLO\" THEN\n"
" PRINT \"equal\"\n"
"ELSE\n"
" PRINT \"not equal\"\n"
"END IF\n"
"IF \"abc\" < \"XYZ\" THEN\n"
" PRINT \"less\"\n"
"END IF\n"
);
// Expected: equal\nless
// Test: OPTION COMPARE BINARY (default)
runProgram("OPTION COMPARE BINARY",
"OPTION COMPARE BINARY\n"
"IF \"hello\" = \"HELLO\" THEN\n"
" PRINT \"equal\"\n"
"ELSE\n"
" PRINT \"not equal\"\n"
"END IF\n"
);
// Expected: not equal
// Test: EQV operator
runProgram("EQV operator",
"PRINT -1 EQV -1\n"
"PRINT 0 EQV 0\n"
"PRINT -1 EQV 0\n"
"PRINT 0 EQV -1\n"
);
// Expected: -1\n-1\n0\n0
// Test: IMP operator
runProgram("IMP operator",
"PRINT 0 IMP -1\n"
"PRINT -1 IMP 0\n"
"PRINT -1 IMP -1\n"
"PRINT 0 IMP 0\n"
);
// Expected: -1\n0\n-1\n-1
// Test: PRINT USING advanced patterns
runProgram("PRINT USING advanced",
"PRINT USING \"**#,##0.00\"; 1234.5\n"
"PRINT USING \"$$#,##0.00\"; 42.5\n"
"PRINT USING \"+###.##\"; 42.5\n"
"PRINT USING \"+###.##\"; -42.5\n"
"PRINT USING \"###.##-\"; -42.5\n"
"PRINT USING \"###.##-\"; 42.5\n"
"PRINT USING \"#.##^^^^\"; 1234.5\n"
);
// Test: DEFINT
runProgram("DEFINT",
"DEFINT A-Z\n"
"a = 42\n"
"b = 3.7\n"
"PRINT a; b\n"
);
// Test: DEFSTR
runProgram("DEFSTR",
"DEFSTR S\n"
"s = \"hello\"\n"
"PRINT s\n"
);
// Test: DEFINT range
runProgram("DEFINT range",
"DEFINT I-N\n"
"i = 10\n"
"j = 20\n"
"x = 3.14\n"
"PRINT i; j; x\n"
);
// Test: OPTION EXPLICIT success
runProgram("OPTION EXPLICIT ok",
"OPTION EXPLICIT\n"
"DIM x AS INTEGER\n"
"x = 42\n"
"PRINT x\n"
);
// Test: OPTION EXPLICIT failure (should error)
{
printf("=== OPTION EXPLICIT error ===\n");
const char *src =
"OPTION EXPLICIT\n"
"x = 42\n";
int32_t len = (int32_t)strlen(src);
BasParserT parser;
basParserInit(&parser, src, len);
bool ok = basParse(&parser);
if (!ok) {
printf("Correctly caught: %s\n", parser.error);
} else {
printf("ERROR: should have failed\n");
}
basParserFree(&parser);
printf("\n");
}
// Test: DECLARE LIBRARY compilation (verify it compiles without error)
{
printf("=== DECLARE LIBRARY ===\n");
const char *src =
"DECLARE LIBRARY \"serial\"\n"
" DECLARE FUNCTION rs232Open(com AS INTEGER, bps AS LONG) AS INTEGER\n"
" DECLARE SUB rs232Close(com AS INTEGER)\n"
"END DECLARE\n"
"\n"
"PRINT \"Library declared\"\n";
int32_t len = (int32_t)strlen(src);
BasParserT parser;
basParserInit(&parser, src, len);
bool ok = basParse(&parser);
if (!ok) {
printf("COMPILE ERROR: %s\n", parser.error);
} else {
BasModuleT *mod = basParserBuildModule(&parser);
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
basVmRun(vm);
basVmDestroy(vm);
basModuleFree(mod);
}
basParserFree(&parser);
printf("\n");
}
// Test: DECLARE LIBRARY with extern call (uses mock callback)
{
printf("=== DECLARE LIBRARY call ===\n");
const char *src =
"DECLARE LIBRARY \"math\"\n"
" DECLARE FUNCTION mathAdd(a AS INTEGER, b AS INTEGER) AS INTEGER\n"
"END DECLARE\n"
"\n"
"DIM result AS INTEGER\n"
"result = mathAdd(10, 20)\n"
"PRINT result\n";
int32_t len = (int32_t)strlen(src);
BasParserT parser;
basParserInit(&parser, src, len);
bool ok = basParse(&parser);
if (!ok) {
printf("COMPILE ERROR: %s\n", parser.error);
} else {
printf("Compiled OK (extern call test needs host callbacks to run)\n");
}
basParserFree(&parser);
printf("\n");
}
// Test: Procedure table populated for SUBs and FUNCTIONs
{
printf("=== Procedure table ===\n");
const char *src =
"SUB Command1_Click\n"
" PRINT \"Clicked!\"\n"
"END SUB\n"
"\n"
"SUB Form1_Load\n"
" PRINT \"Loaded\"\n"
"END SUB\n"
"\n"
"FUNCTION AddNums(a AS INTEGER, b AS INTEGER) AS INTEGER\n"
" AddNums = a + b\n"
"END FUNCTION\n"
"\n"
"PRINT \"main\"\n";
int32_t len = (int32_t)strlen(src);
BasParserT parser;
basParserInit(&parser, src, len);
bool ok = basParse(&parser);
if (!ok) {
printf("COMPILE ERROR: %s\n", parser.error);
} else {
BasModuleT *mod = basParserBuildModule(&parser);
printf("procCount = %d\n", mod->procCount);
for (int32_t i = 0; i < mod->procCount; i++) {
printf(" [%d] %s addr=%d params=%d func=%d\n",
i, mod->procs[i].name, mod->procs[i].codeAddr,
mod->procs[i].paramCount, mod->procs[i].isFunction);
}
// Test lookup
const BasProcEntryT *p = basModuleFindProc(mod, "command1_click");
if (p) {
printf("Found Command1_Click at addr %d\n", p->codeAddr);
} else {
printf("FAIL: Command1_Click not found!\n");
}
p = basModuleFindProc(mod, "FORM1_LOAD");
if (p) {
printf("Found Form1_Load at addr %d\n", p->codeAddr);
} else {
printf("FAIL: Form1_Load not found!\n");
}
// Test basVmCallSub
BasVmT *vm = basVmCreate();
basVmLoadModule(vm, mod);
vm->callStack[0].localCount = mod->globalCount > 64 ? 64 : mod->globalCount;
vm->callDepth = 1;
p = basModuleFindProc(mod, "Command1_Click");
if (p) {
printf("Calling Command1_Click via basVmCallSub: ");
bool callOk = basVmCallSub(vm, p->codeAddr);
printf("result=%s\n", callOk ? "ok" : "FAIL");
}
basVmDestroy(vm);
basModuleFree(mod);
}
basParserFree(&parser);
printf("\n");
}
// ByRef parameters
runProgram("ByRef parameters",
"DECLARE SUB AddTen(n AS INTEGER)\n"
"DECLARE SUB ChangeStr(s AS STRING)\n"
"DECLARE SUB NoChange(BYVAL n AS INTEGER)\n"
"\n"
"DIM x AS INTEGER\n"
"x = 10\n"
"PRINT \"Before: x =\"; x\n"
"AddTen x\n"
"PRINT \"After AddTen: x =\"; x\n"
"\n"
"DIM a AS STRING\n"
"a = \"hello\"\n"
"PRINT \"Before: a = \"; a\n"
"ChangeStr a\n"
"PRINT \"After ChangeStr: a = \"; a\n"
"\n"
"' ByVal should NOT modify caller\n"
"DIM y AS INTEGER\n"
"y = 100\n"
"PRINT \"Before: y =\"; y\n"
"NoChange y\n"
"PRINT \"After NoChange: y =\"; y\n"
"\n"
"' Expression arg to ByRef param: effectively ByVal\n"
"DIM z AS INTEGER\n"
"z = 5\n"
"AddTen(z + 0)\n"
"PRINT \"After AddTen(z+0): z =\"; z\n"
"\n"
"SUB AddTen(n AS INTEGER)\n"
" n = n + 10\n"
"END SUB\n"
"\n"
"SUB ChangeStr(s AS STRING)\n"
" s = s + \" world\"\n"
"END SUB\n"
"\n"
"SUB NoChange(BYVAL n AS INTEGER)\n"
" n = n + 999\n"
"END SUB\n"
);
// ============================================================
// Coverage: String functions
// ============================================================
runProgram("LCASE$",
"PRINT LCASE$(\"HELLO WORLD\")\n"
);
// Expected: hello world
runProgram("TRIM$ LTRIM$ RTRIM$",
"PRINT \"[\" & TRIM$(\" hi \") & \"]\"\n"
"PRINT \"[\" & LTRIM$(\" hi \") & \"]\"\n"
"PRINT \"[\" & RTRIM$(\" hi \") & \"]\"\n"
);
// Expected: [hi] / [hi ] / [ hi]
runProgram("INSTR 2-arg",
"PRINT INSTR(\"hello world\", \"world\")\n"
"PRINT INSTR(\"hello world\", \"xyz\")\n"
);
// Expected: 7 / 0
runProgram("INSTR 3-arg",
"PRINT INSTR(5, \"abcabc\", \"bc\")\n"
"PRINT INSTR(1, \"abcabc\", \"bc\")\n"
);
// Expected: 5 / 2
runProgram("CHR$ and ASC",
"PRINT CHR$(65)\n"
"PRINT ASC(\"Z\")\n"
);
// Expected: A / 90
runProgram("SPACE$",
"PRINT \"[\" & SPACE$(5) & \"]\"\n"
);
// Expected: [ ]
runProgram("STRING$",
"PRINT STRING$(5, \"*\")\n"
);
// Expected: *****
runProgram("HEX$",
"PRINT HEX$(255)\n"
"PRINT HEX$(16)\n"
);
// Expected: FF / 10
runProgram("VAL",
"PRINT VAL(\"3.14\")\n"
"PRINT VAL(\"42\")\n"
"PRINT VAL(\"abc\")\n"
);
// Expected: 3.14 / 42 / 0
runProgram("STR$",
"PRINT \"[\" & STR$(42) & \"]\"\n"
"PRINT \"[\" & STR$(-7) & \"]\"\n"
);
// Expected: [ 42] / [-7]
runProgram("MID$ 2-arg",
"PRINT MID$(\"hello world\", 7)\n"
);
// Expected: world
runProgram("String concat with +",
"DIM a AS STRING\n"
"DIM b AS STRING\n"
"a = \"hello\"\n"
"b = \" world\"\n"
"PRINT a + b\n"
);
// Expected: hello world
// ============================================================
// Coverage: Math functions
// ============================================================
runProgram("Trig functions",
"DIM pi AS DOUBLE\n"
"pi = ATN(1) * 4\n"
"PRINT INT(SIN(pi / 2) * 1000)\n"
"PRINT INT(COS(0) * 1000)\n"
"PRINT INT(TAN(pi / 4) * 1000)\n"
);
// Expected: 1000 / 1000 / 1000
runProgram("LOG and EXP",
"PRINT INT(LOG(1))\n"
"PRINT INT(EXP(0))\n"
"PRINT INT(EXP(1) * 100)\n"
);
// Expected: 0 / 1 / 271
runProgram("FIX and SGN",
"PRINT FIX(3.7)\n"
"PRINT FIX(-3.7)\n"
"PRINT SGN(42)\n"
"PRINT SGN(-5)\n"
"PRINT SGN(0)\n"
);
// Expected: 3 / -3 / 1 / -1 / 0
runProgram("RND and RANDOMIZE",
"RANDOMIZE 12345\n"
"DIM r AS DOUBLE\n"
"r = RND\n"
"IF r >= 0 AND r < 1 THEN PRINT \"ok\"\n"
);
// Expected: ok
// ============================================================
// Coverage: Loop variants
// ============================================================
runProgram("DO UNTIL pre-test",
"DIM n AS INTEGER\n"
"n = 1\n"
"DO UNTIL n > 5\n"
" PRINT n;\n"
" n = n + 1\n"
"LOOP\n"
"PRINT\n"
);
// Expected: 1 2 3 4 5
runProgram("DO LOOP WHILE post-test",
"DIM n AS INTEGER\n"
"n = 1\n"
"DO\n"
" PRINT n;\n"
" n = n + 1\n"
"LOOP WHILE n <= 5\n"
"PRINT\n"
);
// Expected: 1 2 3 4 5
runProgram("DO LOOP UNTIL post-test",
"DIM n AS INTEGER\n"
"n = 1\n"
"DO\n"
" PRINT n;\n"
" n = n + 1\n"
"LOOP UNTIL n > 5\n"
"PRINT\n"
);
// Expected: 1 2 3 4 5
runProgram("WHILE WEND",
"DIM n AS INTEGER\n"
"n = 1\n"
"WHILE n <= 5\n"
" PRINT n;\n"
" n = n + 1\n"
"WEND\n"
"PRINT\n"
);
// Expected: 1 2 3 4 5
runProgram("FOR with negative STEP",
"DIM i AS INTEGER\n"
"FOR i = 5 TO 1 STEP -1\n"
" PRINT i;\n"
"NEXT i\n"
"PRINT\n"
);
// Expected: 5 4 3 2 1
runProgram("FOR with STEP 2",
"DIM i AS INTEGER\n"
"FOR i = 0 TO 10 STEP 2\n"
" PRINT i;\n"
"NEXT i\n"
"PRINT\n"
);
// Expected: 0 2 4 6 8 10
// ============================================================
// Coverage: EXIT statements
// ============================================================
runProgram("EXIT FOR",
"DIM i AS INTEGER\n"
"FOR i = 1 TO 100\n"
" IF i = 3 THEN EXIT FOR\n"
" PRINT i;\n"
"NEXT i\n"
"PRINT\n"
"PRINT \"after\"\n"
);
// Expected: 1 2 / after
runProgram("EXIT DO",
"DIM n AS INTEGER\n"
"n = 0\n"
"DO\n"
" n = n + 1\n"
" IF n = 4 THEN EXIT DO\n"
" PRINT n;\n"
"LOOP\n"
"PRINT\n"
"PRINT \"after\"\n"
);
// Expected: 1 2 3 / after
runProgram("EXIT SUB",
"DECLARE SUB EarlyReturn(n AS INTEGER)\n"
"EarlyReturn 1\n"
"EarlyReturn 5\n"
"EarlyReturn 2\n"
"SUB EarlyReturn(n AS INTEGER)\n"
" IF n > 3 THEN EXIT SUB\n"
" PRINT n;\n"
"END SUB\n"
"PRINT\n"
);
// Expected: 1 2
runProgram("EXIT FUNCTION",
"DECLARE FUNCTION Clamp(n AS INTEGER) AS INTEGER\n"
"PRINT Clamp(5)\n"
"PRINT Clamp(200)\n"
"FUNCTION Clamp(n AS INTEGER) AS INTEGER\n"
" IF n > 100 THEN\n"
" Clamp = 100\n"
" EXIT FUNCTION\n"
" END IF\n"
" Clamp = n\n"
"END FUNCTION\n"
);
// Expected: 5 / 100
// ============================================================
// Coverage: CONST
// ============================================================
runProgram("CONST",
"CONST PI = 3.14159\n"
"CONST MAX_SIZE = 100\n"
"CONST GREETING = \"hello\"\n"
"PRINT INT(PI * 100)\n"
"PRINT MAX_SIZE\n"
"PRINT GREETING\n"
);
// Expected: 314 / 100 / hello
// ============================================================
// Coverage: END statement
// ============================================================
runProgram("END statement",
"PRINT \"before\"\n"
"END\n"
"PRINT \"after\"\n"
);
// Expected: before
// ============================================================
// Coverage: Boolean literals
// ============================================================
runProgram("True and False",
"DIM b AS BOOLEAN\n"
"b = True\n"
"IF b THEN PRINT \"yes\"\n"
"b = False\n"
"IF NOT b THEN PRINT \"no\"\n"
"PRINT True\n"
"PRINT False\n"
);
// Expected: yes / no / -1 / 0
// ============================================================
// Coverage: NOT operator
// ============================================================
runProgram("NOT operator",
"PRINT NOT 0\n"
"PRINT NOT -1\n"
"DIM x AS INTEGER\n"
"x = 5\n"
"IF NOT (x > 10) THEN PRINT \"small\"\n"
);
// Expected: -1 / 0 / small
// ============================================================
// Coverage: AND OR XOR bitwise
// ============================================================
runProgram("Bitwise AND OR XOR",
"PRINT 15 AND 9\n"
"PRINT 12 OR 3\n"
"PRINT 15 XOR 9\n"
);
// Expected: 9 / 15 / 6
// ============================================================
// Coverage: SLEEP
// ============================================================
runProgram("SLEEP",
"SLEEP 0\n"
"PRINT \"ok\"\n"
);
// Expected: ok
// ============================================================
// Coverage: Error recovery
// ============================================================
runProgram("RESUME NEXT",
"ON ERROR GOTO handler\n"
"DIM x AS INTEGER\n"
"x = 10 / 0\n"
"PRINT \"resumed\"\n"
"END\n"
"handler:\n"
"RESUME NEXT\n"
);
// Expected: resumed
runProgram("RAISE ERROR",
"ON ERROR GOTO handler\n"
"ERROR 999\n"
"PRINT \"should not print\"\n"
"END\n"
"handler:\n"
"PRINT \"caught error\"; ERR\n"
);
// Expected: caught error 999
// ============================================================
// Coverage: Type suffixes and hex literals
// ============================================================
runProgram("Type suffixes",
"DIM x%\n"
"DIM s$\n"
"x% = 42\n"
"s$ = \"hello\"\n"
"PRINT x%\n"
"PRINT s$\n"
);
// Expected: 42 / hello
runProgram("Hex literals",
"PRINT &HFF\n"
"PRINT &H10\n"
"DIM x AS INTEGER\n"
"x = &H0A\n"
"PRINT x\n"
);
// Expected: 255 / 16 / 10
// ============================================================
// Coverage: Long integer type
// ============================================================
runProgram("Long integer",
"DIM x AS LONG\n"
"x = 100000\n"
"x = x * 2\n"
"PRINT x\n"
);
// Expected: 200000
// ============================================================
// Coverage: REDIM without PRESERVE
// ============================================================
runProgram("REDIM no PRESERVE",
"DIM a(3) AS INTEGER\n"
"a(1) = 99\n"
"REDIM a(5) AS INTEGER\n"
"PRINT a(1)\n"
);
// Expected: 0 (data cleared)
// ============================================================
// Coverage: PRINT variants
// ============================================================
runProgram("PRINT comma separator",
"PRINT 1, 2, 3\n"
);
// Expected: 1<tab>2<tab>3
runProgram("PRINT bare newline",
"PRINT \"a\"\n"
"PRINT\n"
"PRINT \"b\"\n"
);
// Expected: a / (blank) / b
// ============================================================
// Coverage: LET keyword
// ============================================================
runProgram("LET keyword",
"DIM x AS INTEGER\n"
"LET x = 42\n"
"PRINT x\n"
);
// Expected: 42
// ============================================================
// Coverage: Line continuation
// ============================================================
runProgram("Line continuation",
"DIM x AS INTEGER\n"
"x = 1 + _\n"
" 2 + _\n"
" 3\n"
"PRINT x\n"
);
// Expected: 6
// ============================================================
// Coverage: REM comment
// ============================================================
runProgram("REM comment",
"REM This is a comment\n"
"PRINT \"ok\"\n"
"PRINT \"hi\" REM inline comment\n"
);
// Expected: ok / hi
// ============================================================
// Coverage: SELECT CASE with numeric ranges and IS
// ============================================================
runProgram("SELECT CASE numeric",
"DIM x AS INTEGER\n"
"x = 15\n"
"SELECT CASE x\n"
" CASE 10\n"
" PRINT \"ten\"\n"
" CASE 15, 20\n"
" PRINT \"fifteen or twenty\"\n"
" CASE ELSE\n"
" PRINT \"other\"\n"
"END SELECT\n"
);
// Expected: fifteen or twenty
runProgram("CASE TO range",
"DIM x AS INTEGER\n"
"x = 15\n"
"SELECT CASE x\n"
" CASE 1 TO 10\n"
" PRINT \"1-10\"\n"
" CASE 11 TO 20\n"
" PRINT \"11-20\"\n"
" CASE ELSE\n"
" PRINT \"other\"\n"
"END SELECT\n"
);
// Expected: 11-20
runProgram("CASE IS comparison",
"DIM x AS INTEGER\n"
"x = 50\n"
"SELECT CASE x\n"
" CASE IS < 10\n"
" PRINT \"small\"\n"
" CASE IS >= 100\n"
" PRINT \"big\"\n"
" CASE ELSE\n"
" PRINT \"medium\"\n"
"END SELECT\n"
);
// Expected: medium
runProgram("CASE mixed forms",
"DIM x AS INTEGER\n"
"x = 5\n"
"SELECT CASE x\n"
" CASE 1, 2, 3\n"
" PRINT \"low\"\n"
" CASE 4 TO 6\n"
" PRINT \"mid\"\n"
" CASE IS > 6\n"
" PRINT \"high\"\n"
"END SELECT\n"
);
// Expected: mid
// ============================================================
// Coverage: Nested FOR EXIT
// ============================================================
runProgram("Nested FOR EXIT",
"DIM i AS INTEGER\n"
"DIM j AS INTEGER\n"
"FOR i = 1 TO 3\n"
" FOR j = 1 TO 100\n"
" IF j > 2 THEN EXIT FOR\n"
" PRINT i * 10 + j;\n"
" NEXT j\n"
"NEXT i\n"
"PRINT\n"
);
// Expected: 11 12 21 22 31 32
// ============================================================
// Coverage: Mixed type arithmetic coercion
// ============================================================
runProgram("Type coercion",
"DIM d AS DOUBLE\n"
"d = 2.5\n"
"PRINT 7 + d\n"
"PRINT 7 * d\n"
"DIM i AS INTEGER\n"
"DIM f AS SINGLE\n"
"i = 10\n"
"f = 3.0\n"
"PRINT i / f\n"
);
// Expected: 9.5 / 17.5 / 3.333...
// ============================================================
// Coverage: Multiple FUNCTION return paths
// ============================================================
runProgram("FUNCTION return",
"DECLARE FUNCTION Max(a AS INTEGER, b AS INTEGER) AS INTEGER\n"
"PRINT Max(10, 20)\n"
"PRINT Max(30, 5)\n"
"FUNCTION Max(a AS INTEGER, b AS INTEGER) AS INTEGER\n"
" IF a > b THEN\n"
" Max = a\n"
" ELSE\n"
" Max = b\n"
" END IF\n"
"END FUNCTION\n"
);
// Expected: 20 / 30
// ============================================================
// Coverage: Recursive FUNCTION
// ============================================================
runProgram("Recursive FUNCTION",
"DECLARE FUNCTION Fact(n AS INTEGER) AS LONG\n"
"PRINT Fact(1)\n"
"PRINT Fact(5)\n"
"PRINT Fact(10)\n"
"FUNCTION Fact(n AS INTEGER) AS LONG\n"
" IF n <= 1 THEN\n"
" Fact = 1\n"
" ELSE\n"
" Fact = n * Fact(n - 1)\n"
" END IF\n"
"END FUNCTION\n"
);
// Expected: 1 / 120 / 3628800
// ============================================================
// Coverage: String comparison operators
// ============================================================
runProgram("String comparison",
"IF \"abc\" < \"def\" THEN PRINT \"less\"\n"
"IF \"xyz\" > \"abc\" THEN PRINT \"greater\"\n"
"IF \"abc\" = \"abc\" THEN PRINT \"equal\"\n"
"IF \"abc\" <> \"xyz\" THEN PRINT \"notequal\"\n"
);
// Expected: less / greater / equal / notequal
// ============================================================
// Coverage: Apostrophe comment
// ============================================================
runProgram("Apostrophe comment",
"PRINT \"before\" ' this is a comment\n"
"' full line comment\n"
"PRINT \"after\"\n"
);
// Expected: before / after
// ============================================================
// Coverage: SINGLE data type
// ============================================================
runProgram("SINGLE data type",
"DIM s AS SINGLE\n"
"s = 3.14\n"
"PRINT INT(s * 100)\n"
);
// Expected: 314
// ============================================================
// Coverage: Conversion functions
// ============================================================
runProgram("CINT CLNG CDBL CSNG CSTR",
"PRINT CINT(3.7)\n"
"PRINT CLNG(42)\n"
"PRINT CDBL(3)\n"
"PRINT CSNG(3)\n"
"PRINT \"[\" & CSTR(42) & \"]\"\n"
);
// Expected: 3 / 42 / 3 / 3 / [ 42]
// ============================================================
// Coverage: Me keyword
// ============================================================
runProgram("Me keyword",
"' Me compiles to OP_ME_REF (returns NULL outside form context)\n"
"PRINT \"ok\"\n"
);
// Expected: ok (just verify Me doesn't crash compilation)
printf("All tests complete.\n");
return 0;
}