Compiler: accept unnumbered programs, fix string concat in PRINT

Three fixes that lift seven test programs from skipped to passing,
bringing the AOT compiler harness from 56/56 to 63/63.

- Unnumbered programs (compiler_main.c): src/compiler_main.c skipped
  any line that didn't start with a digit, so direct-mode .bas files
  like hello.bas, math_ops.bas, string_ops.bas (no line numbers)
  failed with "No program lines found".  load_file now auto-assigns
  line numbers (last_num + 10) to unnumbered lines, with overflow
  protection at line 65520.

- String concatenation in PRINT (codegen.c): emit_str_atom had a
  broken concat loop that emitted "; _cat = gw_str_concat(&...
  _cat.sval ...)" — _cat was never declared, so any program with a
  string-literal concat in PRINT (like PRINT "ABC" + "DEF") failed
  to link.  Concat is properly handled by emit_str_expr's outer
  loop; remove the dead/broken code in the atom.  Fixes
  string_ops.bas.

- Transcendental result type (codegen.c): peek_expr_type returned
  VT_DBL for ATN/LOG/EXP/VAL, so PRINT formatted them with 15-digit
  double precision (e.g. 3.141592653589793) while real GW-BASIC and
  the interpreter format the single-precision result as 3.141593.
  Real GW-BASIC's transcendentals are single-precision; only CDBL
  forces double.  Demote ATN/LOG/EXP/VAL to VT_SNG; CDBL stays
  VT_DBL.  Fixes math_ops.bas.

Also: tests/run_compiler_tests.sh now runs the compiled binary from
the project root rather than the tempdir where it was built, so
test programs that reference tests/programs/ via relative paths
(chain_test, common_test, run_file, misc_stmts) resolve their
targets.  Earlier I'd misdiagnosed those failures as ON ERROR
divergence — they were just CWD-dependent path lookups.

