// 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.

// 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 <stdbool.h>
#include <stdio.h>
#include <string.h>

// Count of failed checks.  main() returns non-zero when this is > 0 so
// `make tests` actually gates on these assertions instead of the harness
// always exiting 0 while printing FAIL.
static int32_t gFailCount = 0;

// PRINT output captured from the VM for comparison against the expected
// text of a test program.
#define CAPTURE_MAX 8192

typedef struct {
    char    buf[CAPTURE_MAX];
    int32_t len;
} CaptureT;

// Function prototypes
static void captureAppend(CaptureT *c, const char *s);
static void capturePrint(void *ctx, const char *text, bool newline);
static void expectCompileError(const char *name, const char *source, const char *needle);
static void runProgram(const char *name, const char *source, const char *expected);


static void captureAppend(CaptureT *c, const char *s) {
    int32_t n = (int32_t)strlen(s);

    if (c->len + n >= CAPTURE_MAX - 1) {
        n = CAPTURE_MAX - 1 - c->len;
    }

    if (n > 0) {
        memcpy(c->buf + c->len, s, n);
        c->len += n;
        c->buf[c->len] = '\0';
    }
}


static void capturePrint(void *ctx, const char *text, bool newline) {
    CaptureT *c = (CaptureT *)ctx;

    if (text) {
        captureAppend(c, text);
    }

    if (newline) {
        captureAppend(c, "\n");
    }
}


// Compile source expecting it to be rejected; the error text must
// contain needle.
static void expectCompileError(const char *name, const char *source, const char *needle) {
    printf("=== %s ===\n", name);

    BasParserT parser;
    basParserInit(&parser, source, (int32_t)strlen(source));

    bool ok = basParse(&parser);

    if (ok) {
        printf("FAIL: compiled but expected error containing [%s]\n\n", needle);
        gFailCount++;
    } else if (strstr(parser.error, needle) == NULL) {
        printf("FAIL: error [%s] does not contain [%s]\n\n", parser.error, needle);
        gFailCount++;
    } else {
        printf("OK (expected error: %s)\n\n", parser.error);
    }

    basParserFree(&parser);
}


// Compile and run source.  PRINT output is captured and, when expected
// is not NULL, must match it exactly; a compile error or a VM error is
// always a failure.
static void runProgram(const char *name, const char *source, const char *expected) {
    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);
        gFailCount++;
        basParserFree(&parser);
        return;
    }

    BasModuleT *mod = basParserBuildModule(&parser);
    basParserFree(&parser);

    if (!mod) {
        printf("MODULE BUILD FAILED\n\n");
        gFailCount++;
        return;
    }

    CaptureT cap;
    cap.buf[0] = '\0';
    cap.len    = 0;

    BasVmT *vm = basVmCreate();
    basVmLoadModule(vm, mod);
    basVmSetPrintCallback(vm, capturePrint, &cap);

    BasVmResultE result = basVmRun(vm);

    fputs(cap.buf, stdout);

    if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
        printf("[VM error %d: %s]\n", result, basVmGetError(vm));
        gFailCount++;
    } else if (expected != NULL && strcmp(cap.buf, expected) != 0) {
        printf("FAIL: expected output [%s]\n", expected);
        gFailCount++;
    }

    basVmDestroy(vm);
    basModuleFree(mod);
    printf("\n");
}


