diff --git a/CMakeLists.txt b/CMakeLists.txt index 29f9348..9476bf4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,9 @@ project(gw-basic-2026 C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) +# POSIX extensions (opendir, chdir, strcasecmp, etc.) +add_compile_definitions(_DEFAULT_SOURCE) + # Warnings add_compile_options(-Wall -Wextra -Wno-unused-parameter) diff --git a/README.md b/README.md index 9aab982..bd99f32 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Interactive mode launches the authentic GW-BASIC full-screen editor: ``` $ ./gwbasic -GW-BASIC 2026 0.5.0 +GW-BASIC 2026 0.6.0 (C) Eremey Valetov 2026. MIT License. Based on Microsoft GW-BASIC assembly source. Ok @@ -73,7 +73,8 @@ SPACE$, STRING$, HEX$, OCT$, INSTR, INPUT$ | Program I/O | SAVE, LOAD, MERGE, CHAIN | | Error handling | ON ERROR GOTO, RESUME, ERROR, ERR, ERL | | User functions | DEF FN, RANDOMIZE | -| File management | KILL, NAME | +| File management | KILL, NAME, FILES, MKDIR, RMDIR, CHDIR, SHELL | +| Date/time | DATE$, TIME$, TIMER | | Screen | LOCATE, COLOR, WIDTH, SCREEN, KEY ON/OFF/LIST | | Graphics | PSET, PRESET, LINE, CIRCLE, DRAW, PAINT | | Sound | SOUND, BEEP, PLAY (MML) | @@ -145,7 +146,7 @@ Key design differences from the original: ## Tests -50 test programs in `tests/programs/`, with CI via GitHub Actions: +52 test programs in `tests/programs/`, with CI via GitHub Actions: ```bash bash tests/run_tests.sh diff --git a/include/error.h b/include/error.h index 1d10c89..8125f2f 100644 --- a/include/error.h +++ b/include/error.h @@ -49,6 +49,8 @@ #define ERR_NM 65 /* Bad file name */ #define ERR_DS 67 /* Direct statement in file */ #define ERR_TF 68 /* Too many files */ +#define ERR_DA 70 /* Disk already exists */ +#define ERR_PE 76 /* Path not found */ const char *gw_error_msg(int errnum); void gw_error(int errnum); diff --git a/include/gwbasic.h b/include/gwbasic.h index 0ff53c6..aa4ed47 100644 --- a/include/gwbasic.h +++ b/include/gwbasic.h @@ -10,7 +10,7 @@ #include "gw_math.h" #include "strings.h" -#define GW_VERSION "0.5.0" +#define GW_VERSION "0.6.0" #define GW_BANNER "GW-BASIC " GW_VERSION /* Tokenizer */ diff --git a/src/error.c b/src/error.c index d4e6c1c..6280f79 100644 --- a/src/error.c +++ b/src/error.c @@ -52,6 +52,8 @@ static const struct { int num; const char *msg; } error_table[] = { { 65, "Bad file name" }, { 67, "Direct statement in file" }, { 68, "Too many files" }, + { 70, "Disk already exists" }, + { 76, "Path not found" }, { 0, NULL } }; diff --git a/src/eval.c b/src/eval.c index 86962bd..8f83420 100644 --- a/src/eval.c +++ b/src/eval.c @@ -4,6 +4,7 @@ #include #include #include +#include /* * Expression evaluator - reimplements GWEVAL.ASM's FRMEVL. @@ -919,7 +920,7 @@ static gw_value_t eval_atom(void) gw.text_ptr = save; } - /* Extended statement tokens that work as functions (DATE$, TIME$) */ + /* Extended statement tokens that work as functions (DATE$, TIME$, TIMER) */ if (tok == TOK_PREFIX_FE) { uint8_t *save = gw.text_ptr; gw_chrget(); @@ -928,13 +929,29 @@ static gw_value_t eval_atom(void) gw_chrget(); gw_value_t v; v.type = VT_STR; + char tbuf[16]; + time_t now = time(NULL); + struct tm *tm = localtime(&now); if (xtok == XSTMT_DATE) { - v.sval = gw_str_from_cstr("01-01-2026"); + snprintf(tbuf, sizeof(tbuf), "%02d-%02d-%04d", + tm->tm_mon + 1, tm->tm_mday, tm->tm_year + 1900); + v.sval = gw_str_from_cstr(tbuf); } else { - v.sval = gw_str_from_cstr("00:00:00"); + snprintf(tbuf, sizeof(tbuf), "%02d:%02d:%02d", + tm->tm_hour, tm->tm_min, tm->tm_sec); + v.sval = gw_str_from_cstr(tbuf); } return v; } + if (xtok == XSTMT_TIMER) { + gw_chrget(); + time_t now = time(NULL); + struct tm *tm = localtime(&now); + gw_value_t v; + v.type = VT_SNG; + v.fval = (float)(tm->tm_hour * 3600 + tm->tm_min * 60 + tm->tm_sec); + return v; + } gw.text_ptr = save; } diff --git a/src/interp.c b/src/interp.c index 04a5145..78d9599 100644 --- a/src/interp.c +++ b/src/interp.c @@ -7,6 +7,10 @@ #include #include #include +#include +#include +#include +#include /* * Execution loop and control flow - the heart of the interpreter. @@ -15,6 +19,14 @@ jmp_buf gw_run_jmp; +static int ci_strcmp(const char *a, const char *b) +{ + for (;; a++, b++) { + int d = tolower((unsigned char)*a) - tolower((unsigned char)*b); + if (d != 0 || !*a) return d; + } +} + /* ================================================================ * Program Storage * ================================================================ */ @@ -873,6 +885,124 @@ void gw_exec_stmt(void) free(cmd); return; } + /* FILES [filespec$] */ + if (xstmt == XSTMT_FILES) { + gw_chrget(); + gw_skip_spaces(); + char *pattern = NULL; + if (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE) { + gw_value_t v = gw_eval_str(); + pattern = gw_str_to_cstr(&v.sval); + gw_str_free(&v.sval); + } + const char *dir = "."; + char dirpath[256] = "."; + const char *wild = NULL; + if (pattern) { + /* Split "dir/pattern" into directory and wildcard */ + char *sep = strrchr(pattern, '/'); + if (sep) { + *sep = '\0'; + snprintf(dirpath, sizeof(dirpath), "%s", pattern); + dir = dirpath; + wild = sep + 1; + } else { + wild = pattern; + } + if (wild && !*wild) wild = NULL; + } + DIR *dp = opendir(dir); + if (!dp) { free(pattern); gw_error(ERR_PE); } + struct dirent *ent; + int col = 0; + while ((ent = readdir(dp)) != NULL) { + if (ent->d_name[0] == '.') continue; + if (wild) { + /* Simple *.ext matching: if wild starts with *. check extension */ + if (wild[0] == '*' && wild[1] == '.') { + const char *ext = strrchr(ent->d_name, '.'); + if (!ext || ci_strcmp(ext + 1, wild + 2) != 0) continue; + } else if (strcmp(wild, "*.*") != 0 && strcmp(wild, "*") != 0) { + /* Exact match */ + if (ci_strcmp(ent->d_name, wild) != 0) continue; + } + } + char entry[270]; + snprintf(entry, sizeof(entry), "%-14s", ent->d_name); + if (gw_hal) gw_hal->puts(entry); + else fputs(entry, stdout); + col += 14; + if (col >= 70) { + if (gw_hal) gw_hal->puts("\n"); + else fputs("\n", stdout); + col = 0; + } + } + if (col > 0) { + if (gw_hal) gw_hal->puts("\n"); + else fputs("\n", stdout); + } + closedir(dp); + free(pattern); + return; + } + /* SHELL [command$] */ + if (xstmt == XSTMT_SHELL) { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE) { + gw_value_t v = gw_eval_str(); + char *cmd = gw_str_to_cstr(&v.sval); + gw_str_free(&v.sval); + int rc = system(cmd); + free(cmd); + (void)rc; + } else { + const char *sh = getenv("SHELL"); + if (!sh) sh = "/bin/sh"; + int rc = system(sh); + (void)rc; + } + return; + } + /* CHDIR path$ */ + if (xstmt == XSTMT_CHDIR) { + gw_chrget(); + gw_value_t v = gw_eval_str(); + char *path = gw_str_to_cstr(&v.sval); + gw_str_free(&v.sval); + if (chdir(path) != 0) { free(path); gw_error(ERR_PE); } + free(path); + return; + } + /* MKDIR path$ */ + if (xstmt == XSTMT_MKDIR) { + gw_chrget(); + gw_value_t v = gw_eval_str(); + char *path = gw_str_to_cstr(&v.sval); + gw_str_free(&v.sval); + if (mkdir(path, 0755) != 0) { + int e = errno; + free(path); + gw_error(e == EEXIST ? ERR_FE : ERR_PE); + } + free(path); + return; + } + /* RMDIR path$ */ + if (xstmt == XSTMT_RMDIR) { + gw_chrget(); + gw_value_t v = gw_eval_str(); + char *path = gw_str_to_cstr(&v.sval); + gw_str_free(&v.sval); + if (rmdir(path) != 0) { + int e = errno; + free(path); + gw_error(e == ENOENT ? ERR_PE : ERR_FF); + } + free(path); + return; + } /* Stubs: VIEW, WINDOW, PALETTE */ if (xstmt == XSTMT_VIEW || xstmt == XSTMT_WINDOW || xstmt == XSTMT_PALETTE) { diff --git a/tests/programs/datetime.bas b/tests/programs/datetime.bas new file mode 100644 index 0000000..dfc79c7 --- /dev/null +++ b/tests/programs/datetime.bas @@ -0,0 +1,15 @@ +10 REM DATE$, TIME$, TIMER test +20 D$ = DATE$ +30 T$ = TIME$ +40 R = TIMER +50 IF LEN(D$) <> 10 THEN PRINT "FAIL date len": END +60 IF MID$(D$,3,1) <> "-" THEN PRINT "FAIL date sep1": END +70 IF MID$(D$,6,1) <> "-" THEN PRINT "FAIL date sep2": END +80 IF LEN(T$) <> 8 THEN PRINT "FAIL time len": END +90 IF MID$(T$,3,1) <> ":" THEN PRINT "FAIL time sep1": END +100 IF MID$(T$,6,1) <> ":" THEN PRINT "FAIL time sep2": END +110 IF R < 0 OR R > 86400 THEN PRINT "FAIL timer range": END +120 PRINT "DATE$ = "; D$ +130 PRINT "TIME$ = "; T$ +140 PRINT "TIMER ="; INT(R) +150 PRINT "All date/time tests passed" diff --git a/tests/programs/filesystem.bas b/tests/programs/filesystem.bas new file mode 100644 index 0000000..36b65e6 --- /dev/null +++ b/tests/programs/filesystem.bas @@ -0,0 +1,11 @@ +10 REM FILES, MKDIR, CHDIR, RMDIR test +20 MKDIR "gwb_test_dir" +30 OPEN "gwb_test_dir/test.txt" FOR OUTPUT AS #1 +40 PRINT #1, "hello" +50 CLOSE #1 +60 CHDIR "gwb_test_dir" +70 SHELL "pwd > /dev/null" +80 CHDIR ".." +90 KILL "gwb_test_dir/test.txt" +100 RMDIR "gwb_test_dir" +110 PRINT "All filesystem tests passed"