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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user