Phase 4: CHAIN, RUN "file", random-access I/O, MBF conversions

Add CHAIN statement for loading and running chained programs with
optional ALL flag to preserve variables. Extend RUN to accept a
filename string argument. Implement random-access file I/O with
FIELD, LSET, RSET, PUT#, GET# and the MBF conversion functions
CVI/CVS/CVD/MKI$/MKS$/MKD$. Add COMMON statement (parse and skip).
Five new test programs covering all new features (27 total).
This commit is contained in:
Eremey Valetov
2026-02-10 12:11:25 -05:00
parent c2d73e9c24
commit 66479b5d6e
11 changed files with 571 additions and 11 deletions
+49 -2
View File
@@ -727,12 +727,59 @@ static gw_value_t eval_atom(void)
return eval_function(TOK_PREFIX_FF, func);
}
/* Extended function (0xFD prefix) */
/* Extended function (0xFD prefix): CVI, CVS, CVD, MKI$, MKS$, MKD$ */
if (tok == TOK_PREFIX_FD) {
gw_chrget();
uint8_t func = gw_chrgot();
gw_chrget();
return eval_function(TOK_PREFIX_FD, func);
switch (func) {
case XFUNC_CVI:
{
gw_expect('(');
gw_value_t arg = gw_eval_str();
gw_expect_rparen();
return gw_fn_cvi(&arg);
}
case XFUNC_CVS:
{
gw_expect('(');
gw_value_t arg = gw_eval_str();
gw_expect_rparen();
return gw_fn_cvs(&arg);
}
case XFUNC_CVD:
{
gw_expect('(');
gw_value_t arg = gw_eval_str();
gw_expect_rparen();
return gw_fn_cvd(&arg);
}
case XFUNC_MKI:
{
gw_expect('(');
int16_t n = gw_eval_int();
gw_expect_rparen();
return gw_fn_mki(n);
}
case XFUNC_MKS:
{
gw_expect('(');
gw_value_t arg = gw_eval_num();
gw_expect_rparen();
return gw_fn_mks(gw_to_sng(&arg));
}
case XFUNC_MKD:
{
gw_expect('(');
gw_value_t arg = gw_eval_num();
gw_expect_rparen();
return gw_fn_mkd(gw_to_dbl(&arg));
}
default:
gw_error(ERR_SN);
break;
}
}
/* STRING$ function (single-byte token but acts like function) */
+309 -3
View File
@@ -22,6 +22,10 @@ void gw_file_close_all(void)
gw.files[i].mode = 0;
gw.files[i].eof_flag = false;
}
free(gw.files[i].field_buf);
gw.files[i].field_buf = NULL;
gw.files[i].field_count = 0;
gw.files[i].record_len = 0;
}
}
@@ -172,12 +176,31 @@ void gw_stmt_open(void)
gw.files[file_num].mode = mode;
gw.files[file_num].file_num = file_num;
gw.files[file_num].eof_flag = false;
gw.files[file_num].field_buf = NULL;
gw.files[file_num].field_count = 0;
/* Skip optional record length: , reclen */
/* Parse optional record length */
int reclen = 128;
gw_skip_spaces();
if (gw_chrgot() == ',') {
/* Modern syntax: LEN = n */
if (gw_is_letter(gw_chrgot()) && toupper(gw_chrgot()) == 'L') {
/* Skip LEN */
while (gw_is_letter(gw_chrgot())) gw_chrget();
gw_skip_spaces();
if (gw_chrgot() == TOK_EQ) {
gw_chrget();
reclen = gw_eval_int();
}
} else if (gw_chrgot() == ',') {
/* Compact syntax: , reclen */
gw_chrget();
gw_eval_int(); /* consume and ignore record length */
reclen = gw_eval_int();
}
gw.files[file_num].record_len = reclen;
if (mode == 'R') {
gw.files[file_num].field_buf = calloc(1, reclen);
if (!gw.files[file_num].field_buf) gw_error(ERR_OM);
}
}
@@ -202,6 +225,10 @@ void gw_stmt_close(void)
gw.files[num].fp = NULL;
gw.files[num].mode = 0;
gw.files[num].eof_flag = false;
free(gw.files[num].field_buf);
gw.files[num].field_buf = NULL;
gw.files[num].field_count = 0;
gw.files[num].record_len = 0;
}
gw_skip_spaces();
if (gw_chrgot() != ',')
@@ -472,3 +499,282 @@ void gw_stmt_line_input_file(void)
if (feof(f->fp))
f->eof_flag = true;
}
/* ================================================================
* Random-access file I/O: FIELD, LSET, RSET, PUT, GET
* ================================================================ */
static void field_buf_to_vars(file_entry_t *f);
static void vars_to_field_buf(file_entry_t *f);
/* FIELD #n, width AS var$ [, width AS var$ ...] */
void gw_stmt_field(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 != 'R')
gw_error(ERR_BM);
gw_skip_spaces();
if (gw_chrgot() == ',')
gw_chrget();
f->field_count = 0;
int offset = 0;
for (;;) {
gw_skip_spaces();
if (gw_chrgot() == 0 || gw_chrgot() == ':')
break;
int width = gw_eval_int();
if (width < 0) gw_error(ERR_FC);
if (offset + width > f->record_len) gw_error(ERR_FO);
gw_skip_spaces();
/* Skip AS keyword */
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();
char name[2];
gw_valtype_t type = gw_parse_varname(name);
if (type != VT_STR) gw_error(ERR_TM);
if (f->field_count >= 32) gw_error(ERR_FO);
f->fields[f->field_count].name[0] = name[0];
f->fields[f->field_count].name[1] = name[1];
f->fields[f->field_count].type = type;
f->fields[f->field_count].offset = offset;
f->fields[f->field_count].width = width;
f->field_count++;
offset += width;
gw_skip_spaces();
if (gw_chrgot() == ',') {
gw_chrget();
continue;
}
break;
}
/* Initialize FIELD variables with proper widths (space-filled) */
field_buf_to_vars(f);
}
/* Copy field buffer data into the FIELD variables */
static void field_buf_to_vars(file_entry_t *f)
{
for (int i = 0; i < f->field_count; i++) {
var_entry_t *var = gw_var_find_or_create(f->fields[i].name,
f->fields[i].type);
gw_str_free(&var->val.sval);
var->val.type = VT_STR;
var->val.sval = gw_str_alloc(f->fields[i].width);
memcpy(var->val.sval.data,
f->field_buf + f->fields[i].offset,
f->fields[i].width);
}
}
/* Copy FIELD variables into the field buffer */
static void vars_to_field_buf(file_entry_t *f)
{
for (int i = 0; i < f->field_count; i++) {
var_entry_t *var = gw_var_find_or_create(f->fields[i].name,
f->fields[i].type);
int width = f->fields[i].width;
int len = (var->val.type == VT_STR) ? var->val.sval.len : 0;
int copy = (len < width) ? len : width;
if (copy > 0)
memcpy(f->field_buf + f->fields[i].offset,
var->val.sval.data, copy);
if (copy < width)
memset(f->field_buf + f->fields[i].offset + copy, ' ',
width - copy);
}
}
/* LSET var$ = expr$ - left-justify into field variable */
void gw_stmt_lset(void)
{
gw_skip_spaces();
char name[2];
gw_valtype_t type = gw_parse_varname(name);
if (type != VT_STR) gw_error(ERR_TM);
var_entry_t *var = gw_var_find_or_create(name, type);
gw_skip_spaces();
gw_expect(TOK_EQ);
gw_value_t rhs = gw_eval_str();
int target_len = var->val.sval.len;
if (target_len == 0) {
gw_str_free(&rhs.sval);
return;
}
int copy = (rhs.sval.len < target_len) ? rhs.sval.len : target_len;
memcpy(var->val.sval.data, rhs.sval.data, copy);
if (copy < target_len)
memset(var->val.sval.data + copy, ' ', target_len - copy);
gw_str_free(&rhs.sval);
}
/* RSET var$ = expr$ - right-justify into field variable */
void gw_stmt_rset(void)
{
gw_skip_spaces();
char name[2];
gw_valtype_t type = gw_parse_varname(name);
if (type != VT_STR) gw_error(ERR_TM);
var_entry_t *var = gw_var_find_or_create(name, type);
gw_skip_spaces();
gw_expect(TOK_EQ);
gw_value_t rhs = gw_eval_str();
int target_len = var->val.sval.len;
if (target_len == 0) {
gw_str_free(&rhs.sval);
return;
}
int copy = (rhs.sval.len < target_len) ? rhs.sval.len : target_len;
int pad = target_len - copy;
if (pad > 0)
memset(var->val.sval.data, ' ', pad);
memcpy(var->val.sval.data + pad, rhs.sval.data, copy);
gw_str_free(&rhs.sval);
}
/* PUT #n [, record] - write field buffer to file */
void gw_stmt_put(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 != 'R')
gw_error(ERR_BM);
long record = -1;
gw_skip_spaces();
if (gw_chrgot() == ',') {
gw_chrget();
record = gw_eval_int();
if (record < 1) gw_error(ERR_RN);
}
vars_to_field_buf(f);
if (record > 0)
fseek(f->fp, (long)(record - 1) * f->record_len, SEEK_SET);
fwrite(f->field_buf, 1, f->record_len, f->fp);
fflush(f->fp);
}
/* GET #n [, record] - read record from file into field buffer */
void gw_stmt_get(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 != 'R')
gw_error(ERR_BM);
long record = -1;
gw_skip_spaces();
if (gw_chrgot() == ',') {
gw_chrget();
record = gw_eval_int();
if (record < 1) gw_error(ERR_RN);
}
if (record > 0)
fseek(f->fp, (long)(record - 1) * f->record_len, SEEK_SET);
memset(f->field_buf, 0, f->record_len);
size_t got = fread(f->field_buf, 1, f->record_len, f->fp);
if (got == 0)
f->eof_flag = true;
field_buf_to_vars(f);
}
/* ================================================================
* MBF Conversion Functions: CVI, CVS, CVD, MKI$, MKS$, MKD$
* ================================================================ */
gw_value_t gw_fn_cvi(gw_value_t *s)
{
if (s->type != VT_STR) gw_error(ERR_TM);
if (s->sval.len < 2) gw_error(ERR_FC);
gw_value_t v;
v.type = VT_INT;
v.ival = (int16_t)((uint8_t)s->sval.data[0] |
((uint8_t)s->sval.data[1] << 8));
gw_str_free(&s->sval);
return v;
}
gw_value_t gw_fn_cvs(gw_value_t *s)
{
if (s->type != VT_STR) gw_error(ERR_TM);
if (s->sval.len < 4) gw_error(ERR_FC);
gw_value_t v;
v.type = VT_SNG;
memcpy(&v.fval, s->sval.data, 4);
gw_str_free(&s->sval);
return v;
}
gw_value_t gw_fn_cvd(gw_value_t *s)
{
if (s->type != VT_STR) gw_error(ERR_TM);
if (s->sval.len < 8) gw_error(ERR_FC);
gw_value_t v;
v.type = VT_DBL;
memcpy(&v.dval, s->sval.data, 8);
gw_str_free(&s->sval);
return v;
}
gw_value_t gw_fn_mki(int16_t n)
{
gw_value_t v;
v.type = VT_STR;
v.sval = gw_str_alloc(2);
v.sval.data[0] = (char)(n & 0xFF);
v.sval.data[1] = (char)((n >> 8) & 0xFF);
return v;
}
gw_value_t gw_fn_mks(float f)
{
gw_value_t v;
v.type = VT_STR;
v.sval = gw_str_alloc(4);
memcpy(v.sval.data, &f, 4);
return v;
}
gw_value_t gw_fn_mkd(double d)
{
gw_value_t v;
v.type = VT_STR;
v.sval = gw_str_alloc(8);
memcpy(v.sval.data, &d, 8);
return v;
}
+145 -1
View File
@@ -542,6 +542,97 @@ void gw_stmt_mid_assign(void)
gw_str_free(&rhs.sval);
}
/* ================================================================
* CHAIN "filename" [, linenum] [, ALL]
* ================================================================ */
void gw_stmt_chain(void)
{
gw_skip_spaces();
/* Skip optional MERGE keyword */
bool merge = false;
if (gw_is_letter(gw_chrgot()) && toupper(gw_chrgot()) == 'M') {
/* Could be MERGE - skip the word */
uint8_t *save = gw.text_ptr;
while (gw_is_letter(gw_chrgot()))
gw_chrget();
gw_skip_spaces();
merge = true;
/* If no string follows, this wasn't MERGE */
if (gw_chrgot() != '"') {
gw.text_ptr = save;
merge = false;
}
}
gw_value_t fname_val = gw_eval_str();
char *filename = gw_str_to_cstr(&fname_val.sval);
gw_str_free(&fname_val.sval);
uint16_t start_line = 0;
bool has_start = false;
bool keep_all = false;
gw_skip_spaces();
if (gw_chrgot() == ',') {
gw_chrget();
gw_skip_spaces();
/* Optional line number */
uint8_t ch = gw_chrgot();
if ((ch >= 0x11 && ch <= 0x1A) || ch == TOK_INT1 || ch == TOK_INT2
|| ch == TOK_CONST_SNG || ch == TOK_CONST_DBL) {
start_line = gw_eval_uint16();
has_start = true;
}
gw_skip_spaces();
if (gw_chrgot() == ',') {
gw_chrget();
gw_skip_spaces();
/* ALL keyword - keep all variables */
if (gw_is_letter(gw_chrgot()) && toupper(gw_chrgot()) == 'A') {
keep_all = true;
while (gw_is_letter(gw_chrgot()))
gw_chrget();
}
}
}
/* Load the new program */
gw_stmt_load_internal(filename, !merge);
free(filename);
if (!keep_all && !merge) {
/* Clear variables unless ALL specified */
gw_vars_clear();
gw_arrays_clear();
}
/* Start execution */
program_line_t *start = gw.prog_head;
if (has_start) {
start = gw_find_line(start_line);
if (!start) gw_error(ERR_UL);
}
if (start) {
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;
gw.cur_line = start;
gw.text_ptr = start->tokens;
gw.cur_line_num = start->num;
gw.running = true;
gw_run_loop();
}
}
/* ================================================================
* Statement Dispatcher
* ================================================================ */
@@ -604,6 +695,43 @@ void gw_exec_stmt(void)
if (gw_hal) gw_hal->shutdown();
exit(0);
}
if (xstmt == XSTMT_CHAIN) {
gw_chrget();
gw_stmt_chain();
return;
}
if (xstmt == XSTMT_COMMON) {
/* COMMON var, var... - just skip, variables already in table */
gw_chrget();
while (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE)
gw.text_ptr++;
return;
}
if (xstmt == XSTMT_FIELD) {
gw_chrget();
gw_stmt_field();
return;
}
if (xstmt == XSTMT_LSET) {
gw_chrget();
gw_stmt_lset();
return;
}
if (xstmt == XSTMT_RSET) {
gw_chrget();
gw_stmt_rset();
return;
}
if (xstmt == XSTMT_PUT) {
gw_chrget();
gw_stmt_put();
return;
}
if (xstmt == XSTMT_GET) {
gw_chrget();
gw_stmt_get();
return;
}
/* Graphics/sound stubs - parse and discard arguments */
if (xstmt == XSTMT_CIRCLE || xstmt == XSTMT_DRAW ||
xstmt == XSTMT_PAINT || xstmt == XSTMT_PLAY ||
@@ -690,10 +818,26 @@ void gw_exec_stmt(void)
gw_chrget();
gw_skip_spaces();
/* RUN "filename" - load and run a file */
if (gw_chrgot() == '"') {
gw_value_t fname_val = gw_eval_str();
char *filename = gw_str_to_cstr(&fname_val.sval);
gw_str_free(&fname_val.sval);
gw_stmt_load_internal(filename, true);
free(filename);
if (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();
}
return;
}
/* RUN with line number */
program_line_t *start = gw.prog_head;
if (gw_chrgot() >= TOK_INT2 && gw_chrgot() <= TOK_CONST_DBL) {
/* has a line number arg */
uint16_t num = gw_eval_uint16();
start = gw_find_line(num);
if (!start) gw_error(ERR_UL);
+5 -3
View File
@@ -39,7 +39,9 @@ void gw_stmt_save(void)
}
/* Helper: load lines from a file into the program, optionally clearing first */
static void load_from_file(const char *filename, bool clear)
void gw_stmt_load_internal(const char *filename, bool clear);
void gw_stmt_load_internal(const char *filename, bool clear)
{
FILE *fp = fopen(filename, "r");
if (!fp)
@@ -120,7 +122,7 @@ void gw_stmt_load(void)
}
}
load_from_file(filename, true);
gw_stmt_load_internal(filename, true);
free(filename);
if (run_after && gw.prog_head) {
@@ -140,6 +142,6 @@ void gw_stmt_merge(void)
char *filename = gw_str_to_cstr(&fname_val.sval);
gw_str_free(&fname_val.sval);
load_from_file(filename, false);
gw_stmt_load_internal(filename, false);
free(filename);
}