From c317d683fbe4d0300bdafcb5316bd0af1e97d7e4 Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Mon, 4 May 2026 16:32:09 -0400 Subject: [PATCH] Compiler: accept unnumbered programs, fix string concat in PRINT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 4 ++-- docs/development.md | 4 ++-- docs/index.md | 2 +- docs/roadmap.md | 8 ++++---- src/codegen.c | 18 ++++++++---------- src/compiler_main.c | 26 +++++++++++++++++++++----- tests/run_compiler_tests.sh | 27 ++++++++------------------- 7 files changed, 46 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 241e11e..a1ded94 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/development.md b/docs/development.md index b295332..ec9fa3e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -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: diff --git a/docs/index.md b/docs/index.md index a622ce9..911e2b4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/docs/roadmap.md b/docs/roadmap.md index 1948324..7501fc0 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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: diff --git a/src/codegen.c b/src/codegen.c index 4281e73..e6fd455 100644 --- a/src/codegen.c +++ b/src/codegen.c @@ -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; } diff --git a/src/compiler_main.c b/src/compiler_main.c index 2fbc8a7..a9c6950 100644 --- a/src/compiler_main.c +++ b/src/compiler_main.c @@ -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); } diff --git a/tests/run_compiler_tests.sh b/tests/run_compiler_tests.sh index 5bdfb8e..2888165 100755 --- a/tests/run_compiler_tests.sh +++ b/tests/run_compiler_tests.sh @@ -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