Files
gw-basic-2026/docs/getting-started.md
Eremey Valetov cabd93caea Release 0.18.0 + automated FreeDOS QEMU smoke (#2)
* tests: automated headless FreeDOS QEMU smoke

Fully-automated counterpart to the manual run_freedos_qemu.sh: overlays the
FreeDOS image (no mutation), stages the interpreter and a SYSTEM-terminated
smoke on C:, injects the run plus poweroff into the image's startup batch,
boots headless, and diffs OUT.TXT against the golden file. Local-dev only
(needs qemu, a FreeDOS qcow2, mtools, nbd, and passwordless sudo); CI keeps
using the DOSBox-X path. Exercises the binary on a real FreeDOS install
rather than DOSBox-X emulation.

* Release 0.18.0

Cross-language linking (link BASIC into C/Fortran, call C from BASIC via
'$EXTERN), the v0.18 codegen/perf batch (paren string-comparison fix,
--no-gc-check/--fast-math, larger 32-bit caps, process-local DATE$/TIME$),
and the automated FreeDOS QEMU smoke. Bumps GW_VERSION and updates the
banners, CHANGES.TXT, and the development history table.

---------

Co-authored-by: Eremey Valetov <evvaletov@users.noreply.github.com>
2026-06-13 15:37:45 +03:00

11 KiB

Getting Started

Dependencies

  • C11 compiler (GCC or Clang)
  • CMake 3.10+
  • PulseAudio development library (libpulse-simple) -- optional, for SOUND/BEEP/PLAY

On Debian/Ubuntu:

sudo apt-get install build-essential cmake libpulse-dev

On Fedora/RHEL:

sudo dnf install gcc cmake pulseaudio-libs-devel

Building

git clone https://github.com/evvaletov/gw-basic-2026.git
cd gw-basic-2026
mkdir -p build && cd build
cmake .. && make

The binary is build/gwbasic.

Usage

Interactive Mode

Running ./gwbasic with no arguments launches the full-screen editor:

$ ./gwbasic
GW-BASIC 2026 0.18.0
(C) Eremey Valetov 2026. MIT License.
Based on Microsoft GW-BASIC assembly source.
Ok
PRINT 2+2
 4
Ok
FOR I=1 TO 5:PRINT I;:NEXT
 1  2  3  4  5
Ok

Use arrow keys to move the cursor freely. Press Enter on any screen line to re-enter it. F1-F10 insert common commands (F2 runs the program).

Running a Program File

./gwbasic tests/programs/prime_sieve.bas

Piped Input

echo '10 FOR I=1 TO 10:PRINT I*I;:NEXT' | ./gwbasic

Direct Mode Expressions

Type expressions and statements at the Ok prompt:

PRINT SIN(3.14159/2)
 1
A$="HELLO WORLD":MID$(A$,7,5)="BASIC":PRINT A$
HELLO BASIC

Command-Line Options

Usage: gwbasic [options] [file.bas]
Options:
  -f, --full         Use full terminal size (default: 25x80)
  -h, --help         Show this help
  --lpt DEVICE|FILE  Printer output destination (default: LPT1.TXT)
                     Use LPT1 or /dev/lp0 for real hardware
  -v, --version      Show version

Ahead-of-Time Compiler

gwbasic-compile translates .bas programs to C source, then optionally invokes GCC to produce native executables linked against libgwrt.a.

Basic Usage

# Emit C source to stdout
build/gwbasic-compile program.bas

# Compile to native executable
build/gwbasic-compile -c --runtime . program.bas

Both numbered (10 PRINT "HI") and unnumbered (PRINT "HI") sources compile. Unnumbered lines get auto-assigned numbers (10, 20, 30, ...) so the analysis pass and codegen can produce labeled statements; explicit line numbers are preserved. Direct-mode scratchpad scripts and classic "just a list of statements" programs compile without manual renumbering.

Compiler Options

Usage: gwbasic-compile [options] input.bas
Options:
  -o FILE          Output C source file (default: stdout)
  -c               Compile to executable (invoke gcc)
  -O LEVEL         GCC optimization level (default: 2)
  --keep-c         Keep generated C file (with -c)
  --runtime DIR    Path to runtime headers/library
  --warn           Static analysis warnings
  --safe           Runtime safety checks (implies --warn)
  --safe=sanitize  Above + address/UB sanitizers (with -c)
  --no-gc-check    Skip per-line gwrt_check_line() (no GC, no Break)
  --fast-math      Skip division-by-zero checks

Performance Flags (--no-gc-check / --fast-math)

--no-gc-check skips the gwrt_check_line() call emitted at the start of every non-REM line. That call drives the string-pool compacting GC and the Ctrl+Break trap. Removing it gives a small per-line speedup for programs that don't allocate strings or need responsive interruption. String reassignment can still trigger compaction lazily, but the guaranteed periodic check is gone.

--fast-math removes the explicit divide-by-zero check around the / operator. The result of X = 10 / 0 becomes inf rather than raising "Division by zero". Useful for compute-bound code that already validates inputs.

Memory Safety (--warn / --safe)

The --warn flag enables compile-time static analysis warnings:

  • Uninitialized variables -- variables used before their first assignment (via LET, FOR, READ, INPUT)
  • GOTO/GOSUB to nonexistent line -- jump targets that don't exist in the program
  • Unreachable code -- lines after unconditional GOTO/END/STOP that are not jump targets

The --safe flag (implies --warn) adds runtime safety checks to the generated C:

  • Integer overflow detection -- arithmetic on integer (%) variables uses checked functions (gw_int_add, gw_int_sub, gw_int_mul) that raise "Overflow" instead of silently wrapping, matching real GW-BASIC behavior
  • Enhanced array diagnostics -- subscript errors report the array name, subscript value, line number, and which dimension exceeded its bound
  • GOSUB stack diagnostics -- stack overflow reports the source line and current depth

The --safe=sanitize flag (with -c) additionally passes -fsanitize=address,undefined to GCC for full memory error detection.

# Warnings only (zero runtime cost)
build/gwbasic-compile --warn program.bas

# Runtime safety checks
build/gwbasic-compile --safe -c --runtime . program.bas

# Full sanitizer build (debugging)
build/gwbasic-compile --safe=sanitize -c --runtime . program.bas

Cross-Language Linking (--emit-obj / --main-name)

--emit-obj produces a .o object file instead of a final executable; --main-name NAME renames the entry point so it doesn't collide with the host project's main(). Together they let you link BASIC into a larger C or Fortran build.

# BASIC source compiled to greet.o with renamed entry point
build/gwbasic-compile --emit-obj --main-name=run_basic_greet \
    --runtime . greet.bas

C driver:

extern int run_basic_greet(int argc, char **argv);

int main(void) {
    run_basic_greet(0, NULL);  /* runs the BASIC program */
    return 0;
}

Link both together:

gcc driver.c greet.o -L./build -lgwrt -lm -lpthread -lpulse-simple

Fortran driver (modern, with iso_c_binding):

program main
  use iso_c_binding
  interface
    function run_basic_greet(argc, argv) bind(c, name="run_basic_greet")
      use iso_c_binding
      integer(c_int), value :: argc
      type(c_ptr), value :: argv
      integer(c_int) :: run_basic_greet
    end function
  end interface
  integer(c_int) :: rc
  rc = run_basic_greet(0, c_null_ptr)
end program
gfortran driver.f90 greet.o -L./build -lgwrt -lm -lpthread -lpulse-simple

The BASIC code shares the gw interpreter state with libgwrt, so a single binary runs at most one BASIC program at a time. Calling BASIC from C / Fortran is always safe; calling C / Fortran from BASIC uses the '$EXTERN pragma described next.

Foreign Functions from BASIC ('$EXTERN)

A '$EXTERN pragma declares a C function that compiled BASIC can call directly. It is written as an apostrophe comment, so the interpreter ignores it while the compiler picks it up:

10 '$EXTERN Cmul(DOUBLE, DOUBLE) AS DOUBLE
20 '$EXTERN Greet(STRING) AS STRING
30 '$EXTERN Getn AS INTEGER
40 PRINT Cmul(2.5, 4)
50 PRINT Greet("World")
60 PRINT Getn

Type mapping at the boundary:

BASIC type C type
INTEGER int16_t
SINGLE float
DOUBLE double
STRING const char * (NUL-terminated)

The C side supplies the symbols at link time:

#include <stdint.h>
int16_t     Getn(void)                  { return 42; }
double      Cmul(double a, double b)    { return a * b; }
const char *Greet(const char *who) {
    static char buf[128];
    snprintf(buf, sizeof buf, "Hello, %s!", who);
    return buf;   /* callee owns the buffer; BASIC copies it */
}

Build and link as in the --emit-obj example above:

build/gwbasic-compile --emit-obj --runtime . demo.bas   # -> demo.o
gcc -c lib.c -o lib.o
gcc demo.o lib.o -L./build -lgwrt -lm -lpthread -lpulse-simple -o demo

Notes and constraints:

  • The function name is matched case-insensitively at the call site (BASIC convention) but emitted as the C symbol with the case written in the pragma, so Cmul calls C's Cmul, not cmul.
  • Names must be BASIC-legal identifiers (letters and digits) because the call site is tokenized as ordinary BASIC. To call a C function whose name contains underscores or other characters (e.g. sqlite3_open), write a thin C wrapper with a BASIC-legal name.
  • String arguments cross as const char * (the compiler converts and frees a temporary copy); a STRING return value is copied into the BASIC string pool and the callee retains ownership of its own buffer.
  • For Fortran callees, declare the routine bind(c) with a matching name.

Building for DOS / FreeDOS

GW-BASIC 2026 cross-compiles to DOS using OpenWatcom V2 (wcc / wcc386). Two targets are available:

Produces a standalone 128KB MZ executable -- no DOS extender required.

wmake -f Makefile.dos16

Requires OpenWatcom V2 with 16-bit DOS target. Uses MEDIUM memory model (-mm): code can exceed 64KB, data must fit in 64KB.

32-bit DOS/4GW

Produces a 175KB LE executable requiring DOS4GW.EXE (265KB) at runtime. Also builds the compiler (GWBASCOM.EXE) and runtime library (GWRT.LIB).

wmake -f Makefile.dos

Running on FreeDOS

Copy GWBASIC.EXE (and DOS4GW.EXE for the 32-bit build) to your FreeDOS system. Run programs from the command line:

C:\> GWBASIC PROGRAM.BAS

Running without arguments launches the interactive editor. The TUI renders through BIOS INT 10h with the screen buffer in far memory, so the full-screen editor, F-key bar, cursor positioning, and scrolling all work on bare FreeDOS without ANSI.SYS.

Verifying the DOS Build

Two automated checks run from a Linux host:

./build_dos.sh 16            # produces gwbasic16.exe (~128KB)
./build_dos.sh 32            # produces gwbasic.exe (~175KB)
bash tests/run_dos_smoke.sh  # runs gwbasic16.exe under DOSBox-X, diffs golden

The smoke harness validates non-interactive features (arithmetic, strings, control flow, GOSUB, FOR/NEXT, DATA/READ, DEF FN, file I/O via OPEN/PRINT#). The interactive TUI features below need a manual session under DOSBox-X or real FreeDOS:

Check What to do Expected
TUI startup Launch GWBASIC.EXE with no arguments Ok prompt, F-key bar at row 25 (1LIST 2RUN ... in inverse video)
Cursor keys Press up/down/left/right Cursor moves freely without printing characters
Re-enter line Type 10 PRINT "HI", Enter; arrow up to that line, Enter Line re-tokenized; subsequent LIST shows it stored
F1 (LIST) Press F1 then Enter Inserts LIST , runs LIST
F2 (RUN) Type a program, press F2 Runs it (RUN\r is appended)
Insert toggle Press Ins; type characters mid-line Cursor switches between block (insert) and underline (overwrite) shapes; characters insert vs overstrike accordingly
Home / End Press Home, End Cursor jumps to column 0 / past last printable char on the row
Scroll Fill the screen with output Bottom row pinned to the F-key bar; new lines push old ones up
Ctrl-C Run 10 GOTO 10 and press Ctrl-C Program stops with Break in 10
KEY OFF / KEY ON KEY OFF then KEY ON F-key bar disappears / reappears
CLS CLS Screen clears, cursor at top-left
Exit SYSTEM Returns to DOS prompt cleanly (no leftover escape codes)