From df5c3083b17666b6c73897e47fc176fbd91f5201 Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Tue, 10 Feb 2026 15:53:57 -0500 Subject: [PATCH] Add README, implement KILL/NAME, use relative paths in tests Add README.md for public release. Implement KILL and NAME statements for file management. Change test programs to use relative paths instead of /tmp/ and clean up temp files with KILL. Update .gitignore for test artifacts. --- .gitignore | 4 + README.md | 127 +++++++++++++++++++++++++++++++ src/interp.c | 30 ++++++++ tests/programs/file_io.bas | 5 +- tests/programs/random_access.bas | 3 +- tests/programs/save_load.bas | 3 +- tests/programs/write_input.bas | 5 +- 7 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 README.md diff --git a/.gitignore b/.gitignore index e3f78a4..55b33cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ build/ *.o *.d +gwbasic_*.txt +gwbasic_*.dat +gwbasic_*.bas +!tests/programs/*.bas diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0366a9 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# gwbasic-c + +A portable C reimplementation of Microsoft GW-BASIC, using the +[original 8088 assembly source](https://github.com/microsoft/GW-BASIC) +(released by Microsoft in 2020) as the authoritative reference. + +This is not a transpilation — it reimplements the algorithms in clean C11 +with modern data structures while targeting bug-compatible behavior. + +## Building + +```bash +mkdir -p build && cd build +cmake .. && make +``` + +Requires a C11 compiler and CMake 3.10+. No other dependencies. + +## Usage + +Interactive mode: + +``` +$ ./gwbasic +GW-BASIC 0.4.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 +``` + +Run a program file: + +```bash +./gwbasic examples/prime_sieve.bas +``` + +Pipe input: + +```bash +echo '10 FOR I=1 TO 10:PRINT I*I;:NEXT' | ./gwbasic +``` + +## What Works + +**Data types:** INTEGER (%), SINGLE (!), DOUBLE (#), STRING ($) + +**Operators:** `+ - * / ^ \ MOD AND OR XOR EQV IMP NOT < = > <= >= <>` + +**Numeric functions:** SGN, INT, ABS, SQR, SIN, COS, TAN, ATN, LOG, EXP, +RND, FIX, CINT, CSNG, CDBL + +**String functions:** LEN, ASC, CHR$, VAL, STR$, LEFT$, RIGHT$, MID$, +SPACE$, STRING$, HEX$, OCT$, INSTR + +**Statements:** + +| Category | Statements | +|----------|------------| +| Output | PRINT, LPRINT, PRINT USING, WRITE, CLS | +| Variables | LET, DIM, ERASE, SWAP, DEFINT/SNG/DBL/STR | +| Control flow | GOTO, GOSUB/RETURN, FOR/NEXT, IF/THEN/ELSE, WHILE/WEND, ON...GOTO/GOSUB | +| Input | INPUT, LINE INPUT, DATA/READ/RESTORE | +| Program | RUN, RUN "file", CONT, STOP, END, NEW, LIST, CLEAR | +| Sequential I/O | OPEN, CLOSE, PRINT#, WRITE#, INPUT#, LINE INPUT# | +| Random-access I/O | FIELD, LSET, RSET, PUT, GET, CVI/CVS/CVD, MKI$/MKS$/MKD$ | +| Program I/O | SAVE, LOAD, MERGE, CHAIN | +| Error handling | ON ERROR GOTO, RESUME, ERROR | +| User functions | DEF FN, RANDOMIZE | +| File management | KILL, NAME | +| Screen | LOCATE, COLOR, WIDTH | + +Graphics statements (SCREEN, PSET, LINE, CIRCLE, DRAW, PAINT, etc.) are +parsed but produce no output — programs that use them won't crash. + +## Architecture + +The interpreter follows the original GW-BASIC's internal structure: + +``` +Source text → Tokenizer (CRUNCH) → Token stream + ↓ + Expression evaluator (FRMEVL) + ↓ + Statement dispatcher (NEWSTT) + ↓ + HAL (platform I/O) +``` + +| Module | File | Original | +|--------|------|----------| +| Tokenizer | tokenizer.c | GWMAIN.ASM | +| Evaluator | eval.c | GWEVAL.ASM | +| Interpreter | interp.c | BINTRP.ASM | +| Tokens | tokens.c | IBMRES.ASM | +| Errors | error.c | GWDATA.ASM | +| Math | math_int.c, math_float.c, math_transcend.c | MATH1/2.ASM | +| Strings | strings.c | BISTRS.ASM | +| File I/O | fileio.c | BIPTRG.ASM | +| PRINT USING | print_using.c | BIPRTU.ASM | +| Platform | hal_posix.c | OEM*.ASM | + +Key design differences from the original: +- IEEE 754 floating point (MBF conversion only for CVI/CVS/CVD compatibility) +- Dynamic memory allocation instead of 64KB segment layout +- malloc'd strings instead of compacting garbage collector +- setjmp/longjmp for error recovery + +## Tests + +27 test programs in `tests/programs/`: + +```bash +for f in tests/programs/*.bas; do + echo "$(basename $f):" + timeout 5 ./build/gwbasic "$f" +done +``` + +## License + +MIT License. See [LICENSE](LICENSE). diff --git a/src/interp.c b/src/interp.c index 9a55957..0e7b53c 100644 --- a/src/interp.c +++ b/src/interp.c @@ -732,6 +732,36 @@ void gw_exec_stmt(void) gw_stmt_get(); return; } + if (xstmt == XSTMT_KILL) { + gw_chrget(); + gw_value_t fname = gw_eval_str(); + char *path = gw_str_to_cstr(&fname.sval); + gw_str_free(&fname.sval); + if (remove(path) != 0) { free(path); gw_error(ERR_FF); } + free(path); + return; + } + if (xstmt == XSTMT_NAME) { + gw_chrget(); + gw_value_t old = gw_eval_str(); + char *oldpath = gw_str_to_cstr(&old.sval); + gw_str_free(&old.sval); + gw_skip_spaces(); + /* Skip AS */ + if (gw_is_letter(gw_chrgot()) && toupper(gw_chrgot()) == 'A') { + gw_chrget(); + if (gw_is_letter(gw_chrgot()) && toupper(gw_chrgot()) == 'S') + gw_chrget(); + } + gw_value_t new_val = gw_eval_str(); + char *newpath = gw_str_to_cstr(&new_val.sval); + gw_str_free(&new_val.sval); + if (rename(oldpath, newpath) != 0) { + free(oldpath); free(newpath); gw_error(ERR_FF); + } + free(oldpath); free(newpath); + return; + } /* Graphics/sound stubs - parse and discard arguments */ if (xstmt == XSTMT_CIRCLE || xstmt == XSTMT_DRAW || xstmt == XSTMT_PAINT || xstmt == XSTMT_PLAY || diff --git a/tests/programs/file_io.bas b/tests/programs/file_io.bas index 86cb4dd..47ba390 100644 --- a/tests/programs/file_io.bas +++ b/tests/programs/file_io.bas @@ -1,11 +1,12 @@ 10 REM File I/O test -20 OPEN "/tmp/gwbasic_fio_test.txt" FOR OUTPUT AS #1 +20 OPEN "gwbasic_fio_test.txt" FOR OUTPUT AS #1 30 PRINT #1, "Hello from GW-BASIC" 40 PRINT #1, "Second line" 50 CLOSE #1 -60 OPEN "/tmp/gwbasic_fio_test.txt" FOR INPUT AS #1 +60 OPEN "gwbasic_fio_test.txt" FOR INPUT AS #1 70 LINE INPUT #1, A$ 80 LINE INPUT #1, B$ 90 CLOSE #1 100 PRINT A$ 110 PRINT B$ +120 KILL "gwbasic_fio_test.txt" diff --git a/tests/programs/random_access.bas b/tests/programs/random_access.bas index 11d8e2f..37663d5 100644 --- a/tests/programs/random_access.bas +++ b/tests/programs/random_access.bas @@ -1,5 +1,5 @@ 10 REM Random-access file I/O test -20 OPEN "R", #1, "/tmp/gwbasic_random.dat", 32 +20 OPEN "R", #1, "gwbasic_random.dat", 32 30 FIELD #1, 20 AS N$, 4 AS A$, 8 AS S$ 40 LSET N$ = "Alice" 50 LSET A$ = MKI$(25) + " " @@ -14,3 +14,4 @@ 140 GET #1, 2 150 PRINT LEFT$(N$, 3); CVI(A$) 160 CLOSE #1 +170 KILL "gwbasic_random.dat" diff --git a/tests/programs/save_load.bas b/tests/programs/save_load.bas index ee18360..e406a66 100644 --- a/tests/programs/save_load.bas +++ b/tests/programs/save_load.bas @@ -1,3 +1,4 @@ 10 REM SAVE test 20 PRINT "Program saved" -30 SAVE "/tmp/gwbasic_save_test.bas" +30 SAVE "gwbasic_save_test.bas" +40 KILL "gwbasic_save_test.bas" diff --git a/tests/programs/write_input.bas b/tests/programs/write_input.bas index bde67ef..f7be754 100644 --- a/tests/programs/write_input.bas +++ b/tests/programs/write_input.bas @@ -1,11 +1,12 @@ 10 REM WRITE#/INPUT# and EOF test -20 OPEN "/tmp/gwbasic_wi_test.txt" FOR OUTPUT AS #1 +20 OPEN "gwbasic_wi_test.txt" FOR OUTPUT AS #1 30 WRITE #1, "Alice", 25 40 WRITE #1, "Bob", 30 50 CLOSE #1 -60 OPEN "/tmp/gwbasic_wi_test.txt" FOR INPUT AS #1 +60 OPEN "gwbasic_wi_test.txt" FOR INPUT AS #1 70 WHILE NOT EOF(1) 80 INPUT #1, N$, A 90 PRINT N$; A 100 WEND 110 CLOSE #1 +120 KILL "gwbasic_wi_test.txt"