Implement DATE$/TIME$/TIMER, FILES, SHELL, CHDIR, MKDIR, RMDIR

DATE$, TIME$, and TIMER now return real system date/time instead of
hardcoded values. Added directory and shell access statements with
proper GW-BASIC error codes (Path not found 76, File already exists 60).

Bump to v0.6.0, 52 tests.
This commit is contained in:
Eremey Valetov
2026-02-22 12:40:18 -05:00
parent ad21350003
commit ece018d06a
9 changed files with 188 additions and 7 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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 */

View File

@@ -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 }
};

View File

@@ -4,6 +4,7 @@
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
/*
* 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;
}

View File

@@ -7,6 +7,10 @@
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
/*
* 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) {

View File

@@ -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"

View File

@@ -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"