int main(void) {
    printf("DVX BASIC Compiler Tests\n");
    printf("========================\n\n");

    // Test 1: Hello World
    runProgram("Hello World",
        "PRINT \"Hello, World!\"\n",
        "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",
        "14 \n3 \n1 \n256 \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",
        "Hello, BASIC!\n13 \nHello\nBASIC!\nBASIC\nHELLO, BASIC!\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",
        "medium\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",
        "1 2 3 4 5 6 7 8 9 10 \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",
        "1 2 3 4 5 \n"
    );

    // Test 7: SUB and FUNCTION
    runProgram("SUB and FUNCTION",
        "DECLARE SUB Greet(who AS STRING)\n"
        "DECLARE FUNCTION Square(x AS INTEGER) AS INTEGER\n"
        "\n"
        "CALL Greet(\"World\")\n"
        "PRINT Square(7)\n"
        "\n"
        "SUB Greet(who AS STRING)\n"
        "    PRINT \"Hello, \" & who & \"!\"\n"
        "END SUB\n"
        "\n"
        "FUNCTION Square(x AS INTEGER) AS INTEGER\n"
        "    Square = x * x\n"
        "END FUNCTION\n",
        "Hello, World!\n49 \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",
        "Good\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",
        "0 1 1 2 3 5 8 13 21 34 \n"
    );

    // Test 10: Math functions
    runProgram("Math Functions",
        "PRINT ABS(-42)\n"
        "PRINT SQR(144)\n"
        "PRINT INT(3.7)\n",
        "42 \n12 \n3 \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",
        "Hello from BASIC!\nLine two\n42\n3 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",
        "First line\nAppended line\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",
        "1 4 9 16 25 \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",
        "11 12 21 22 \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",
        "10 20 30 \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",
        "5 10 \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",
        "10 20 \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",
        "Alice\nBob\nCharlie\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",
        "100 200 300 500 \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",
        "99 \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",
        "1 3 6 10 15 \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",
        "before\nafter\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",
        "5 \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",
        "20 \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",
        "caught\n11 \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",
        "big\nexact\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",
        "3 \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",
        "20 10 \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",
        "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",
        "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",
        "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",
        "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",
        "10 20 hello\n10 \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",
        "3 \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",
        "1 2 3 \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",
        "10 \n42 \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",
        "10 30 \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",
        "100 \nworld\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",
        "1 2 3 \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",
        "150 \ndone\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",
        "10,\"hello\",3.14\n"
    );
    // Expected: 10,"hello",3.14

    // Test: FREEFILE
    runProgram("FREEFILE",
        "DIM f AS INTEGER\n"
        "f = FREEFILE\n"
        "PRINT f\n",
        "1 \n"
    );
    // Expected: 1

    // Test: PRINT USING numeric
    runProgram("PRINT USING numeric",
        "PRINT USING \"###.##\"; 3.14159\n",
        "  3.14\n"
    );
    // Expected:   3.14

    // Test: PRINT USING string
    runProgram("PRINT USING string",
        "PRINT USING \"!\"; \"Hello\"\n",
        "H\n"
    );
    // Expected: H

    // Test: SPC and TAB in PRINT
    runProgram("SPC/TAB",
        "PRINT SPC(3); \"hi\"\n",
        "   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",
        "[Hi   ]\n5 \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",
        "Hello BASIC\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",
        "12345 \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",
        "ok\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",
        "ABC\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",
        "ok\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",
        "twenty\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",
        "fallthrough\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",
        "20 \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",
        "1,234.50\n0.50\n-42\n75%\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",
        "ok\n"
    );
    // Expected: ok

    // Test: SHELL as statement
    runProgram("SHELL statement",
        "SHELL \"echo hello > /dev/null\"\n"
        "PRINT \"done\"\n",
        "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",
        "equal\nless\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",
        "not equal\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",
        "-1 \n-1 \n0 \n0 \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",
        "-1 \n0 \n-1 \n-1 \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",
        "*1,234.50\n$  42.50\n+ 42.50\n- 42.50\n 42.50-\n 42.50 \n1.23E+03\n"
    );

    // Test: DEFINT -- every A-Z variable is INTEGER, so 3.7 rounds on store
    runProgram("DEFINT",
        "DEFINT A-Z\n"
        "a = 42\n"
        "b = 3.7\n"
        "PRINT a; b\n",
        "42 4 \n"
    );

    // Test: DEFSTR
    runProgram("DEFSTR",
        "DEFSTR S\n"
        "s = \"hello\"\n"
        "PRINT s\n",
        "hello\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",
        "10 20 3.14 \n"
    );

    // Test: OPTION EXPLICIT success
    runProgram("OPTION EXPLICIT ok",
        "OPTION EXPLICIT\n"
        "DIM x AS INTEGER\n"
        "x = 42\n"
        "PRINT x\n",
        "42 \n"
    );

    // Test: OPTION EXPLICIT failure (should error)
    expectCompileError("OPTION EXPLICIT error",
        "OPTION EXPLICIT\n"
        "x = 42\n",
        "Variable not declared: x"
    );

    // 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);

            if (mod) {
                BasVmT *vm = basVmCreate();
                basVmLoadModule(vm, mod);
                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");
    }

    // Regression test: App.Data must parse even though Data is a BASIC keyword
    runProgram("App.Data parses",
        "DIM p AS STRING\n"
        "p = App.Data\n"
        "PRINT p\n",
        NULL
    );

    // Regression test: RGB(r,g,b) packs into 0x00RRGGBB
    runProgram("RGB packing",
        "PRINT RGB(255, 128, 0)\n"
        "PRINT RGB(0, 255, 0)\n"
        "PRINT RGB(0, 0, 255)\n",
        "16744448 \n65280 \n255 \n"
    );
    // Expected: 16744448 (0xFF8000), 65280 (0x00FF00), 255 (0x0000FF)

    // Regression test: GetRed/Green/Blue round-trip
    runProgram("RGB round-trip",
        "DIM c AS LONG\n"
        "c = RGB(17, 99, 200)\n"
        "PRINT GetRed(c); GetGreen(c); GetBlue(c)\n",
        "17 99 200 \n"
    );
    // Expected: 17 99 200

    // Regression test: MsgBox function accepts optional title (3rd arg)
    // The test harness's default msgBox callback returns 1; we just verify
    // the program compiles and runs.
    runProgram("MsgBox 3-arg compile",
        "DIM r AS INTEGER\n"
        "r = MsgBox(\"hi\", 0, \"My Title\")\n"
        "PRINT \"ok\"\n",
        "ok\n"
    );

    // Regression test: CONST accepts AS type
    runProgram("CONST AS type",
        "CONST PI AS DOUBLE = 3.14159\n"
        "CONST N AS INTEGER = 42\n"
        "PRINT PI\n"
        "PRINT N\n",
        "3.14159 \n42 \n"
    );

    // Regression test: PRINT #ch with ; separator
    runProgram("PRINT # with semicolon",
        "OPEN \"/tmp/dvxbasic_psemi.txt\" FOR OUTPUT AS #1\n"
        "PRINT #1, \"Line one\"\n"
        "PRINT #1, \"ans = \"; 42\n"
        "PRINT #1, \"x\"; \"y\"; \"z\"\n"
        "CLOSE #1\n"
        "DIM s AS STRING\n"
        "OPEN \"/tmp/dvxbasic_psemi.txt\" FOR INPUT AS #1\n"
        "DO WHILE NOT EOF(1)\n"
        "    LINE INPUT #1, s\n"
        "    PRINT s\n"
        "LOOP\n"
        "CLOSE #1\n",
        "Line one\nans = 42\nxyz\n"
    );
    // Expected: Line one / ans = 42 / xyz

    // Regression test: IniRead$ with $ suffix must tokenize as keyword
    runProgram("IniRead$ tokenizes",
        "DIM v AS STRING\n"
        "v = IniRead$(\"/tmp/nofile.ini\", \"S\", \"K\", \"default\")\n"
        "PRINT v\n",
        "default\n"
    );
    // Expected: default (file doesn't exist)

    // Regression test: SUB extern call without parens must emit OP_CALL_EXTERN
    // (previously emitted OP_CALL which called nothing, because no internal
    // proc at address 0 existed for the extern.)
    {
        printf("=== DECLARE LIBRARY SUB no-parens ===\n");
        const char *src =
            "DECLARE LIBRARY \"basrt\"\n"
            "    DECLARE SUB DoIt(BYVAL s AS STRING)\n"
            "END DECLARE\n"
            "DoIt \"hello\"\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);

            if (mod) {
                bool found = false;

                for (int32_t i = 0; i < mod->codeLen; i++) {
                    if (mod->code[i] == OP_CALL_EXTERN) {
                        found = true;
                        break;
                    }
                }

                printf(found ? "PASS: OP_CALL_EXTERN emitted\n" : "FAIL: OP_CALL_EXTERN not emitted\n");
                if (!found) { gFailCount++; }
                basModuleFree(mod);
            } else {
                printf("MODULE BUILD FAILED\n");
                gFailCount++;
            }
        }

        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");
                gFailCount++;
            }

            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");
                gFailCount++;
            }

            // Test basVmCallSub
            BasVmT *vm = basVmCreate();
            basVmLoadModule(vm, mod);

            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");
                if (!callOk) { gFailCount++; }
            }

            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",
        "Before: x =10 \nAfter AddTen: x =20 \nBefore: a = hello\nAfter ChangeStr: a = hello world\nBefore: y =100 \nAfter NoChange: y =100 \nAfter AddTen(z+0): z =5 \n"
    );

    // ============================================================
    // Coverage: String functions
    // ============================================================

    runProgram("LCASE$",
        "PRINT LCASE$(\"HELLO WORLD\")\n",
        "hello world\n"
    );
    // Expected: hello world

    runProgram("TRIM$ LTRIM$ RTRIM$",
        "PRINT \"[\" & TRIM$(\"  hi  \") & \"]\"\n"
        "PRINT \"[\" & LTRIM$(\"  hi  \") & \"]\"\n"
        "PRINT \"[\" & RTRIM$(\"  hi  \") & \"]\"\n",
        "[hi]\n[hi  ]\n[  hi]\n"
    );
    // Expected: [hi] / [hi  ] / [  hi]

    runProgram("INSTR 2-arg",
        "PRINT INSTR(\"hello world\", \"world\")\n"
        "PRINT INSTR(\"hello world\", \"xyz\")\n",
        "7 \n0 \n"
    );
    // Expected: 7 / 0

    runProgram("INSTR 3-arg",
        "PRINT INSTR(5, \"abcabc\", \"bc\")\n"
        "PRINT INSTR(1, \"abcabc\", \"bc\")\n",
        "5 \n2 \n"
    );
    // Expected: 5 / 2

    runProgram("CHR$ and ASC",
        "PRINT CHR$(65)\n"
        "PRINT ASC(\"Z\")\n",
        "A\n90 \n"
    );
    // Expected: A / 90

    runProgram("SPACE$",
        "PRINT \"[\" & SPACE$(5) & \"]\"\n",
        "[     ]\n"
    );
    // Expected: [     ]

    runProgram("STRING$",
        "PRINT STRING$(5, \"*\")\n",
        "*****\n"
    );
    // Expected: *****

    runProgram("HEX$",
        "PRINT HEX$(255)\n"
        "PRINT HEX$(16)\n",
        "FF\n10\n"
    );
    // Expected: FF / 10

    runProgram("VAL",
        "PRINT VAL(\"3.14\")\n"
        "PRINT VAL(\"42\")\n"
        "PRINT VAL(\"abc\")\n",
        "3.14 \n42 \n0 \n"
    );
    // Expected: 3.14 / 42 / 0

    runProgram("STR$",
        "PRINT \"[\" & STR$(42) & \"]\"\n"
        "PRINT \"[\" & STR$(-7) & \"]\"\n",
        "[ 42]\n[-7]\n"
    );
    // Expected: [ 42] / [-7]

    runProgram("MID$ 2-arg",
        "PRINT MID$(\"hello world\", 7)\n",
        "world\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",
        "hello world\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",
        "1000 \n1000 \n999 \n"
    );
    // Expected: 1000 / 1000 / 999 (TAN(pi/4)*1000 is 999.99..., INT truncates)

    runProgram("LOG and EXP",
        "PRINT INT(LOG(1))\n"
        "PRINT INT(EXP(0))\n"
        "PRINT INT(EXP(1) * 100)\n",
        "0 \n1 \n271 \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",
        "3 \n-3 \n1 \n-1 \n0 \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",
        "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",
        "1 2 3 4 5 \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",
        "1 2 3 4 5 \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",
        "1 2 3 4 5 \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",
        "1 2 3 4 5 \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",
        "5 4 3 2 1 \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",
        "0 2 4 6 8 10 \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",
        "1 2 \nafter\n"
    );
    // Expected: 1 2 / after

    // Regression test: FOR loop inside a SUB with local loop variable
    runProgram("FOR inside SUB with local",
        "CALL MySub\n"
        "SUB MySub\n"
        "    DIM i AS LONG\n"
        "    FOR i = 0 TO 3\n"
        "        PRINT i;\n"
        "    NEXT i\n"
        "    PRINT\n"
        "END SUB\n",
        "0 1 2 3 \n"
    );
    // Expected: 0 1 2 3

    // Regression test: resedit pattern -- FOR in SUB with n = FUNC(...)
    runProgram("FOR with func-returned limit",
        "CALL DoIt\n"
        "SUB DoIt\n"
        "    DIM n AS LONG\n"
        "    n = GetCount()\n"
        "    DIM i AS LONG\n"
        "    FOR i = 0 TO n - 1\n"
        "        PRINT i;\n"
        "    NEXT i\n"
        "    PRINT\n"
        "END SUB\n"
        "FUNCTION GetCount() AS LONG\n"
        "    GetCount = 4\n"
        "END FUNCTION\n",
        "0 1 2 3 \n"
    );
    // Expected: 0 1 2 3

    // Regression test: resedit pattern -- FOR in SUB inside a FORM
    runProgram("FOR in SUB inside FORM",
        "BEGINFORM \"Form1\"\n"
        "DIM formVar AS LONG\n"
        "formVar = 42\n"
        "CALL DoIt\n"
        "SUB DoIt\n"
        "    DIM n AS LONG\n"
        "    n = 4\n"
        "    DIM ix AS LONG\n"
        "    FOR ix = 0 TO n - 1\n"
        "        PRINT ix;\n"
        "    NEXT ix\n"
        "    PRINT\n"
        "END SUB\n"
        "ENDFORM\n",
        ""
    );
    // Expected: nothing -- form init code (including the CALL) only runs when
    // the form is loaded, which this harness never does.

    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",
        "1 2 3 \nafter\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",
        "1 2 \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",
        "5 \n100 \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",
        "314 \n100 \nhello\n"
    );
    // Expected: 314 / 100 / hello

    // ============================================================
    // Coverage: END statement
    // ============================================================

    runProgram("END statement",
        "PRINT \"before\"\n"
        "END\n"
        "PRINT \"after\"\n",
        "before\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",
        "yes\nno\nTrue \nFalse \n"
    );
    // Expected: yes / no / True / False

    // ============================================================
    // 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",
        "-1 \n0 \nsmall\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",
        "9 \n15 \n6 \n"
    );
    // Expected: 9 / 15 / 6

    // ============================================================
    // Coverage: SLEEP
    // ============================================================

    runProgram("SLEEP",
        "SLEEP 0\n"
        "PRINT \"ok\"\n",
        "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",
        "resumed\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",
        "caught error999 \n"
    );
    // Expected: caught error999

    // ============================================================
    // 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",
        "42 \nhello\n"
    );
    // Expected: 42 / hello

    runProgram("Hex literals",
        "PRINT &HFF\n"
        "PRINT &H10\n"
        "DIM x AS INTEGER\n"
        "x = &H0A\n"
        "PRINT x\n",
        "255 \n16 \n10 \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",
        "200000 \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",
        "0 \n"
    );
    // Expected: 0 (data cleared)

    // ============================================================
    // Coverage: PRINT variants
    // ============================================================

    runProgram("PRINT comma separator",
        "PRINT 1, 2, 3\n",
        "1 \t2 \t3 \n"
    );
    // Expected: 1<tab>2<tab>3

    runProgram("PRINT bare newline",
        "PRINT \"a\"\n"
        "PRINT\n"
        "PRINT \"b\"\n",
        "a\n\nb\n"
    );
    // Expected: a / (blank) / b

    // ============================================================
    // Coverage: LET keyword
    // ============================================================

    runProgram("LET keyword",
        "DIM x AS INTEGER\n"
        "LET x = 42\n"
        "PRINT x\n",
        "42 \n"
    );
    // Expected: 42

    // ============================================================
    // Coverage: Line continuation
    // ============================================================

    runProgram("Line continuation",
        "DIM x AS INTEGER\n"
        "x = 1 + _\n"
        "    2 + _\n"
        "    3\n"
        "PRINT x\n",
        "6 \n"
    );
    // Expected: 6

    // ============================================================
    // Coverage: REM comment
    // ============================================================

    runProgram("REM comment",
        "REM This is a comment\n"
        "PRINT \"ok\"\n"
        "PRINT \"hi\" REM inline comment\n",
        "ok\nhi\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",
        "fifteen or twenty\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",
        "11-20\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",
        "medium\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",
        "mid\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",
        "11 12 21 22 31 32 \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",
        "9.5 \n17.5 \n3.33333333333333 \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",
        "20 \n30 \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",
        "1 \n120 \n3628800 \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",
        "less\ngreater\nequal\nnotequal\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",
        "before\nafter\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",
        "314 \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",
        "4 \n42 \n3 \n3 \n[42]\n"
    );
    // Expected: 4 / 42 / 3 / 3 / [42]

    // ============================================================
    // Coverage: Me.Show / Me.Hide as statements
    // ============================================================

    {
        printf("=== Me.Show / Me.Hide statements ===\n");

        // These compile -- runtime needs a form context to actually show/hide
        const char *src =
            "Sub Form1_Load ()\n"
            "    Me.Show\n"
            "    Me.Hide\n"
            "    PRINT \"me ok\"\n"
            "End Sub\n";

        int32_t len = (int32_t)strlen(src);
        BasParserT parser;
        basParserInit(&parser, src, len);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n\n", parser.error);
            basParserFree(&parser);
        } else {
            BasModuleT *mod = basParserBuildModule(&parser);
            basParserFree(&parser);

            if (mod) {
                printf("Compiled OK, proc=%s\n", mod->procs[0].name);
                basModuleFree(mod);
            }
        }

        printf("\n");
    }

    // ============================================================
    // Coverage: Me keyword with form context
    // ============================================================
    {
        printf("=== Me keyword with form context ===\n");

        const char *src =
            "Sub Form1_Load ()\n"
            "    PRINT \"load fired\"\n"
            "End Sub\n"
            "\n"
            "Sub Command1_Click ()\n"
            "    PRINT \"click fired\"\n"
            "End Sub\n"
            "\n"
            "Sub Text1_KeyPress (KeyAscii As Integer)\n"
            "    PRINT \"key:\"; KeyAscii\n"
            "End Sub\n"
            "\n"
            "Sub Picture1_MouseDown (Button As Integer, X As Integer, Y As Integer)\n"
            "    PRINT \"mouse:\"; Button; X; Y\n"
            "End Sub\n";

        int32_t len = (int32_t)strlen(src);
        BasParserT parser;
        basParserInit(&parser, src, len);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n\n", parser.error);
            basParserFree(&parser);
        } else {
            BasModuleT *mod = basParserBuildModule(&parser);
            basParserFree(&parser);

            if (!mod) {
                printf("MODULE BUILD FAILED\n\n");
                gFailCount++;
            } else {
                BasVmT *vm = basVmCreate();
                basVmLoadModule(vm, mod);

                // Test: find and call Form1_Load (no args)
                const BasProcEntryT *loadProc = basModuleFindProc(mod, "Form1_Load");

                if (loadProc) {
                    basVmCallSub(vm, loadProc->codeAddr);
                } else {
                    printf("Form1_Load not found!\n");
                    gFailCount++;
                }

                // Test: find and call Command1_Click (no args)
                const BasProcEntryT *clickProc = basModuleFindProc(mod, "Command1_Click");

                if (clickProc) {
                    basVmCallSub(vm, clickProc->codeAddr);
                } else {
                    printf("Command1_Click not found!\n");
                    gFailCount++;
                }

                // Test: find and call Text1_KeyPress with parameter
                const BasProcEntryT *keyProc = basModuleFindProc(mod, "Text1_KeyPress");

                if (keyProc) {
                    BasValueT args[1];
                    args[0] = basValLong(65); // 'A'
                    basVmCallSubWithArgs(vm, keyProc->codeAddr, args, 1);
                } else {
                    printf("Text1_KeyPress not found!\n");
                }

                // Test: find and call Picture1_MouseDown with 3 parameters
                const BasProcEntryT *mouseProc = basModuleFindProc(mod, "Picture1_MouseDown");

                if (mouseProc) {
                    BasValueT args[3];
                    args[0] = basValLong(1);   // left button
                    args[1] = basValLong(50);  // X
                    args[2] = basValLong(100); // Y
                    basVmCallSubWithArgs(vm, mouseProc->codeAddr, args, 3);
                } else {
                    printf("Picture1_MouseDown not found!\n");
                }

                // Test: verify proc table
                printf("Proc count: %d\n", (int)mod->procCount);

                for (int32_t i = 0; i < mod->procCount; i++) {
                    printf("  [%d] %s (params=%d)\n", (int)i, mod->procs[i].name, (int)mod->procs[i].paramCount);
                }

                basVmDestroy(vm);
                basModuleFree(mod);
            }
        }

        printf("\n");
    }

    // ============================================================
    // Coverage: Event parameter mismatch
    // ============================================================

    {
        printf("=== Event parameter mismatch ===\n");

        const char *src =
            "Sub NoParams ()\n"
            "    PRINT \"no params ok\"\n"
            "End Sub\n"
            "\n"
            "Sub OneParam (X As Integer)\n"
            "    PRINT \"X =\"; X\n"
            "End Sub\n"
            "\n"
            "Sub TwoParams (A As Integer, B As Integer)\n"
            "    PRINT \"A =\"; A; \"B =\"; B\n"
            "End Sub\n";

        int32_t len = (int32_t)strlen(src);
        BasParserT parser;
        basParserInit(&parser, src, len);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n\n", parser.error);
            basParserFree(&parser);
        } else {
            BasModuleT *mod = basParserBuildModule(&parser);
            basParserFree(&parser);

            if (mod) {
                BasVmT *vm = basVmCreate();
                basVmLoadModule(vm, mod);

                // Call NoParams with no args
                const BasProcEntryT *p = basModuleFindProc(mod, "NoParams");

                if (p) {
                    basVmCallSub(vm, p->codeAddr);
                }

                // Call OneParam with 1 arg
                p = basModuleFindProc(mod, "OneParam");

                if (p) {
                    BasValueT args[1];
                    args[0] = basValLong(42);
                    basVmCallSubWithArgs(vm, p->codeAddr, args, 1);
                }

                // Call TwoParams with 2 args
                p = basModuleFindProc(mod, "TwoParams");

                if (p) {
                    BasValueT args[2];
                    args[0] = basValLong(10);
                    args[1] = basValLong(20);
                    basVmCallSubWithArgs(vm, p->codeAddr, args, 2);
                }

                basVmDestroy(vm);
                basModuleFree(mod);
            }
        }

        printf("\n");
    }

    // ============================================================
    // Coverage: Procedure extraction from source
    // ============================================================

    runProgram("Mixed module-level and procedures",
        "DIM x As Integer\n"
        "x = 10\n"
        "PRINT \"module:\"; x\n"
        "\n"
        "Sub Helper ()\n"
        "    PRINT \"helper called\"\n"
        "End Sub\n"
        "\n"
        "Function Add (a As Integer, b As Integer) As Integer\n"
        "    Add = a + b\n"
        "End Function\n"
        "\n"
        "CALL Helper\n"
        "PRINT \"sum:\"; Add(3, 4)\n",
        "module:10 \nhelper called\nsum:7 \n"
    );

    // ============================================================
    // Coverage: ? shortcut for PRINT
    // ============================================================

    runProgram("? shortcut for PRINT",
        "? \"hello\"\n"
        "? 1 + 2\n",
        "hello\n3 \n"
    );

    // ============================================================
    // Coverage: Code in .frm format (Sub after form definition)
    // ============================================================

    {
        printf("=== FRM code extraction ===\n");

        // Simulate what the IDE does: code section from a .frm
        const char *frmCode =
            "Sub Form1_Load ()\n"
            "    PRINT \"form loaded\"\n"
            "End Sub\n"
            "\n"
            "Sub Command1_Click ()\n"
            "    PRINT \"button clicked\"\n"
            "End Sub\n";

        int32_t len = (int32_t)strlen(frmCode);
        BasParserT parser;
        basParserInit(&parser, frmCode, len);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n\n", parser.error);
            basParserFree(&parser);
        } else {
            BasModuleT *mod = basParserBuildModule(&parser);
            basParserFree(&parser);

            if (mod) {
                BasVmT *vm = basVmCreate();
                basVmLoadModule(vm, mod);

                // Fire both events
                const BasProcEntryT *p1 = basModuleFindProc(mod, "Form1_Load");
                const BasProcEntryT *p2 = basModuleFindProc(mod, "Command1_Click");

                if (p1) { basVmCallSub(vm, p1->codeAddr); }
                else    { printf("Form1_Load NOT FOUND\n"); }

                if (p2) { basVmCallSub(vm, p2->codeAddr); }
                else    { printf("Command1_Click NOT FOUND\n"); }

                basVmDestroy(vm);
                basModuleFree(mod);
            }
        }

        printf("\n");
    }

    // ============================================================
    // Coverage: RANDOMIZE TIMER
    // ============================================================

    runProgram("RANDOMIZE TIMER",
        "RANDOMIZE TIMER\n"
        "DIM x AS SINGLE\n"
        "x = RND\n"
        "PRINT \"rnd ok\"\n",
        "rnd ok\n"
    );

    // ============================================================
    // Coverage: Forward GOSUB
    // ============================================================

    runProgram("Forward GOSUB",
        "GOSUB doWork\n"
        "PRINT \"back\"\n"
        "END\n"
        "doWork:\n"
        "PRINT \"working\"\n"
        "RETURN\n",
        "working\nback\n"
    );

    // ============================================================
    // Coverage: Forward GOTO
    // ============================================================

    runProgram("Forward GOTO to label",
        "GOTO skip\n"
        "PRINT \"should not print\"\n"
        "skip:\n"
        "PRINT \"jumped\"\n",
        "jumped\n"
    );

    // ============================================================
    // Coverage: Array of UDT
    // ============================================================

    runProgram("Array of UDT",
        "TYPE PointT\n"
        "    x AS INTEGER\n"
        "    y AS INTEGER\n"
        "END TYPE\n"
        "\n"
        "DIM pts(3) AS PointT\n"
        "pts(1).x = 10\n"
        "pts(1).y = 20\n"
        "pts(2).x = 30\n"
        "pts(2).y = 40\n"
        "PRINT pts(1).x; pts(1).y\n"
        "PRINT pts(2).x; pts(2).y\n",
        "10 20 \n30 40 \n"
    );

    // ============================================================
    // Coverage: Nested UDTs
    // ============================================================

    runProgram("Nested UDT",
        "TYPE AddressT\n"
        "    city AS STRING\n"
        "    zip AS INTEGER\n"
        "END TYPE\n"
        "\n"
        "TYPE PersonT\n"
        "    fullName AS STRING\n"
        "    addr AS AddressT\n"
        "END TYPE\n"
        "\n"
        "DIM p AS PersonT\n"
        "p.fullName = \"Alice\"\n"
        "p.addr.city = \"NYC\"\n"
        "p.addr.zip = 10001\n"
        "PRINT p.fullName\n"
        "PRINT p.addr.city\n"
        "PRINT p.addr.zip\n",
        "Alice\nNYC\n10001 \n"
    );

    // ============================================================
    // Coverage: Operator precedence stress
    // ============================================================

    runProgram("Operator precedence",
        "' NOT has highest unary, then AND, OR, XOR, EQV, IMP\n"
        "PRINT NOT 0 AND -1\n"              // NOT 0 = -1, -1 AND -1 = -1
        "PRINT (1 > 0) AND (2 > 1)\n"       // True AND True = -1
        "PRINT (1 > 0) OR (2 < 1)\n"        // True OR False = -1
        "PRINT 5 AND 3\n"                    // bitwise: 1
        "PRINT 5 OR 3\n"                     // bitwise: 7
        "PRINT 5 XOR 3\n"                    // bitwise: 6
        "PRINT 2 + 3 * 4 - 1\n"             // 2 + 12 - 1 = 13
        "PRINT (2 + 3) * (4 - 1)\n"         // 5 * 3 = 15
        "PRINT -2 ^ 2\n",                     // -(2^2) = -4 (VB precedence)
        "-1 \n-1 \n-1 \n1 \n7 \n6 \n13 \n15 \n-4 \n"
    );

    // ============================================================
    // Coverage: Nested FOR with EXIT
    // ============================================================

    runProgram("Nested FOR with EXIT FOR",
        "DIM i AS INTEGER\n"
        "DIM j AS INTEGER\n"
        "FOR i = 1 TO 3\n"
        "    FOR j = 1 TO 3\n"
        "        IF j = 2 THEN EXIT FOR\n"
        "        PRINT i; j\n"
        "    NEXT j\n"
        "NEXT i\n",
        "1 1 \n2 1 \n3 1 \n"
    );

    // ============================================================
    // Coverage: Forward reference CALL (sub defined after use)
    // ============================================================

    runProgram("Forward CALL reference",
        "CALL doWork\n"
        "PRINT \"after call\"\n"
        "\n"
        "Sub doWork ()\n"
        "    PRINT \"in sub\"\n"
        "End Sub\n",
        "in sub\nafter call\n"
    );

    // ============================================================
    // Coverage: Multiple CONST declarations
    // ============================================================

    runProgram("Multiple CONST",
        "CONST PI = 3.14159\n"
        "CONST E = 2.71828\n"
        "CONST APPNAME = \"DVX\"\n"
        "PRINT INT(PI * 100)\n"
        "PRINT INT(E * 100)\n"
        "PRINT APPNAME\n",
        "314 \n271 \nDVX\n"
    );

    // ============================================================
    // Coverage: SELECT CASE with strings
    // ============================================================

    runProgram("SELECT CASE strings",
        "DIM s AS STRING\n"
        "s = \"hello\"\n"
        "SELECT CASE s\n"
        "    CASE \"world\"\n"
        "        PRINT \"wrong\"\n"
        "    CASE \"hello\"\n"
        "        PRINT \"right\"\n"
        "    CASE ELSE\n"
        "        PRINT \"default\"\n"
        "END SELECT\n",
        "right\n"
    );

    // ============================================================
    // Coverage: Nested IF/ELSEIF chains
    // ============================================================

    runProgram("Nested IF chains",
        "DIM x AS INTEGER\n"
        "x = 50\n"
        "IF x > 90 THEN\n"
        "    PRINT \"A\"\n"
        "ELSEIF x > 80 THEN\n"
        "    PRINT \"B\"\n"
        "ELSEIF x > 70 THEN\n"
        "    PRINT \"C\"\n"
        "ELSEIF x > 60 THEN\n"
        "    PRINT \"D\"\n"
        "ELSE\n"
        "    PRINT \"F\"\n"
        "END IF\n",
        "F\n"
    );

    // ============================================================
    // Coverage: DO WHILE with EXIT DO
    // ============================================================

    runProgram("DO WHILE with EXIT DO",
        "DIM i AS INTEGER\n"
        "i = 0\n"
        "DO WHILE i < 100\n"
        "    i = i + 1\n"
        "    IF i = 5 THEN EXIT DO\n"
        "LOOP\n"
        "PRINT i\n",
        "5 \n"
    );

    // ============================================================
    // Coverage: Recursive Fibonacci
    // ============================================================

    runProgram("Recursive Fibonacci",
        "Function Fib (n As Integer) As Integer\n"
        "    IF n <= 1 THEN\n"
        "        Fib = n\n"
        "    ELSE\n"
        "        Fib = Fib(n - 1) + Fib(n - 2)\n"
        "    END IF\n"
        "End Function\n"
        "\n"
        "PRINT Fib(0); Fib(1); Fib(5); Fib(10)\n",
        "0 1 5 55 \n"
    );

    // ============================================================
    // Coverage: String comparison operators
    // ============================================================

    runProgram("String comparison ops",
        "IF \"abc\" < \"def\" THEN PRINT \"lt ok\"\n"
        "IF \"abc\" <= \"abc\" THEN PRINT \"le ok\"\n"
        "IF \"def\" > \"abc\" THEN PRINT \"gt ok\"\n"
        "IF \"abc\" >= \"abc\" THEN PRINT \"ge ok\"\n"
        "IF \"abc\" <> \"def\" THEN PRINT \"ne ok\"\n"
        "IF \"abc\" = \"abc\" THEN PRINT \"eq ok\"\n",
        "lt ok\nle ok\ngt ok\nge ok\nne ok\neq ok\n"
    );

    // ============================================================
    // Coverage: LOAD/UNLOAD/Me statements compile
    // ============================================================

    {
        printf("=== Form statements compile ===\n");

        const char *src =
            "Sub Form1_Load ()\n"
            "    Me.Show\n"
            "    Me.Hide\n"
            "    Load frmOther\n"
            "    Unload frmOther\n"
            "End Sub\n";

        int32_t len = (int32_t)strlen(src);
        BasParserT parser;
        basParserInit(&parser, src, len);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
            basParserFree(&parser);
        } else {
            BasModuleT *mod = basParserBuildModule(&parser);
            basParserFree(&parser);

            if (mod) {
                printf("OK: %d procs, %d bytes\n", (int)mod->procCount, (int)mod->codeLen);
                basModuleFree(mod);
            }
        }

        printf("\n");
    }

    // ============================================================
    // Coverage: Me.Property assignment compiles
    // ============================================================

    {
        printf("=== Me.Property assignment ===\n");

        const char *src =
            "Sub Form1_Load ()\n"
            "    Me.Caption = \"Hello\"\n"
            "    Me.Visible = 1\n"
            "End Sub\n";

        int32_t len = (int32_t)strlen(src);
        BasParserT parser;
        basParserInit(&parser, src, len);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
            basParserFree(&parser);
        } else {
            BasModuleT *mod = basParserBuildModule(&parser);
            basParserFree(&parser);

            if (mod) {
                printf("OK\n");
                basModuleFree(mod);
            }
        }

        printf("\n");
    }

    // ============================================================
    // Coverage: Me.Control.Property compiles
    // ============================================================

    {
        printf("=== Me.Control.Property ===\n");

        const char *src =
            "Sub Form1_Load ()\n"
            "    Me.Text1.Text = \"hello\"\n"
            "    Me.Label1.Caption = \"world\"\n"
            "End Sub\n";

        int32_t len = (int32_t)strlen(src);
        BasParserT parser;
        basParserInit(&parser, src, len);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
            basParserFree(&parser);
        } else {
            BasModuleT *mod = basParserBuildModule(&parser);
            basParserFree(&parser);

            if (mod) {
                printf("OK\n");
                basModuleFree(mod);
            }
        }

        printf("\n");
    }

    // ============================================================
    // Coverage: DOEVENTS compiles
    // ============================================================

    {
        printf("=== DOEVENTS compiles ===\n");

        const char *src =
            "Sub Form1_Load ()\n"
            "    DoEvents\n"
            "End Sub\n";

        int32_t len = (int32_t)strlen(src);
        BasParserT parser;
        basParserInit(&parser, src, len);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
            basParserFree(&parser);
        } else {
            BasModuleT *mod = basParserBuildModule(&parser);
            basParserFree(&parser);

            if (mod) {
                printf("OK\n");
                basModuleFree(mod);
            }
        }

        printf("\n");
    }

    // ============================================================
    // Coverage: Bare sub call (no CALL keyword, no parens)
    // ============================================================

    runProgram("Bare sub call",
        "Sub Greet ()\n"
        "    PRINT \"hello\"\n"
        "End Sub\n"
        "\n"
        "Greet\n",
        "hello\n"
    );

    // ============================================================
    // Coverage: Bare sub call with forward reference
    // ============================================================

    runProgram("Bare sub call forward ref",
        "DoWork\n"
        "\n"
        "Sub DoWork ()\n"
        "    PRINT \"worked\"\n"
        "End Sub\n",
        "worked\n"
    );

    // ============================================================
    // Coverage: Unresolved forward reference error
    // ============================================================

    expectCompileError("Unresolved forward reference",
        "NeverDefined\n",
        "Undefined Sub: NeverDefined"
    );

    // ============================================================
    // Coverage: Nested UDT field store
    // ============================================================

    runProgram("Nested UDT store and load",
        "TYPE InnerT\n"
        "    val AS INTEGER\n"
        "END TYPE\n"
        "\n"
        "TYPE OuterT\n"
        "    child AS InnerT\n"
        "    label AS STRING\n"
        "END TYPE\n"
        "\n"
        "DIM o AS OuterT\n"
        "o.label = \"test\"\n"
        "o.child.val = 42\n"
        "PRINT o.label\n"
        "PRINT o.child.val\n",
        "test\n42 \n"
    );

    // ============================================================
    // Coverage: Array of UDT field store
    // ============================================================

    runProgram("Array of UDT field store",
        "TYPE PointT\n"
        "    x AS INTEGER\n"
        "    y AS INTEGER\n"
        "END TYPE\n"
        "\n"
        "DIM pts(5) AS PointT\n"
        "pts(1).x = 10\n"
        "pts(1).y = 20\n"
        "pts(3).x = 30\n"
        "pts(3).y = 40\n"
        "PRINT pts(1).x; pts(1).y\n"
        "PRINT pts(3).x; pts(3).y\n",
        "10 20 \n30 40 \n"
    );

    // ============================================================
    // Coverage: VB operator precedence (^ binds tighter than unary -)
    // ============================================================

    runProgram("Exponent precedence",
        "PRINT -2 ^ 2\n"         // -(2^2) = -4
        "PRINT (-2) ^ 2\n"       // (-2)^2 = 4
        "PRINT 3 ^ 2 + 1\n",     // 9 + 1 = 10
        "-4 \n4 \n10 \n"
    );

    // ============================================================
    // Coverage: Me.Show compiles in a Sub
    // ============================================================

    {
        printf("=== Me.Show in Sub ===\n");

        const char *src =
            "Sub Form1_Load ()\n"
            "    Me.Show\n"
            "    Me.Caption = \"Hello\"\n"
            "    Me.Hide\n"
            "End Sub\n";

        int32_t len = (int32_t)strlen(src);
        BasParserT parser;
        basParserInit(&parser, src, len);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
        } else {
            printf("OK\n");
        }

        basParserFree(&parser);
        printf("\n");
    }

    // ============================================================
    // Coverage: OPTION EXPLICIT with valid declaration
    // ============================================================

    runProgram("OPTION EXPLICIT valid",
        "OPTION EXPLICIT\n"
        "DIM x AS INTEGER\n"
        "x = 42\n"
        "PRINT x\n",
        "42 \n"
    );

    // ============================================================
    // Coverage: STATIC variable retains value
    // ============================================================

    runProgram("STATIC in sub",
        "Sub Counter ()\n"
        "    STATIC n AS INTEGER\n"
        "    n = n + 1\n"
        "    PRINT n\n"
        "End Sub\n"
        "\n"
        "Counter\n"
        "Counter\n"
        "Counter\n",
        "1 \n2 \n3 \n"
    );

    // ============================================================
    // Coverage: MsgBox statement form (no return value)
    // ============================================================

    runProgram("MsgBox statement",
        "MsgBox \"Hello\"\n",
        ""
    );

    // ============================================================
    // Coverage: MsgBox statement with flags
    // ============================================================

    runProgram("MsgBox statement with flags",
        "MsgBox \"Save?\", vbYesNo + vbQuestion\n",
        ""
    );

    // ============================================================
    // Coverage: MsgBox function form (returns value)
    // ============================================================

    runProgram("MsgBox function",
        "DIM result AS INTEGER\n"
        "result = MsgBox(\"Continue?\", vbYesNo)\n"
        "PRINT result\n",
        "1 \n"
    );

    // ============================================================
    // Coverage: MsgBox function with default flags
    // ============================================================

    runProgram("MsgBox function default flags",
        "DIM r AS INTEGER\n"
        "r = MsgBox(\"OK\")\n"
        "PRINT r\n",
        "1 \n"
    );

    // ============================================================
    // Coverage: VB3 predefined constants
    // ============================================================

    runProgram("VB3 predefined constants",
        "PRINT vbOKOnly\n"
        "PRINT vbOKCancel\n"
        "PRINT vbYesNo\n"
        "PRINT vbYesNoCancel\n"
        "PRINT vbRetryCancel\n"
        "PRINT vbInformation\n"
        "PRINT vbExclamation\n"
        "PRINT vbCritical\n"
        "PRINT vbQuestion\n"
        "PRINT vbOK\n"
        "PRINT vbCancel\n"
        "PRINT vbYes\n"
        "PRINT vbNo\n"
        "PRINT vbRetry\n",
        "0 \n1 \n2 \n3 \n4 \n16 \n32 \n48 \n64 \n1 \n2 \n3 \n4 \n5 \n"
    );

    // ============================================================
    // Coverage: MsgBox result used in If
    // ============================================================

    runProgram("MsgBox in If condition",
        "If MsgBox(\"Exit?\", vbYesNo) = vbYes Then\n"
        "    PRINT \"yes\"\n"
        "End If\n",
        ""
    );

    // ============================================================
    // Coverage: Control array read -- Name(0).Caption
    // ============================================================

    {
        printf("=== Control array read ===\n");
        BasParserT parser;
        basParserInit(&parser,
            "DIM x AS STRING\n"
            "x = Command1(0).Caption\n"
            "PRINT x\n",
            -1);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
        } else {
            printf("OK\n");
        }

        basParserFree(&parser);
        printf("\n");
    }

    // ============================================================
    // Coverage: Control array write -- Name(idx).Caption = "text"
    // ============================================================

    {
        printf("=== Control array write ===\n");
        BasParserT parser;
        basParserInit(&parser,
            "DIM idx AS INTEGER\n"
            "idx = 1\n"
            "Command1(idx).Caption = \"Hello\"\n",
            -1);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
        } else {
            printf("OK\n");
        }

        basParserFree(&parser);
        printf("\n");
    }

    // ============================================================
    // Coverage: Control array complex index -- Name(X + 1).Caption
    // ============================================================

    {
        printf("=== Control array complex index ===\n");
        BasParserT parser;
        basParserInit(&parser,
            "DIM i AS INTEGER\n"
            "DIM s AS STRING\n"
            "i = 2\n"
            "s = Btn(i + 1).Text\n"
            "PRINT s\n",
            -1);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
        } else {
            printf("OK\n");
        }

        basParserFree(&parser);
        printf("\n");
    }

    // ============================================================
    // Coverage: Forward-ref function still works (no dot after paren)
    // ============================================================

    runProgram("Forward-ref function still works",
        "Function GetValue () As Integer\n"
        "    GetValue = 42\n"
        "End Function\n"
        "\n"
        "PRINT GetValue()\n",
        "42 \n"
    );

    // ============================================================
    // Coverage: Control array in event handler
    // ============================================================

    {
        printf("=== Control array event handler ===\n");
        BasParserT parser;
        basParserInit(&parser,
            "Sub Cmd_Click (Index As Integer)\n"
            "    Cmd(Index).Caption = \"Clicked\"\n"
            "End Sub\n",
            -1);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
        } else {
            printf("OK\n");
        }

        basParserFree(&parser);
        printf("\n");
    }

    // ============================================================
    // Coverage: Me.Control(idx).Property
    // ============================================================

    {
        printf("=== Me.Control(idx).Property ===\n");
        BasParserT parser;
        basParserInit(&parser,
            "Sub Cmd_Click (Index As Integer)\n"
            "    Me.Cmd(Index).Caption = \"OK\"\n"
            "End Sub\n",
            -1);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
        } else {
            printf("OK\n");
        }

        basParserFree(&parser);
        printf("\n");
    }

    // ============================================================
    // Coverage: BEGINFORM/ENDFORM with form-level DIM
    // ============================================================

    {
        printf("=== Form-level variables ===\n");
        BasParserT parser;
        basParserInit(&parser,
            "BEGINFORM \"Form1\"\n"
            "Dim counter As Integer\n"
            "Dim msg As String\n"
            "\n"
            "Sub Form1_Load ()\n"
            "    counter = 0\n"
            "    msg = \"hello\"\n"
            "End Sub\n"
            "\n"
            "Sub Timer1_Timer ()\n"
            "    counter = counter + 1\n"
            "End Sub\n"
            "ENDFORM\n",
            -1);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
        } else {
            printf("OK\n");
        }

        basParserFree(&parser);
        printf("\n");
    }

    // ============================================================
    // Coverage: Form-scope arrays compile (init deferred to load)
    // ============================================================

    {
        printf("=== Form-scope array ===\n");
        BasParserT parser;
        basParserInit(&parser,
            "BEGINFORM \"Form1\"\n"
            "Dim arr(10) As Integer\n"
            "\n"
            "Sub Form1_Load ()\n"
            "    arr(0) = 42\n"
            "End Sub\n"
            "ENDFORM\n",
            -1);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
        } else {
            printf("OK\n");
        }

        basParserFree(&parser);
        printf("\n");
    }

    // ============================================================
    // Coverage: Two forms with same-named form vars (independent)
    // ============================================================

    {
        printf("=== Two forms same var name ===\n");
        BasParserT parser;
        basParserInit(&parser,
            "BEGINFORM \"Form1\"\n"
            "Dim counter As Integer\n"
            "Sub Form1_Load ()\n"
            "    counter = 1\n"
            "End Sub\n"
            "ENDFORM\n"
            "\n"
            "BEGINFORM \"Form2\"\n"
            "Dim counter As Integer\n"
            "Sub Form2_Load ()\n"
            "    counter = 2\n"
            "End Sub\n"
            "ENDFORM\n",
            -1);

        if (!basParse(&parser)) {
            printf("COMPILE ERROR: %s\n", parser.error);
        } else {
            printf("OK\n");
        }

        basParserFree(&parser);
        printf("\n");
    }

    // ============================================================
    // Coverage: Nested BEGINFORM rejected
    // ============================================================

    expectCompileError("Nested BEGINFORM rejected",
        "BEGINFORM \"Form1\"\n"
        "BEGINFORM \"Form2\"\n"
        "ENDFORM\n"
        "ENDFORM\n",
        "Nested BEGINFORM is not allowed"
    );

    // ============================================================
    // Regression: compiler bugs found by the aab9fac review
    // ============================================================

    expectCompileError("Self-referential TYPE rejected",
        "TYPE T\n"
        "  x AS INTEGER\n"
        "  y AS T\n"
        "END TYPE\n"
        "DIM v AS T\n",
        "cannot contain a field of its own type"
    );

    expectCompileError("DEF FN inside SUB rejected",
        "SUB S\n"
        "  DIM x AS INTEGER\n"
        "  DEF FNd(y) = y * 2\n"
        "END SUB\n",
        "DEF FN is not allowed inside SUB or FUNCTION"
    );

    // RETURN inside a SELECT CASE must discard the live test value before
    // OP_GOSUB_RET pops the return address.
    runProgram("RETURN inside SELECT CASE",
        "DIM x AS INTEGER\n"
        "x = 1\n"
        "GOSUB Handler\n"
        "PRINT \"back\"\n"
        "END\n"
        "Handler:\n"
        "SELECT CASE x\n"
        "  CASE 1\n"
        "    PRINT \"one\"\n"
        "    RETURN\n"
        "END SELECT\n"
        "RETURN\n",
        "one\nback\n"
    );

    expectCompileError("Long identifier rejected",
        "DIM abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij AS INTEGER\n",
        "Identifier too long"
    );

    // A variable first used inside a SUB is local to it: the module-level
    // y is a different (zero) variable, and the implicit string local is
    // "" on every call rather than accumulating.
    runProgram("Implicit variable in SUB is local",
        "SUB S\n"
        "  y = 4\n"
        "  s$ = s$ & \"a\"\n"
        "  PRINT y; s$\n"
        "END SUB\n"
        "S\n"
        "S\n"
        "PRINT y\n",
        "4 a\n4 a\n0 \n"
    );

    runProgram("Implicit locals are per call in recursion",
        "FUNCTION fact(n)\n"
        "  IF n <= 1 THEN\n"
        "    fact = 1\n"
        "  ELSE\n"
        "    t = n\n"
        "    fact = t * fact(n - 1)\n"
        "  END IF\n"
        "END FUNCTION\n"
        "PRINT fact(5)\n",
        "120 \n"
    );

    expectCompileError("Assignment to another FUNCTION name rejected",
        "FUNCTION F\n"
        "  F = 1\n"
        "END FUNCTION\n"
        "SUB S(a)\n"
        "  F = 99\n"
        "END SUB\n",
        "Cannot assign to function 'F'"
    );

    expectCompileError("Assignment to SUB name rejected",
        "SUB S\n"
        "END SUB\n"
        "S = 5\n",
        "Cannot assign to 'S'"
    );

    expectCompileError("Assignment to label rejected",
        "Lbl:\n"
        "Lbl = 5\n",
        "Cannot assign to 'Lbl'"
    );

    expectCompileError("SUB name as operand rejected",
        "SUB S\n"
        "END SUB\n"
        "PRINT S + 1\n",
        "'S' is not a variable"
    );

    runProgram("DATA hex literal keeps 32 bits",
        "DATA &HFFFF, &H12345\n"
        "READ a&, b&\n"
        "PRINT a&; b&\n",
        "65535 74565 \n"
    );

    expectCompileError("EXIT FOR in SUB without loop rejected",
        "SUB S\n"
        "  EXIT FOR\n"
        "END SUB\n"
        "FOR i = 1 TO 3\n"
        "  S\n"
        "NEXT i\n",
        "EXIT FOR outside FOR loop"
    );

    expectCompileError("EXIT DO outside loop rejected",
        "EXIT DO\n",
        "EXIT DO outside DO or WHILE loop"
    );

    expectCompileError("EXIT SUB at module level rejected",
        "EXIT SUB\n",
        "EXIT SUB outside SUB"
    );

    expectCompileError("EXIT SUB inside FUNCTION rejected",
        "FUNCTION F\n"
        "  EXIT SUB\n"
        "END FUNCTION\n",
        "EXIT SUB outside SUB"
    );

    runProgram("EXIT FOR out of SELECT CASE",
        "FOR i = 1 TO 3\n"
        "  SELECT CASE i\n"
        "    CASE 2\n"
        "      EXIT FOR\n"
        "  END SELECT\n"
        "  PRINT i\n"
        "NEXT i\n"
        "PRINT \"done\"\n",
        "1 \ndone\n"
    );

    {
        // Nesting limits: deep parentheses, unary operators and blocks must
        // be compile errors, not stack overflows.
        static char deepSrc[8192];
        int32_t     pos = 0;

        pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "x = ");
        for (int32_t i = 0; i < BAS_MAX_EXPR_DEPTH + 8; i++) {
            pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "(");
        }
        pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "1");
        for (int32_t i = 0; i < BAS_MAX_EXPR_DEPTH + 8; i++) {
            pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, ")");
        }
        snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "\n");
        expectCompileError("Deep expression nesting rejected", deepSrc, "Expression nested too deeply");

        pos = 0;
        pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "x = ");
        for (int32_t i = 0; i < BAS_MAX_EXPR_DEPTH + 8; i++) {
            pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "-");
        }
        snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "1\n");
        expectCompileError("Deep unary nesting rejected", deepSrc, "Expression nested too deeply");

        pos = 0;
        for (int32_t i = 0; i < BAS_MAX_BLOCK_DEPTH + 8; i++) {
            pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "IF 1 THEN\n");
        }
        pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "PRINT 1\n");
        for (int32_t i = 0; i < BAS_MAX_BLOCK_DEPTH + 8; i++) {
            pos += snprintf(deepSrc + pos, sizeof(deepSrc) - pos, "END IF\n");
        }
        expectCompileError("Deep block nesting rejected", deepSrc, "Statements nested too deeply");
    }

    // The signature prescan runs before TYPE statements are parsed; a
    // parameter typed AS a user TYPE must still count towards the arity.
    runProgram("Prescan handles UDT parameters",
        "DIM r AS INTEGER\n"
        "r = foo(1, 2)\n"
        "PRINT r\n"
        "TYPE Pt\n"
        "  x AS INTEGER\n"
        "END TYPE\n"
        "FUNCTION foo(a AS INTEGER, b AS INTEGER) AS INTEGER\n"
        "  foo = a + b\n"
        "END FUNCTION\n"
        "SUB bar(q AS Pt)\n"
        "  PRINT q.x\n"
        "END SUB\n",
        "3 \n"
    );

    expectCompileError("Duplicate label rejected",
        "L1:\n"
        "PRINT 1\n"
        "L1:\n"
        "PRINT 2\n",
        "Duplicate label 'L1'"
    );

    expectCompileError("Duplicate SUB rejected",
        "SUB S\n"
        "  PRINT \"first\"\n"
        "END SUB\n"
        "SUB S\n"
        "  PRINT \"second\"\n"
        "END SUB\n",
        "Sub 'S' is already defined"
    );

    expectCompileError("Nested SUB rejected",
        "SUB A\n"
        "  SUB B\n"
        "  END SUB\n"
        "END SUB\n",
        "SUB cannot be defined inside SUB or FUNCTION"
    );

    // PRINT#1 / CLOSE#1 / LINE INPUT#1 with no space before '#'
    runProgram("PRINT# without a space",
        "OPEN \"test_hash.txt\" FOR OUTPUT AS #1\n"
        "PRINT#1, \"hi\"\n"
        "CLOSE#1\n"
        "OPEN \"test_hash.txt\" FOR INPUT AS #1\n"
        "LINE INPUT#1, a$\n"
        "CLOSE#1\n"
        "PRINT a$\n"
        "KILL \"test_hash.txt\"\n",
        "hi\n"
    );

    expectCompileError("Integer literal wider than 32 bits rejected",
        "x = 4294967297\n",
        "Integer literal out of range"
    );

    expectCompileError("INTEGER suffix literal out of range rejected",
        "x = 70000%\n",
        "INTEGER literal out of range"
    );

    expectCompileError("Method arguments require commas",
        "ctl.Method 1 2\n",
        "Expected ','"
    );

    expectCompileError("Show modal flag must be a constant",
        "DIM m AS INTEGER\n"
        "frm.Show m\n",
        "Show modal flag must be a literal or CONST"
    );

    expectCompileError("DECLARE mismatch rejected",
        "DECLARE SUB S(a AS INTEGER)\n"
        "SUB S(a AS INTEGER, b AS INTEGER)\n"
        "END SUB\n",
        "DECLARE for 'S' does not match its definition"
    );

    runProgram("DECLARE matching the definition",
        "DECLARE SUB S(a AS INTEGER, b AS INTEGER)\n"
        "S 1, 2\n"
        "SUB S(a AS INTEGER, b AS INTEGER)\n"
        "  PRINT a + b\n"
        "END SUB\n",
        "3 \n"
    );

    expectCompileError("STRING * n out of range rejected",
        "DIM s AS STRING * &H10005\n",
        "STRING * length must be 1 to 65535"
    );

    expectCompileError("NEXT variable mismatch rejected",
        "FOR i = 1 TO 2\n"
        "FOR j = 1 TO 2\n"
        "NEXT i\n"
        "NEXT j\n",
        "NEXT i does not match FOR j"
    );

    expectCompileError("CALL of a variable rejected",
        "DIM v AS INTEGER\n"
        "CALL v\n",
        "'v' is not a SUB or FUNCTION"
    );

    expectCompileError("CALL without required arguments rejected",
        "SUB S(a)\n"
        "END SUB\n"
        "CALL S\n",
        "Sub 'S' expects 1 arguments, got 0"
    );

    runProgram("OPTIONAL padding on every call form",
        "SUB S(a, OPTIONAL b$)\n"
        "  PRINT a; \"[\"; b$; \"]\"\n"
        "END SUB\n"
        "CALL S(1)\n"
        "S 2, \"x\"\n"
        "CALL S(3, \"y\")\n",
        "1 []\n2 [x]\n3 [y]\n"
    );

    expectCompileError("Duplicate TYPE field rejected",
        "TYPE T\n"
        "  x AS INTEGER\n"
        "  x AS STRING\n"
        "END TYPE\n",
        "Duplicate field 'x' in TYPE 'T'"
    );

    expectCompileError("DEFINT reversed range rejected",
        "DEFINT Z-A\n",
        "Letter range must be in ascending order"
    );

    expectCompileError("CR-only line endings are counted",
        "PRINT 1\rPRINT 2\rPRINT )\r",
        "Line 3:"
    );

    expectCompileError("STATIC mangled name too long rejected",
        "SUB abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij\n"
        "  STATIC abcdefghijabcdefghijabcdefghij AS INTEGER\n"
        "END SUB\n",
        "STATIC name"
    );

    printf("All tests complete.  Failures: %d\n", (int)gFailCount);
    return gFailCount > 0 ? 1 : 0;
}
