Phase 3: file I/O, PRINT USING, SAVE/LOAD, MID$ assignment, graphics stubs

Add OPEN/CLOSE with both modern (OPEN "f" FOR OUTPUT AS #n) and compact
(OPEN "O",#n,"f") syntaxes. PRINT#, WRITE#, INPUT#, LINE INPUT# for
sequential file access. EOF() now returns real file status with peek-ahead.
LOC/LOF return approximate values.

PRINT USING with numeric (#, ., +, -, $$, **, ^^^^, comma grouping) and
string (!, &, \ \) format specifiers. Shared by PRINT USING and PRINT# USING.

SAVE (ASCII), LOAD (with ,R auto-run), and MERGE for program persistence.

MID$ assignment (MID$(var$, start [,len]) = expr) for in-place string
modification. Works with both scalar variables and array elements.

Graphics stubs for SCREEN, PSET, PRESET, LINE, CIRCLE, DRAW, PAINT,
VIEW, WINDOW, PALETTE - parse and discard arguments so graphics-heavy
programs don't crash.

SYSTEM and NEW/CLEAR now close all open files. Version bumped to 0.3.0.
22 tests pass (16 existing + 6 new).
This commit is contained in:
Eremey Valetov
2026-02-10 11:53:28 -05:00
parent 616259537a
commit c2d73e9c24
16 changed files with 1443 additions and 7 deletions
+3
View File
@@ -26,6 +26,9 @@ set(SOURCES
src/math_transcend.c
src/strings.c
src/print.c
src/fileio.c
src/program_io.c
src/print_using.c
platform/hal_posix.c
)
+23 -1
View File
@@ -10,7 +10,7 @@
#include "gw_math.h"
#include "strings.h"
#define GW_VERSION "0.2.0"
#define GW_VERSION "0.3.0"
#define GW_BANNER "GW-BASIC " GW_VERSION
/* Tokenizer */
@@ -44,6 +44,28 @@ void gw_stmt_line_input(void);
/* DEF FN evaluation (interp.c) */
gw_value_t gw_eval_fn_call(void);
/* File I/O (fileio.c) */
file_entry_t *gw_file_get(int num);
void gw_file_close_all(void);
void gw_stmt_open(void);
void gw_stmt_close(void);
void gw_stmt_print_file(void);
void gw_stmt_write_file(void);
void gw_stmt_input_file(void);
void gw_stmt_line_input_file(void);
int gw_file_eof(int num);
/* Program I/O (program_io.c) */
void gw_stmt_save(void);
void gw_stmt_load(void);
void gw_stmt_merge(void);
/* PRINT USING (print_using.c) */
void gw_print_using(FILE *fp);
/* MID$ assignment (interp.c) */
void gw_stmt_mid_assign(void);
/* Error recovery for run loop */
extern jmp_buf gw_run_jmp;
+3
View File
@@ -55,6 +55,9 @@ typedef struct {
program_line_t *data_line_ptr;
uint16_t data_line;
/* File I/O table (#1-#15, index 0 unused) */
file_entry_t files[16];
/* Error trapping */
uint16_t on_error_line;
bool in_error_handler;
+9
View File
@@ -3,6 +3,7 @@
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
/* Value types matching GW-BASIC's VALTYP */
typedef enum {
@@ -86,6 +87,14 @@ typedef struct {
struct program_line *body_line;
} fn_def_t;
/* File entry for OPEN/CLOSE file table */
typedef struct {
FILE *fp;
int mode; /* 0=closed, 'I'=input, 'O'=output, 'A'=append */
int file_num;
bool eof_flag;
} file_entry_t;
/* MBF (Microsoft Binary Format) types for file compatibility */
typedef struct {
uint8_t mantissa[3];
+30 -3
View File
@@ -651,14 +651,41 @@ static gw_value_t eval_function(uint8_t prefix, uint8_t func_tok)
v.ival = 1;
return v;
case FUNC_EOF:
{
gw_expect('(');
int fnum = gw_eval_int();
gw_expect_rparen();
v.type = VT_INT;
v.ival = gw_file_eof(fnum);
return v;
}
case FUNC_LOC:
case FUNC_LOF:
{
gw_expect('(');
int fnum = gw_eval_int();
gw_expect_rparen();
/* LOC and LOF: return approximate values */
file_entry_t *lf = gw_file_get(fnum);
v.type = VT_SNG;
if (func_tok == FUNC_LOF) {
long cur = ftell(lf->fp);
fseek(lf->fp, 0, SEEK_END);
v.fval = (float)ftell(lf->fp);
fseek(lf->fp, cur, SEEK_SET);
} else {
v.fval = (float)(ftell(lf->fp) / 128 + 1);
}
return v;
}
case FUNC_INP:
case FUNC_PEEK:
case FUNC_PEN:
case FUNC_STICK:
case FUNC_STRIG:
case FUNC_EOF:
case FUNC_LOC:
case FUNC_LOF:
gw_expect('(');
gw_eval();
gw_expect_rparen();
+474
View File
@@ -0,0 +1,474 @@
#include "gwbasic.h"
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
file_entry_t *gw_file_get(int num)
{
if (num < 1 || num > 15)
gw_error(ERR_BN);
file_entry_t *f = &gw.files[num];
if (!f->fp)
gw_error(ERR_BN);
return f;
}
void gw_file_close_all(void)
{
for (int i = 1; i <= 15; i++) {
if (gw.files[i].fp) {
fclose(gw.files[i].fp);
gw.files[i].fp = NULL;
gw.files[i].mode = 0;
gw.files[i].eof_flag = false;
}
}
}
int gw_file_eof(int num)
{
if (num < 1 || num > 15)
gw_error(ERR_BN);
file_entry_t *f = &gw.files[num];
if (!f->fp)
gw_error(ERR_BN);
if (f->eof_flag || feof(f->fp))
return -1;
/* Peek ahead to detect EOF before next read */
int ch = fgetc(f->fp);
if (ch == EOF) {
f->eof_flag = true;
return -1;
}
ungetc(ch, f->fp);
return 0;
}
/* OPEN - two syntaxes:
* Modern: OPEN "file" FOR INPUT|OUTPUT|APPEND AS #n
* Compact: OPEN "I"|"O"|"A", #n, "file"
*/
void gw_stmt_open(void)
{
gw_skip_spaces();
gw_value_t first_str = gw_eval_str();
char *first = gw_str_to_cstr(&first_str.sval);
gw_str_free(&first_str.sval);
gw_skip_spaces();
int mode = 0;
int file_num = 0;
char *filename = NULL;
if (gw_chrgot() == TOK_FOR) {
/* Modern syntax: OPEN "file" FOR mode AS #n */
gw_chrget();
gw_skip_spaces();
filename = first;
/* Parse mode keyword.
* The tokenizer may break words up: "OUTPUT" becomes O U T + TOK_FE XSTMT_PUT
* because "PUT" is a keyword. We identify mode by first letter. */
uint8_t tok = gw_chrgot();
if (tok == TOK_INPUT) {
mode = 'I';
gw_chrget();
} else if (gw_is_letter(tok)) {
char first = toupper(tok);
if (first == 'O') mode = 'O';
else if (first == 'A') mode = 'A';
else if (first == 'R') mode = 'R';
else { free(filename); gw_error(ERR_BM); }
/* Skip remaining letters and embedded tokens of the mode word */
gw_chrget();
while (gw_is_letter(gw_chrgot()) || gw_chrgot() == TOK_PREFIX_FE) {
if (gw_chrgot() == TOK_PREFIX_FE) {
gw_chrget(); /* skip prefix */
gw_chrget(); /* skip extended token */
} else {
gw_chrget();
}
}
} else {
free(filename);
gw_error(ERR_SN);
}
gw_skip_spaces();
/* Skip "AS" - not a token, just letters */
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_skip_spaces();
if (gw_chrgot() == '#')
gw_chrget();
file_num = gw_eval_int();
} else if (gw_chrgot() == ',') {
/* Compact syntax: OPEN "I", #n, "file" */
gw_chrget();
if (strlen(first) >= 1) {
char mc = toupper(first[0]);
if (mc == 'I') mode = 'I';
else if (mc == 'O') mode = 'O';
else if (mc == 'A') mode = 'A';
else if (mc == 'R') mode = 'R'; /* random - treat as input for now */
else { free(first); gw_error(ERR_BM); }
}
free(first);
first = NULL;
gw_skip_spaces();
if (gw_chrgot() == '#')
gw_chrget();
file_num = gw_eval_int();
gw_skip_spaces();
gw_expect(',');
gw_value_t fname_val = gw_eval_str();
filename = gw_str_to_cstr(&fname_val.sval);
gw_str_free(&fname_val.sval);
} else {
free(first);
gw_error(ERR_SN);
}
if (file_num < 1 || file_num > 15) {
free(filename);
gw_error(ERR_BN);
}
if (gw.files[file_num].fp) {
free(filename);
gw_error(ERR_AO);
}
const char *fmode;
switch (mode) {
case 'I': fmode = "r"; break;
case 'O': fmode = "w"; break;
case 'A': fmode = "a"; break;
case 'R': fmode = "r+"; break;
default:
free(filename);
gw_error(ERR_BM);
return;
}
FILE *fp = fopen(filename, fmode);
if (!fp) {
/* For random mode, try creating if doesn't exist */
if (mode == 'R')
fp = fopen(filename, "w+");
if (!fp) {
free(filename);
gw_error(ERR_FF);
}
}
free(filename);
gw.files[file_num].fp = fp;
gw.files[file_num].mode = mode;
gw.files[file_num].file_num = file_num;
gw.files[file_num].eof_flag = false;
/* Skip optional record length: , reclen */
gw_skip_spaces();
if (gw_chrgot() == ',') {
gw_chrget();
gw_eval_int(); /* consume and ignore record length */
}
}
/* CLOSE [#n [, #n ...]] */
void gw_stmt_close(void)
{
gw_skip_spaces();
if (gw_chrgot() == 0 || gw_chrgot() == ':') {
gw_file_close_all();
return;
}
for (;;) {
gw_skip_spaces();
if (gw_chrgot() == '#')
gw_chrget();
int num = gw_eval_int();
if (num < 1 || num > 15)
gw_error(ERR_BN);
if (gw.files[num].fp) {
fclose(gw.files[num].fp);
gw.files[num].fp = NULL;
gw.files[num].mode = 0;
gw.files[num].eof_flag = false;
}
gw_skip_spaces();
if (gw_chrgot() != ',')
break;
gw_chrget();
}
}
/* Helper: write a gw_value_t to a FILE* */
static void fprint_value(FILE *fp, gw_value_t *v)
{
if (v->type == VT_STR) {
fwrite(v->sval.data, 1, v->sval.len, fp);
gw_str_free(&v->sval);
} else {
char buf[64];
gw_format_number(v, buf, sizeof(buf));
fputs(buf, fp);
fputc(' ', fp);
}
}
/* PRINT #n, ... */
void gw_stmt_print_file(void)
{
gw_skip_spaces();
if (gw_chrgot() == '#')
gw_chrget();
int num = gw_eval_int();
file_entry_t *f = gw_file_get(num);
if (f->mode != 'O' && f->mode != 'A')
gw_error(ERR_BM);
gw_skip_spaces();
if (gw_chrgot() == ',')
gw_chrget();
/* Check for USING */
gw_skip_spaces();
if (gw_chrgot() == TOK_USING) {
gw_chrget();
gw_print_using(f->fp);
return;
}
int need_newline = 1;
for (;;) {
gw_skip_spaces();
uint8_t tok = gw_chrgot();
if (tok == 0 || tok == ':' || tok == TOK_ELSE)
break;
if (tok == ';') {
gw_chrget();
need_newline = 0;
continue;
}
if (tok == ',') {
gw_chrget();
/* Tab to next 14-char zone */
/* Tab to next zone: approximate with comma */
fputc(',', f->fp);
need_newline = 0;
continue;
}
gw_value_t v = gw_eval();
fprint_value(f->fp, &v);
need_newline = 1;
}
if (need_newline)
fputc('\n', f->fp);
fflush(f->fp);
}
/* WRITE #n, ... - CSV format with quotes around strings */
void gw_stmt_write_file(void)
{
gw_skip_spaces();
if (gw_chrgot() == '#')
gw_chrget();
int num = gw_eval_int();
file_entry_t *f = gw_file_get(num);
if (f->mode != 'O' && f->mode != 'A')
gw_error(ERR_BM);
gw_skip_spaces();
if (gw_chrgot() == ',')
gw_chrget();
int first = 1;
for (;;) {
gw_skip_spaces();
uint8_t ch = gw_chrgot();
if (ch == 0 || ch == ':') break;
if (!first) fputc(',', f->fp);
gw_value_t v = gw_eval();
if (v.type == VT_STR) {
fputc('"', f->fp);
fwrite(v.sval.data, 1, v.sval.len, f->fp);
fputc('"', f->fp);
gw_str_free(&v.sval);
} else {
char buf[64];
gw_format_number(&v, buf, sizeof(buf));
/* Strip leading space from positive numbers */
char *p = buf;
while (*p == ' ') p++;
fputs(p, f->fp);
}
first = 0;
gw_skip_spaces();
if (gw_chrgot() == ',') { gw_chrget(); continue; }
if (gw_chrgot() == ';') { gw_chrget(); continue; }
break;
}
fputc('\n', f->fp);
fflush(f->fp);
}
/* INPUT #n, var, var... */
void gw_stmt_input_file(void)
{
gw_skip_spaces();
if (gw_chrgot() == '#')
gw_chrget();
int num = gw_eval_int();
file_entry_t *f = gw_file_get(num);
if (f->mode != 'I' && f->mode != 'R')
gw_error(ERR_BM);
gw_skip_spaces();
if (gw_chrgot() == ',')
gw_chrget();
/* Read a line from the file */
char linebuf[256];
if (!fgets(linebuf, sizeof(linebuf), f->fp)) {
f->eof_flag = true;
gw_error(ERR_EF);
}
int len = strlen(linebuf);
while (len > 0 && (linebuf[len - 1] == '\n' || linebuf[len - 1] == '\r'))
linebuf[--len] = '\0';
const char *p = linebuf;
for (;;) {
gw_skip_spaces();
uint8_t ch = gw_chrgot();
if (ch == 0 || ch == ':') break;
char name[2];
gw_valtype_t type = gw_parse_varname(name);
gw_skip_spaces();
var_entry_t *var = NULL;
gw_value_t *arr_elem = NULL;
if (gw_chrgot() == '(') {
arr_elem = gw_array_element(name, type);
} else {
var = gw_var_find_or_create(name, type);
}
while (*p == ' ') p++;
gw_value_t val;
if (type == VT_STR) {
const char *start = p;
if (*p == '"') {
p++;
start = p;
while (*p && *p != '"') p++;
int slen = p - start;
val.type = VT_STR;
val.sval = gw_str_alloc(slen);
memcpy(val.sval.data, start, slen);
if (*p == '"') p++;
} else {
while (*p && *p != ',') p++;
int slen = p - start;
while (slen > 0 && start[slen - 1] == ' ') slen--;
val.type = VT_STR;
val.sval = gw_str_alloc(slen);
memcpy(val.sval.data, start, slen);
}
} else {
char *end;
double d = strtod(p, &end);
if (end == p) d = 0;
p = end;
val.type = VT_DBL;
val.dval = d;
}
if (arr_elem) {
if (type == VT_STR) {
gw_str_free(&arr_elem->sval);
arr_elem->sval = val.sval;
arr_elem->type = VT_STR;
} else {
switch (type) {
case VT_INT: arr_elem->ival = gw_to_int(&val); break;
case VT_SNG: arr_elem->fval = gw_to_sng(&val); break;
case VT_DBL: arr_elem->dval = gw_to_dbl(&val); break;
default: break;
}
arr_elem->type = type;
}
} else {
gw_var_assign(var, &val);
}
if (*p == ',') p++;
gw_skip_spaces();
if (gw_chrgot() == ',') {
gw_chrget();
} else {
break;
}
}
if (feof(f->fp))
f->eof_flag = true;
}
/* LINE INPUT #n, var$ */
void gw_stmt_line_input_file(void)
{
gw_skip_spaces();
if (gw_chrgot() == '#')
gw_chrget();
int num = gw_eval_int();
file_entry_t *f = gw_file_get(num);
if (f->mode != 'I' && f->mode != 'R')
gw_error(ERR_BM);
gw_skip_spaces();
if (gw_chrgot() == ',')
gw_chrget();
char name[2];
gw_valtype_t type = gw_parse_varname(name);
if (type != VT_STR)
gw_error(ERR_TM);
char linebuf[256];
if (!fgets(linebuf, sizeof(linebuf), f->fp)) {
f->eof_flag = true;
gw_error(ERR_EF);
}
int len = strlen(linebuf);
while (len > 0 && (linebuf[len - 1] == '\n' || linebuf[len - 1] == '\r'))
linebuf[--len] = '\0';
gw_value_t val;
val.type = VT_STR;
val.sval = gw_str_from_cstr(linebuf);
var_entry_t *var = gw_var_find_or_create(name, type);
gw_var_assign(var, &val);
if (feof(f->fp))
f->eof_flag = true;
}
+186 -3
View File
@@ -473,6 +473,75 @@ gw_value_t gw_eval_fn_call(void)
return result;
}
/* ================================================================
* MID$ Assignment
* ================================================================ */
void gw_stmt_mid_assign(void)
{
/* Called after TOK_PREFIX_FF FUNC_MID consumed, at '(' */
gw_chrget(); /* skip '(' */
/* Parse the target variable */
char name[2];
gw_valtype_t type = gw_parse_varname(name);
if (type != VT_STR)
gw_error(ERR_TM);
gw_skip_spaces();
var_entry_t *var = NULL;
gw_value_t *arr_elem = NULL;
if (gw_chrgot() == '(') {
arr_elem = gw_array_element(name, type);
} else {
var = gw_var_find_or_create(name, type);
}
gw_skip_spaces();
gw_expect(',');
int start = gw_eval_int();
if (start < 1) gw_error(ERR_FC);
int maxlen = -1;
gw_skip_spaces();
if (gw_chrgot() == ',') {
gw_chrget();
maxlen = gw_eval_int();
if (maxlen < 0) gw_error(ERR_FC);
}
gw_expect_rparen();
gw_skip_spaces();
gw_expect(TOK_EQ);
gw_value_t rhs = gw_eval_str();
/* Get pointer to the target string */
gw_string_t *target;
if (arr_elem) {
target = &arr_elem->sval;
} else {
target = &var->val.sval;
}
/* Perform in-place replacement */
int pos = start - 1; /* 0-based */
if (pos >= target->len) {
gw_str_free(&rhs.sval);
return; /* nothing to replace */
}
int replace_len = rhs.sval.len;
int available = target->len - pos;
if (maxlen >= 0 && replace_len > maxlen)
replace_len = maxlen;
if (replace_len > available)
replace_len = available;
memcpy(target->data + pos, rhs.sval.data, replace_len);
gw_str_free(&rhs.sval);
}
/* ================================================================
* Statement Dispatcher
* ================================================================ */
@@ -496,6 +565,11 @@ void gw_exec_stmt(void)
/* PRINT / ? */
if (tok == TOK_PRINT || tok == '?') {
gw_chrget();
gw_skip_spaces();
if (gw_chrgot() == '#') {
gw_stmt_print_file();
return;
}
gw_stmt_print();
return;
}
@@ -520,14 +594,26 @@ void gw_exec_stmt(void)
return;
}
/* SYSTEM */
/* Extended statements (0xFE prefix) */
if (tok == TOK_PREFIX_FE) {
uint8_t *save = gw.text_ptr;
gw_chrget();
if (gw_chrgot() == XSTMT_SYSTEM) {
uint8_t xstmt = gw_chrgot();
if (xstmt == XSTMT_SYSTEM) {
gw_file_close_all();
if (gw_hal) gw_hal->shutdown();
exit(0);
}
/* Graphics/sound stubs - parse and discard arguments */
if (xstmt == XSTMT_CIRCLE || xstmt == XSTMT_DRAW ||
xstmt == XSTMT_PAINT || xstmt == XSTMT_PLAY ||
xstmt == XSTMT_VIEW || xstmt == XSTMT_WINDOW ||
xstmt == XSTMT_PALETTE) {
gw_chrget();
while (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE)
gw.text_ptr++;
return;
}
gw.text_ptr = save;
}
@@ -561,6 +647,7 @@ void gw_exec_stmt(void)
gw_free_program();
gw_vars_clear();
gw_arrays_clear();
gw_file_close_all();
memset(gw.fn_defs, 0, sizeof(gw.fn_defs));
gw.for_sp = 0;
gw.gosub_sp = 0;
@@ -583,6 +670,7 @@ void gw_exec_stmt(void)
gw_chrget();
gw_vars_clear();
gw_arrays_clear();
gw_file_close_all();
memset(gw.fn_defs, 0, sizeof(gw.fn_defs));
gw.for_sp = 0;
gw.gosub_sp = 0;
@@ -1096,19 +1184,36 @@ void gw_exec_stmt(void)
/* INPUT */
if (tok == TOK_INPUT) {
gw_chrget();
gw_skip_spaces();
if (gw_chrgot() == '#') {
gw_stmt_input_file();
return;
}
gw_stmt_input();
return;
}
/* LINE INPUT */
/* LINE INPUT / LINE (graphics stub) */
if (tok == TOK_LINE) {
gw_chrget();
gw_skip_spaces();
if (gw_chrgot() == TOK_INPUT) {
gw_chrget();
gw_skip_spaces();
if (gw_chrgot() == '#') {
gw_stmt_line_input_file();
return;
}
gw_stmt_line_input();
return;
}
/* LINE (x1,y1)-(x2,y2) [,[color][,B[F]]] - graphics stub */
if (gw_chrgot() == '(' || gw_chrgot() == TOK_MINUS) {
/* Parse and discard all arguments */
while (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE)
gw.text_ptr++;
return;
}
gw_error(ERR_SN);
}
@@ -1287,6 +1392,28 @@ void gw_exec_stmt(void)
return;
}
/* SCREEN - graphics stub */
if (tok == TOK_SCREEN) {
gw_chrget();
gw_eval_int(); /* screen mode */
while (gw_chrgot() == ',') {
gw_chrget();
gw_skip_spaces();
if (gw_chrgot() != ',' && gw_chrgot() != 0 && gw_chrgot() != ':')
gw_eval_int();
}
return;
}
/* PSET / PRESET - graphics stub */
if (tok == TOK_PSET || tok == TOK_PRESET) {
gw_chrget();
/* Parse (x,y) [,color] and discard */
while (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE)
gw.text_ptr++;
return;
}
/* BEEP */
if (tok == TOK_BEEP) {
gw_chrget();
@@ -1317,9 +1444,49 @@ void gw_exec_stmt(void)
return;
}
/* OPEN */
if (tok == TOK_OPEN) {
gw_chrget();
gw_stmt_open();
return;
}
/* CLOSE */
if (tok == TOK_CLOSE) {
gw_chrget();
gw_stmt_close();
return;
}
/* SAVE */
if (tok == TOK_SAVE) {
gw_chrget();
gw_stmt_save();
return;
}
/* LOAD */
if (tok == TOK_LOAD) {
gw_chrget();
gw_stmt_load();
return;
}
/* MERGE */
if (tok == TOK_MERGE) {
gw_chrget();
gw_stmt_merge();
return;
}
/* WRITE */
if (tok == TOK_WRITE) {
gw_chrget();
gw_skip_spaces();
if (gw_chrgot() == '#') {
gw_stmt_write_file();
return;
}
int first = 1;
for (;;) {
gw_skip_spaces();
@@ -1349,6 +1516,22 @@ void gw_exec_stmt(void)
return;
}
/* MID$ assignment: MID$(var$, start [,len]) = expr */
if (tok == TOK_PREFIX_FF) {
uint8_t *save = gw.text_ptr;
gw_chrget();
if (gw_chrgot() == FUNC_MID) {
gw_chrget();
gw_skip_spaces();
if (gw_chrgot() == '(') {
gw_stmt_mid_assign();
return;
}
}
gw.text_ptr = save;
tok = gw_chrgot();
}
/* LET (explicit) */
if (tok == TOK_LET) {
gw_chrget();
+8
View File
@@ -49,6 +49,14 @@ void gw_print_value(gw_value_t *v)
*/
void gw_stmt_print(void)
{
/* Check for USING */
gw_skip_spaces();
if (gw_chrgot() == TOK_USING) {
gw_chrget();
gw_print_using(NULL);
return;
}
int need_newline = 1;
int screen_width = 80;
if (gw_hal)
+514
View File
@@ -0,0 +1,514 @@
#include "gwbasic.h"
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <ctype.h>
/*
* PRINT USING formatter - implements GW-BASIC's formatted output.
*
* Numeric format characters:
* # Digit position
* . Decimal point
* , Insert commas every 3 digits (before decimal)
* + Print sign (at start or end)
* - Trailing negative sign
* $$ Leading dollar sign
* ** Asterisk fill
* **$ Asterisk fill + dollar
* ^^^^ Scientific notation
*
* String format characters:
* ! First character only
* & Entire string
* \ \ Fixed width (count chars between backslashes inclusive)
*
* _ Literal escape: next char printed as-is
*/
/* Output helpers */
static void pu_putch(FILE *fp, int ch)
{
if (fp) {
fputc(ch, fp);
} else if (gw_hal) {
gw_hal->putch(ch);
} else {
putchar(ch);
}
}
static void pu_puts(FILE *fp, const char *s)
{
while (*s)
pu_putch(fp, *s++);
}
/* Format a numeric value according to the format spec */
static void format_number(FILE *fp, const char *fmt, int fmtlen, double val)
{
/* Parse the format spec */
bool leading_plus = false, trailing_plus = false;
bool trailing_minus = false;
bool dollar = false;
bool asterisk_fill = false;
bool use_commas = false;
bool scientific = false;
int total_digits = 0; /* total # positions */
int decimal_digits = -1; /* -1 = no decimal point */
bool has_decimal = false;
int i = 0;
/* Check leading + */
if (i < fmtlen && fmt[i] == '+') {
leading_plus = true;
i++;
}
/* Check ** or **$ or $$ */
if (i + 1 < fmtlen && fmt[i] == '*' && fmt[i + 1] == '*') {
asterisk_fill = true;
total_digits += 2;
i += 2;
if (i < fmtlen && fmt[i] == '$') {
dollar = true;
i++;
total_digits++;
}
} else if (i + 1 < fmtlen && fmt[i] == '$' && fmt[i + 1] == '$') {
dollar = true;
total_digits += 2;
i += 2;
}
/* Count # before decimal */
while (i < fmtlen && fmt[i] == '#') {
total_digits++;
i++;
}
/* Check for comma */
if (i < fmtlen && fmt[i] == ',') {
use_commas = true;
i++;
}
/* More # after comma but before decimal */
while (i < fmtlen && fmt[i] == '#') {
total_digits++;
i++;
}
/* Decimal point */
if (i < fmtlen && fmt[i] == '.') {
has_decimal = true;
decimal_digits = 0;
i++;
while (i < fmtlen && fmt[i] == '#') {
decimal_digits++;
i++;
}
}
/* Scientific notation (^^^^) */
int caret_count = 0;
while (i < fmtlen && fmt[i] == '^') {
caret_count++;
i++;
}
if (caret_count >= 4)
scientific = true;
/* Trailing sign */
if (i < fmtlen && fmt[i] == '+') {
trailing_plus = true;
i++;
} else if (i < fmtlen && fmt[i] == '-') {
trailing_minus = true;
i++;
}
/* Format the number */
bool negative = val < 0;
double absval = fabs(val);
if (scientific) {
int dec = decimal_digits >= 0 ? decimal_digits : 0;
char numbuf[64];
snprintf(numbuf, sizeof(numbuf), "%+.*E", dec, val);
/* Reformat to match GW-BASIC: sign, digits, E+nn */
int field_width = total_digits + (has_decimal ? 1 : 0) + 5; /* E+nn + sign */
if (leading_plus || trailing_plus || trailing_minus)
field_width++;
char outbuf[64];
int oi = 0;
if (leading_plus)
outbuf[oi++] = negative ? '-' : '+';
else if (!trailing_plus && !trailing_minus)
outbuf[oi++] = negative ? '-' : ' ';
/* Format mantissa */
char mantissa[32];
int exp_val;
if (val == 0) {
snprintf(mantissa, sizeof(mantissa), "%.*f", dec, 0.0);
exp_val = 0;
} else {
exp_val = (int)floor(log10(absval));
double mant = absval / pow(10, exp_val);
snprintf(mantissa, sizeof(mantissa), "%.*f", dec, mant);
/* Check for rounding overflow (e.g., 9.95 -> 10.0 with 1 dec) */
if (mantissa[0] != '0' && (mantissa[0] - '0') >= 10) {
exp_val++;
mant = absval / pow(10, exp_val);
snprintf(mantissa, sizeof(mantissa), "%.*f", dec, mant);
}
}
for (int j = 0; mantissa[j] && oi < (int)sizeof(outbuf) - 8; j++)
outbuf[oi++] = mantissa[j];
snprintf(outbuf + oi, sizeof(outbuf) - oi, "E%+03d", exp_val);
oi = strlen(outbuf);
if (trailing_plus)
outbuf[oi++] = negative ? '-' : '+';
else if (trailing_minus)
outbuf[oi++] = negative ? '-' : ' ';
outbuf[oi] = '\0';
pu_puts(fp, outbuf);
return;
}
/* Fixed-point formatting */
int dec = (decimal_digits >= 0) ? decimal_digits : 0;
char numbuf[64];
snprintf(numbuf, sizeof(numbuf), "%.*f", dec, absval);
/* Split into integer and fractional parts */
char *dot = strchr(numbuf, '.');
char intpart[48], fracpart[48];
if (dot) {
int ilen = dot - numbuf;
memcpy(intpart, numbuf, ilen);
intpart[ilen] = '\0';
strcpy(fracpart, dot + 1);
} else {
strcpy(intpart, numbuf);
fracpart[0] = '\0';
}
/* Handle commas */
char int_with_commas[80];
if (use_commas) {
int ilen = strlen(intpart);
int oi = 0;
for (int j = 0; j < ilen; j++) {
if (j > 0 && (ilen - j) % 3 == 0)
int_with_commas[oi++] = ',';
int_with_commas[oi++] = intpart[j];
}
int_with_commas[oi] = '\0';
} else {
strcpy(int_with_commas, intpart);
}
/* Build the number string without sign */
char numstr[160];
if (has_decimal)
snprintf(numstr, sizeof(numstr), "%s.%s", int_with_commas, fracpart);
else
snprintf(numstr, sizeof(numstr), "%s", int_with_commas);
/* Calculate total field width for integer part */
int int_field = total_digits;
int total_field = int_field + (has_decimal ? 1 + dec : 0);
/* Pad the number to fill the field */
char result[80];
int numlen = strlen(numstr);
int padding = total_field - numlen;
if (padding < 0) padding = 0;
int ri = 0;
/* Sign handling */
bool sign_placed = false;
if (leading_plus) {
result[ri++] = negative ? '-' : '+';
sign_placed = true;
}
/* Fill padding */
char fillch = asterisk_fill ? '*' : ' ';
bool dollar_placed = false;
if (numlen > total_field) {
/* Overflow: print % followed by the number */
pu_putch(fp, '%');
if (!sign_placed && !trailing_plus && !trailing_minus) {
if (negative) pu_putch(fp, '-');
}
pu_puts(fp, numstr);
if (trailing_plus)
pu_putch(fp, negative ? '-' : '+');
else if (trailing_minus)
pu_putch(fp, negative ? '-' : ' ');
return;
}
for (int j = 0; j < padding; j++) {
if (dollar && !dollar_placed && j == padding - 1) {
if (!sign_placed && negative && !trailing_plus && !trailing_minus) {
result[ri++] = fillch == '*' ? '*' : ' ';
}
result[ri++] = '$';
dollar_placed = true;
} else {
result[ri++] = fillch;
}
}
/* Place sign if not yet placed and no trailing sign */
if (!sign_placed && !trailing_plus && !trailing_minus) {
if (negative) {
/* Find rightmost fill char and replace with - */
bool placed = false;
for (int j = ri - 1; j >= 0; j--) {
if (result[j] == ' ' || (result[j] == '*' && !asterisk_fill)) {
result[j] = '-';
placed = true;
break;
}
}
if (!placed && ri > 0) {
/* Put sign before number */
if (result[0] == ' ' || result[0] == '*')
result[0] = '-';
else {
/* Shift everything right */
memmove(result + 1, result, ri);
result[0] = '-';
ri++;
}
}
}
}
/* Dollar sign if not placed yet */
if (dollar && !dollar_placed) {
if (ri > 0 && (result[ri - 1] == ' ' || result[ri - 1] == '*'))
result[ri - 1] = '$';
else
result[ri++] = '$';
}
/* Copy the number digits */
for (int j = 0; j < numlen && ri < (int)sizeof(result) - 4; j++)
result[ri++] = numstr[j];
/* Trailing sign */
if (trailing_plus)
result[ri++] = negative ? '-' : '+';
else if (trailing_minus)
result[ri++] = negative ? '-' : ' ';
result[ri] = '\0';
pu_puts(fp, result);
}
/* Format a string value */
static void format_string(FILE *fp, const char *fmt, int fmtlen, gw_string_t *s)
{
if (fmtlen == 1 && fmt[0] == '!') {
/* First character only */
if (s->len > 0)
pu_putch(fp, s->data[0]);
else
pu_putch(fp, ' ');
} else if (fmtlen == 1 && fmt[0] == '&') {
/* Entire string */
for (int i = 0; i < s->len; i++)
pu_putch(fp, s->data[i]);
} else if (fmt[0] == '\\') {
/* Fixed width: count chars between backslashes, inclusive */
int width = fmtlen; /* includes both backslashes */
for (int i = 0; i < width; i++) {
if (i < s->len)
pu_putch(fp, s->data[i]);
else
pu_putch(fp, ' ');
}
}
}
/*
* PRINT USING "format"; expr [, expr ...]
* fp: output FILE* (NULL = screen via HAL)
* Called after USING token has been consumed.
*/
void gw_print_using(FILE *fp)
{
gw_skip_spaces();
gw_value_t fmt_val = gw_eval_str();
char *fmt = gw_str_to_cstr(&fmt_val.sval);
int fmtlen = fmt_val.sval.len;
gw_str_free(&fmt_val.sval);
gw_skip_spaces();
if (gw_chrgot() == ';')
gw_chrget();
int fi = 0; /* format index */
bool had_semicolon = false;
for (;;) {
gw_skip_spaces();
uint8_t tok = gw_chrgot();
if (tok == 0 || tok == ':' || tok == TOK_ELSE)
break;
/* Reset format position if we've gone past the end */
if (fi >= fmtlen)
fi = 0;
/* Output literal characters until we hit a format spec */
while (fi < fmtlen) {
char ch = fmt[fi];
/* Literal escape */
if (ch == '_') {
fi++;
if (fi < fmtlen)
pu_putch(fp, fmt[fi++]);
continue;
}
/* Numeric format start */
if (ch == '#' || ch == '+' || ch == '.' ||
(ch == '*' && fi + 1 < fmtlen && fmt[fi + 1] == '*') ||
(ch == '$' && fi + 1 < fmtlen && fmt[fi + 1] == '$'))
break;
/* String format start */
if (ch == '!' || ch == '&' || ch == '\\')
break;
/* Regular character - output as literal */
pu_putch(fp, ch);
fi++;
}
if (fi >= fmtlen) {
/* Check if there are more values to format */
gw_skip_spaces();
tok = gw_chrgot();
if (tok == 0 || tok == ':' || tok == TOK_ELSE)
break;
/* More values: reset format and re-enter the loop */
fi = 0;
continue;
}
/* Determine format spec type and length */
char ch = fmt[fi];
/* String formats */
if (ch == '!') {
gw_value_t v = gw_eval_str();
format_string(fp, "!", 1, &v.sval);
gw_str_free(&v.sval);
fi++;
} else if (ch == '&') {
gw_value_t v = gw_eval_str();
format_string(fp, "&", 1, &v.sval);
gw_str_free(&v.sval);
fi++;
} else if (ch == '\\') {
/* Count to closing backslash */
int start = fi;
fi++;
while (fi < fmtlen && fmt[fi] != '\\')
fi++;
if (fi < fmtlen) fi++; /* skip closing backslash */
int width = fi - start;
gw_value_t v = gw_eval_str();
format_string(fp, fmt + start, width, &v.sval);
gw_str_free(&v.sval);
} else {
/* Numeric format - collect the entire spec */
int start = fi;
/* Leading + */
if (fi < fmtlen && fmt[fi] == '+') fi++;
/* ** or **$ or $$ */
if (fi + 1 < fmtlen && fmt[fi] == '*' && fmt[fi + 1] == '*') {
fi += 2;
if (fi < fmtlen && fmt[fi] == '$') fi++;
} else if (fi + 1 < fmtlen && fmt[fi] == '$' && fmt[fi + 1] == '$') {
fi += 2;
}
/* # digits and commas */
while (fi < fmtlen && (fmt[fi] == '#' || fmt[fi] == ','))
fi++;
/* Decimal point and fraction digits */
if (fi < fmtlen && fmt[fi] == '.') {
fi++;
while (fi < fmtlen && fmt[fi] == '#')
fi++;
}
/* Carets for scientific notation */
while (fi < fmtlen && fmt[fi] == '^')
fi++;
/* Trailing sign */
if (fi < fmtlen && (fmt[fi] == '+' || fmt[fi] == '-'))
fi++;
gw_value_t v = gw_eval_num();
double dv = gw_to_dbl(&v);
format_number(fp, fmt + start, fi - start, dv);
}
/* Check for separator */
gw_skip_spaces();
had_semicolon = false;
if (gw_chrgot() == ';') {
gw_chrget();
had_semicolon = true;
} else if (gw_chrgot() == ',') {
gw_chrget();
had_semicolon = true;
}
if (!had_semicolon)
break;
}
/* Output remaining literal chars from format */
while (fi < fmtlen) {
if (fmt[fi] == '_') {
fi++;
if (fi < fmtlen)
pu_putch(fp, fmt[fi++]);
} else {
pu_putch(fp, fmt[fi++]);
}
}
if (!had_semicolon)
pu_putch(fp, '\n');
if (fp)
fflush(fp);
free(fmt);
}
+145
View File
@@ -0,0 +1,145 @@
#include "gwbasic.h"
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
/* SAVE "filename" [,A] - save program as ASCII text */
void gw_stmt_save(void)
{
gw_skip_spaces();
gw_value_t fname_val = gw_eval_str();
char *filename = gw_str_to_cstr(&fname_val.sval);
gw_str_free(&fname_val.sval);
/* We only support ASCII saves (,A is optional/default) */
gw_skip_spaces();
if (gw_chrgot() == ',') {
gw_chrget();
gw_skip_spaces();
/* Skip A or P flag */
if (gw_is_letter(gw_chrgot()))
gw_chrget();
}
FILE *fp = fopen(filename, "w");
if (!fp) {
free(filename);
gw_error(ERR_IO);
}
free(filename);
char listbuf[512];
program_line_t *p = gw.prog_head;
while (p) {
gw_list_line(p->tokens, p->len, listbuf, sizeof(listbuf));
fprintf(fp, "%u %s\n", p->num, listbuf);
p = p->next;
}
fclose(fp);
}
/* Helper: load lines from a file into the program, optionally clearing first */
static void load_from_file(const char *filename, bool clear)
{
FILE *fp = fopen(filename, "r");
if (!fp)
gw_error(ERR_FF);
if (clear) {
gw_free_program();
gw_vars_clear();
gw_arrays_clear();
gw_file_close_all();
memset(gw.fn_defs, 0, sizeof(gw.fn_defs));
gw.for_sp = 0;
gw.gosub_sp = 0;
gw.while_sp = 0;
gw.data_ptr = NULL;
gw.data_line_ptr = NULL;
gw.cont_text = NULL;
gw.cont_line = NULL;
gw.on_error_line = 0;
gw.in_error_handler = false;
}
char buf[256];
while (fgets(buf, sizeof(buf), fp)) {
int len = strlen(buf);
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r'))
buf[--len] = '\0';
if (buf[0] == '\0') continue;
int clen = gw_crunch(buf, gw.kbuf, sizeof(gw.kbuf));
/* Parse line number from tokenized form */
uint8_t *tp = gw.kbuf;
while (*tp == ' ') tp++;
uint8_t tok = *tp;
uint16_t num;
int skip = 0;
if (tok >= 0x11 && tok <= 0x1A) {
num = tok - 0x11;
skip = (tp - gw.kbuf) + 1;
} else if (tok == 0x0F) {
num = tp[1];
skip = (tp - gw.kbuf) + 2;
} else if (tok == 0x0E) {
num = (uint16_t)(tp[1] | (tp[2] << 8));
skip = (tp - gw.kbuf) + 3;
} else {
continue; /* skip non-numbered lines */
}
int data_len = clen - skip;
uint8_t *data = gw.kbuf + skip;
while (*data == ' ' && data_len > 0) { data++; data_len--; }
if (data_len > 0 && *data != 0)
gw_store_line(num, data, data_len);
}
fclose(fp);
}
/* LOAD "filename" [,R] */
void gw_stmt_load(void)
{
gw_skip_spaces();
gw_value_t fname_val = gw_eval_str();
char *filename = gw_str_to_cstr(&fname_val.sval);
gw_str_free(&fname_val.sval);
bool run_after = false;
gw_skip_spaces();
if (gw_chrgot() == ',') {
gw_chrget();
gw_skip_spaces();
if (gw_is_letter(gw_chrgot()) && toupper(gw_chrgot()) == 'R') {
run_after = true;
gw_chrget();
}
}
load_from_file(filename, true);
free(filename);
if (run_after && gw.prog_head) {
gw.cur_line = gw.prog_head;
gw.text_ptr = gw.prog_head->tokens;
gw.cur_line_num = gw.prog_head->num;
gw.running = true;
gw_run_loop();
}
}
/* MERGE "filename" - load without clearing existing program */
void gw_stmt_merge(void)
{
gw_skip_spaces();
gw_value_t fname_val = gw_eval_str();
char *filename = gw_str_to_cstr(&fname_val.sval);
gw_str_free(&fname_val.sval);
load_from_file(filename, false);
free(filename);
}
+11
View File
@@ -0,0 +1,11 @@
10 REM File I/O test
20 OPEN "/tmp/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
70 LINE INPUT #1, A$
80 LINE INPUT #1, B$
90 CLOSE #1
100 PRINT A$
110 PRINT B$
+9
View File
@@ -0,0 +1,9 @@
10 REM Graphics stubs test
20 SCREEN 2
30 PSET(10,20),3
40 PRESET(50,50)
50 LINE (0,0)-(100,100),1
60 CIRCLE(160,100),50,2
70 DRAW "U10 R10 D10 L10"
80 PAINT (50,50),1,2
90 PRINT "Graphics stubs OK"
+7
View File
@@ -0,0 +1,7 @@
10 REM MID$ assignment test
20 A$="HELLO WORLD"
30 MID$(A$,7,5)="BASIC"
40 PRINT A$
50 B$="ABCDEF"
60 MID$(B$,1,2)="XY"
70 PRINT B$
+7
View File
@@ -0,0 +1,7 @@
10 REM PRINT USING test
20 PRINT USING "###.##"; 3.14
30 PRINT USING "###.##"; -42.5
40 PRINT USING "+###.##"; 99.1
50 PRINT USING "!"; "Hello"
60 PRINT USING "\ \"; "Hello World"
70 PRINT USING "$$###.##"; 1234.56
+3
View File
@@ -0,0 +1,3 @@
10 REM SAVE test
20 PRINT "Program saved"
30 SAVE "/tmp/gwbasic_save_test.bas"
+11
View File
@@ -0,0 +1,11 @@
10 REM WRITE#/INPUT# and EOF test
20 OPEN "/tmp/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
70 WHILE NOT EOF(1)
80 INPUT #1, N$, A
90 PRINT N$; A
100 WEND
110 CLOSE #1