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:
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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$
|
||||
@@ -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"
|
||||
@@ -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$
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
10 REM SAVE test
|
||||
20 PRINT "Program saved"
|
||||
30 SAVE "/tmp/gwbasic_save_test.bas"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user