Doc/test counts: 56 → 63 in README, docs/index.md, docs/development.md,
docs/roadmap.md.  Roadmap updated to note the compiler now accepts
unnumbered programs.
This commit is contained in:
Eremey Valetov
2026-05-04 16:32:09 -04:00
parent 99eb992ead
commit c317d683fb
7 changed files with 46 additions and 43 deletions
+2 -2
View File
@@ -55,7 +55,7 @@ to produce native executables linked against `libgwrt.a`.
Pipeline: `.bas` → tokenizer → analysis → C codegen → `gcc` → native binary.
56 of 56 eligible test programs compile via `gwbasic-compile` and produce
63 of 63 eligible test programs compile via `gwbasic-compile` and produce
output matching the interpreter's golden files. Run `bash
tests/run_compiler_tests.sh` to verify.
@@ -111,7 +111,7 @@ jupyter notebook # select "GW-BASIC 2026" kernel
## Tests
72 interpreter tests, 14 kernel tests, 56 compiler tests -- all passing.
72 interpreter tests, 14 kernel tests, 63 compiler tests -- all passing.
```bash
bash tests/run_tests.sh # interpreter
+2 -2
View File
@@ -26,8 +26,8 @@
72 automated test programs in `tests/programs/`, plus 4 classic interactive
programs in `tests/classic/` (Hamurabi, Lunar Lander, Gunner, Diamond from
David Ahl's *BASIC Computer Games*). 14 Jupyter kernel tests. 56 compiler
tests (eligible numbered programs compiled to native executables via
David Ahl's *BASIC Computer Games*). 14 Jupyter kernel tests. 63 compiler
tests (eligible programs compiled to native executables via
`gwbasic-compile`; run `bash tests/run_compiler_tests.sh`).
Run the full automated suite:
+1 -1
View File
@@ -28,7 +28,7 @@ version is structured as modular C suitable for new feature development.
- **Classic programs** -- Hamurabi, Lunar Lander, Gunner, and Diamond from
David Ahl's *BASIC Computer Games* (1978) run out of the box
- **Ahead-of-time compiler** -- `gwbasic-compile prog.bas -c` produces native
executables via C codegen + GCC (56/56 eligible tests pass, 100%)
executables via C codegen + GCC (63/63 eligible tests pass, 100%)
- **Jupyter kernel** -- inline Sixel graphics, INPUT support, Pygments syntax
highlighting; `pip install -e . && gwbasickernel-install --user`
- **72 test programs** with golden-file regression testing and DOSBox-X
+4 -4
View File
@@ -9,10 +9,10 @@ invokes GCC to produce native executables linked against `libgwrt.a`.
Pipeline: `.bas``gw_crunch()` → analysis pass → C codegen → `gcc` → native binary.
**56 of 56 eligible tests pass (100%)** via `tests/run_compiler_tests.sh`.
The harness skips chain/common multi-file flows, hardware-dependent tests
(graphics/sound/timer), and unnumbered direct-mode programs that the
compiler does not currently accept.
**63 of 63 eligible tests pass (100%)** via `tests/run_compiler_tests.sh`.
The harness only skips hardware-dependent tests (graphics/sound/timer)
and CHAIN/RUN target files that aren't standalone. The compiler now
accepts unnumbered direct-mode programs by auto-numbering them.
Language coverage:
+8 -10
View File
@@ -962,7 +962,9 @@ static void emit_str_atom(void)
skip_spaces();
uint8_t tok = cur();
/* String literal */
/* String literal. Concatenation is handled by emit_str_expr (the atom
* just emits the literal value); a previous attempt to handle '+' at
* the atom level emitted references to an undeclared `_cat` variable. */
if (tok == '"') {
EMIT("gw_str_from_cstr(\"");
tp++;
@@ -973,14 +975,6 @@ static void emit_str_atom(void)
}
if (*tp == '"') tp++;
EMIT("\")");
/* Handle concatenation */
skip_spaces();
while (cur() == TOK_PLUS) {
advance();
EMIT("; _cat = gw_str_concat(&(gw_value_t){.type=VT_STR,.sval=_cat.sval}, &(gw_value_t){.type=VT_STR,.sval=");
emit_str_expr();
EMIT("})");
}
return;
}
@@ -1305,9 +1299,13 @@ static gw_valtype_t peek_expr_type(void)
tp = save;
return arg_type;
}
case FUNC_VAL: case FUNC_ATN: case FUNC_LOG: case FUNC_EXP:
/* GW-BASIC's transcendentals are single-precision; only CDBL
* explicitly forces double. Promoting them to VT_DBL here makes
* PRINT use 15-digit format instead of the 7-digit single form. */
case FUNC_CDBL:
return VT_DBL;
case FUNC_VAL: case FUNC_ATN: case FUNC_LOG: case FUNC_EXP:
return VT_SNG;
default:
return VT_SNG;
}
+21 -5
View File
@@ -92,7 +92,10 @@ static int parse_line_num(const char *text, uint16_t *num)
return (int)(p - text);
}
/* Load a .bas file (ASCII format) */
/* Load a .bas file (ASCII format). Lines that don't begin with a number
* are treated as direct-mode statements and given auto-assigned numbers
* (10, 20, ...) so the compiler can handle scratchpad-style programs the
* interpreter accepts via stdin or `gwbasic file.bas`. */
static int load_file(const char *filename)
{
FILE *f = fopen(filename, "r");
@@ -103,6 +106,7 @@ static int load_file(const char *filename)
char buf[256];
uint8_t kbuf[256];
uint16_t last_num = 0;
while (fgets(buf, sizeof(buf), f)) {
int len = strlen(buf);
while (len > 0 && (buf[len-1] == '\n' || buf[len-1] == '\r'))
@@ -111,11 +115,23 @@ static int load_file(const char *filename)
uint16_t num;
int skip = parse_line_num(buf, &num);
if (skip == 0) continue; /* skip non-numbered lines */
const char *content;
if (skip == 0) {
/* Unnumbered direct-mode line: auto-assign next number. */
if (last_num >= 65520) {
fprintf(stderr, "Auto line numbering overflow in %s\n", filename);
fclose(f);
return 1;
}
num = last_num + 10;
content = buf;
while (*content == ' ') content++;
} else {
content = buf + skip;
while (*content == ' ') content++;
}
last_num = num;
/* Tokenize the line content (after the line number) */
const char *content = buf + skip;
while (*content == ' ') content++;
int tok_len = gw_crunch(content, kbuf, sizeof(kbuf));
store_line(num, kbuf, tok_len);
}
+8 -19
View File
@@ -20,20 +20,13 @@ if [ ! -f "$PROJECT_DIR/build/libgwrt.a" ]; then
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
# Programs not meaningful for the AOT path:
# - chain_target.bas / common_target.bas are not standalone (loaded via
# CHAIN, never run directly)
# - interactive / timing-dependent / hardware-output programs
SKIP=(
chain_target.bas
chain_test.bas
common_target.bas
common_test.bas
datetime.bas
on_timer.bas
timer_stop.bas
@@ -43,11 +36,6 @@ SKIP=(
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"
@@ -81,16 +69,17 @@ for bas in "$SCRIPT_DIR"/programs/*.bas; do
popd > /dev/null
continue
fi
popd > /dev/null
# Run from project root so test programs that reference tests/programs/
# (chain_test, common_test, run_file, misc_stmts) resolve relative paths.
actual=$(mktemp)
if ! timeout 5 "./$stem" > "$actual" 2>&1; then
if ! ( cd "$PROJECT_DIR" && timeout 5 "$WORK_DIR/$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