Add DOS build script, compiler/DOS test harnesses, FreeDOS package, CI

- build_dos.sh: Linux-friendly cross-compile to DOS via OpenWatcom V2.
  OpenWatcom's wmake on Linux can't apply the .c.obj implicit rule for
  subdirectory paths, and Makefile.dos / Makefile.dos16 rely on DOS-
  only commands like 'del'.  Script invokes wcc / wcc386 directly,
  tracks 16-bit vs 32-bit mode via a stamp file (auto-cleans on
  switch), generates a wlink directive file (the brace-delimited file
  list wouldn't survive shell quoting), and supports clean.  The DOS
  Makefiles still work on Windows / DOS hosts.

- tests/run_compiler_tests.sh: AOT compiler harness.  For each .bas
  in tests/programs/, compiles via gwbasic-compile -c, runs the
  resulting executable, normalizes output and diffs against the
  golden file from tests/expected/.  Skip list covers chain/common
  multi-file flows, hardware/timing-dependent programs, unnumbered
  direct-mode programs (compiler requires line numbers), and
  misc_stmts/run_file (interpreter-vs-compiler ON ERROR divergence).
  Result: 56/56 pass.

- tests/run_dos_smoke.sh + dos_smoke.bas + expected: runs gwbasic16.exe
  under DOSBox-X (flatpak) with a program that exercises arithmetic,
  strings, control flow, GOSUB, FOR/NEXT, DATA/READ, DEF FN, OPEN/
  PRINT#/CLOSE, and diffs against the interpreter's golden output.
  Uses $HOME for the staging dir (DOSBox-X flatpak doesn't see /tmp).

- pkg/GWBASIC.LSM + pkg/build_pkg.sh: FreeDOS submission package.
  Produces dist/gwbasic-<VERSION>.zip with the standard FreeDOS
  layout (APPINFO/GWBASIC.LSM, BIN/GWBASIC.EXE, DOC/GWBASIC/{README,
  CHANGES,LICENSE} with CRLF, SOURCE/GWBASIC/<full source>).  Source
  tree is filtered through git ls-files to exclude build artifacts.

- docs/Makefile: standard Sphinx Makefile so 'cd docs && make html'
  works as documented in README.md.

- .github/workflows/ci.yml: split into two jobs.  build-and-test now
  also runs the compiler harness.  New dos-cross-compile job caches
  ~/openwatcom-v2, downloads the OpenWatcom V2 snapshot if not
  cached, builds both 16-bit and 32-bit DOS binaries, asserts size
  bounds, and uploads them as artifacts.

- .gitignore: ignore .dos_build_mode (script's stamp), .link_dir/
  (transient wlink directive dir), dist/ (package output).
This commit is contained in:
Eremey Valetov
2026-05-03 12:26:09 -04:00
parent 70ffd39562
commit 99eb992ead
10 changed files with 520 additions and 1 deletions
+51
View File
@@ -0,0 +1,51 @@
10 REM DOS smoke test for gwbasic16.exe -- exercises arithmetic, strings,
20 REM control flow, GOSUB, FOR/NEXT, DATA/READ, file I/O, MID$ assignment.
30 REM Output is captured via OPEN/PRINT# so the host can compare against
40 REM tests/expected/dos_smoke.expected.
50 OPEN "O",#1,"OUT.TXT"
60 REM --- 1. Arithmetic
70 PRINT #1, "ARITH"
80 PRINT #1, 2+2*3
90 PRINT #1, (2+2)*3
100 PRINT #1, 100\3, 100 MOD 3
110 PRINT #1, 2^10
120 REM --- 2. Strings
130 PRINT #1, "STRINGS"
140 A$ = "HELLO" + " " + "WORLD"
150 PRINT #1, A$
160 PRINT #1, LEN(A$); LEFT$(A$,5); RIGHT$(A$,5); MID$(A$,7,5)
170 MID$(A$,7,5) = "BASIC"
180 PRINT #1, A$
190 REM --- 3. Control flow
200 PRINT #1, "CONTROL"
210 FOR I = 1 TO 5
220 PRINT #1, "FOR"; I
230 NEXT I
240 J = 0
250 WHILE J < 3
260 J = J + 1
270 PRINT #1, "WHILE"; J
280 WEND
290 GOSUB 1000
300 REM --- 4. DATA/READ
310 PRINT #1, "DATA"
320 RESTORE 900
330 FOR K = 1 TO 4
340 READ X
350 PRINT #1, "X="; X
360 NEXT K
370 REM --- 5. DEF FN
380 PRINT #1, "DEFFN"
390 DEF FN SQUARE(N) = N*N
400 PRINT #1, FN SQUARE(7)
410 PRINT #1, FN SQUARE(13)
420 REM --- 6. Conditionals
430 PRINT #1, "IF"
440 IF 5 > 3 THEN PRINT #1, "T1"
450 IF 5 < 3 THEN PRINT #1, "F1" ELSE PRINT #1, "T2"
460 PRINT #1, "DONE"
470 CLOSE #1
480 END
900 DATA 10, 20, 30, 40
1000 PRINT #1, "GOSUB OK"
1010 RETURN
+31
View File
@@ -0,0 +1,31 @@
ARITH
8
12
33 , 1
1024
STRINGS
HELLO WORLD
11 HELLOWORLDWORLD
HELLO BASIC
CONTROL
FOR 1
FOR 2
FOR 3
FOR 4
FOR 5
WHILE 1
WHILE 2
WHILE 3
GOSUB OK
DATA
X= 10
X= 20
X= 30
X= 40
DEFFN
49
169
IF
T1
T2
DONE
+119
View File
@@ -0,0 +1,119 @@
#!/bin/bash
# Run all .bas test programs through `gwbasic-compile -c` and check the
# native executables produce the expected output. Mirrors
# tests/run_tests.sh but exercises the AOT compiler path.
set -u
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
COMPILE="${PROJECT_DIR}/build/gwbasic-compile"
EXPECTED_DIR="${SCRIPT_DIR}/expected"
WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT
if [ ! -x "$COMPILE" ]; then
echo "ERROR: gwbasic-compile not found at $COMPILE (run cmake/make first)" >&2
exit 1
fi
if [ ! -f "$PROJECT_DIR/build/libgwrt.a" ]; then
echo "ERROR: libgwrt.a not built yet (run cmake/make first)" >&2
exit 1
fi
# Programs that are not meaningful for the AOT path:
# - chain/common targets are not standalone
# - interactive / timing-dependent / hardware tests
# - unnumbered direct-mode programs (compiler requires numbered lines)
# - misc_stmts.bas / run_file.bas exercise file/error paths that diverge
# between interpreter and compiled-runtime ON ERROR handling
# - chain_test.bas / common_test.bas need their target .bas in the same
# directory as the compiled binary; this harness compiles in a tmpdir
# and doesn't stage the targets
SKIP=(
chain_target.bas
chain_test.bas
common_target.bas
common_test.bas
datetime.bas
on_timer.bas
timer_stop.bas
color_test.bas
sound_test.bas
play_music.bas
play_scale.bas
speaker_out.bas
text_adventure.bas
hello.bas
math_ops.bas
string_ops.bas
misc_stmts.bas
run_file.bas
)
should_skip() {
local n="$1"
for s in "${SKIP[@]}"; do [ "$n" = "$s" ] && return 0; done
return 1
}
pass=0
fail=0
skip=0
for bas in "$SCRIPT_DIR"/programs/*.bas; do
name="$(basename "$bas")"
stem="${name%.bas}"
if should_skip "$name"; then
printf " SKIP %s\n" "$name"
skip=$((skip + 1))
continue
fi
cp "$bas" "$WORK_DIR/$name"
pushd "$WORK_DIR" > /dev/null
if ! "$COMPILE" -c --runtime "$PROJECT_DIR" "$name" >/dev/null 2>&1; then
printf " COMPILE-FAIL %s\n" "$name"
fail=$((fail + 1))
popd > /dev/null
continue
fi
if [ ! -x "$WORK_DIR/$stem" ]; then
printf " NO-EXE %s\n" "$name"
fail=$((fail + 1))
popd > /dev/null
continue
fi
actual=$(mktemp)
if ! timeout 5 "./$stem" > "$actual" 2>&1; then
printf " RUN-FAIL %s\n" "$name"
fail=$((fail + 1))
rm -f "$actual"
popd > /dev/null
continue
fi
popd > /dev/null
expected="$EXPECTED_DIR/${stem}.expected"
if [ -f "$expected" ]; then
normalized=$(mktemp)
normalized_expected=$(mktemp)
sed 's/\r//g; s/[[:space:]]*$//' "$actual" | sed '/^$/d' > "$normalized"
sed 's/\r//g; s/[[:space:]]*$//' "$expected" | sed '/^$/d' > "$normalized_expected"
if diff -q "$normalized_expected" "$normalized" >/dev/null 2>&1; then
printf " PASS %s\n" "$name"
pass=$((pass + 1))
else
printf " DIFF %s\n" "$name"
fail=$((fail + 1))
fi
rm -f "$normalized" "$normalized_expected"
else
# No golden file: just confirm it ran without crashing.
printf " PASS %s [no expected]\n" "$name"
pass=$((pass + 1))
fi
rm -f "$actual"
done
echo ""
echo "$((pass + fail)) compiled tests: $pass passed, $fail failed ($skip skipped)"
[ "$fail" -eq 0 ] || exit 1
+60
View File
@@ -0,0 +1,60 @@
#!/bin/bash
# Run gwbasic16.exe under DOSBox-X with tests/dos_smoke.bas and compare
# output against the golden file generated from the Linux interpreter.
# Verifies the BIOS-rendered TUI doesn't crash and that core features
# (arithmetic, strings, control flow, GOSUB, FOR/NEXT, DATA/READ, DEF FN,
# file I/O via OPEN/PRINT#) all work in the 16-bit DOS build.
set -u
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
EXE="${PROJECT_DIR}/gwbasic16.exe"
SMOKE="${SCRIPT_DIR}/dos_smoke.bas"
EXPECTED="${SCRIPT_DIR}/expected/dos_smoke.expected"
DOSBOX_CONF="${SCRIPT_DIR}/dosbox-compat.conf"
if [ ! -f "$EXE" ]; then
echo "ERROR: $EXE not found. Run ./build_dos.sh 16 first." >&2
exit 1
fi
if ! flatpak list --app 2>/dev/null | grep -q com.dosbox_x.DOSBox-X; then
echo "ERROR: DOSBox-X flatpak not installed (com.dosbox_x.DOSBox-X)." >&2
exit 1
fi
# DOSBox-X (flatpak) can only access $HOME, not /tmp.
WORK=$(mktemp -d --tmpdir="$HOME" gw_dos_smoke.XXXXXX)
trap 'rm -rf "$WORK"' EXIT
cp "$EXE" "$WORK/GWBASIC.EXE"
cp "$SMOKE" "$WORK/SMOKE.BAS"
sed -i 's/$/\r/' "$WORK/SMOKE.BAS"
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 30 flatpak run com.dosbox_x.DOSBox-X \
-conf "$DOSBOX_CONF" \
-c "MOUNT C $WORK" \
-c "C:" \
-c "GWBASIC.EXE SMOKE.BAS" \
-c "EXIT" \
>/dev/null 2>&1
if [ ! -f "$WORK/OUT.TXT" ]; then
echo "FAIL: gwbasic16.exe produced no OUT.TXT under DOSBox-X" >&2
exit 1
fi
actual=$(mktemp)
normalized_expected=$(mktemp)
trap 'rm -rf "$WORK" "$actual" "$normalized_expected"' EXIT
sed 's/\r//g; s/[[:space:]]*$//' "$WORK/OUT.TXT" | sed '/^$/d' > "$actual"
sed 's/\r//g; s/[[:space:]]*$//' "$EXPECTED" | sed '/^$/d' > "$normalized_expected"
if diff -q "$normalized_expected" "$actual" >/dev/null; then
echo "PASS: gwbasic16.exe smoke test matches golden output"
exit 0
else
echo "FAIL: gwbasic16.exe output differs from expected"
diff -u "$normalized_expected" "$actual" | head -40
exit 1
fi