From c2d73e9c24b9c613ec06e284025706fa45af7115 Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Tue, 10 Feb 2026 11:53:28 -0500 Subject: [PATCH] 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). --- CMakeLists.txt | 3 + include/gwbasic.h | 24 +- include/interp.h | 3 + include/types.h | 9 + src/eval.c | 33 +- src/fileio.c | 474 +++++++++++++++++++++++++++ src/interp.c | 189 ++++++++++- src/print.c | 8 + src/print_using.c | 514 ++++++++++++++++++++++++++++++ src/program_io.c | 145 +++++++++ tests/programs/file_io.bas | 11 + tests/programs/graphics_stubs.bas | 9 + tests/programs/mid_assign.bas | 7 + tests/programs/print_using.bas | 7 + tests/programs/save_load.bas | 3 + tests/programs/write_input.bas | 11 + 16 files changed, 1443 insertions(+), 7 deletions(-) create mode 100644 src/fileio.c create mode 100644 src/print_using.c create mode 100644 src/program_io.c create mode 100644 tests/programs/file_io.bas create mode 100644 tests/programs/graphics_stubs.bas create mode 100644 tests/programs/mid_assign.bas create mode 100644 tests/programs/print_using.bas create mode 100644 tests/programs/save_load.bas create mode 100644 tests/programs/write_input.bas diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ac3366..976a91c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 ) diff --git a/include/gwbasic.h b/include/gwbasic.h index b77816c..708e0ce 100644 --- a/include/gwbasic.h +++ b/include/gwbasic.h @@ -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; diff --git a/include/interp.h b/include/interp.h index 49588f1..7c27fff 100644 --- a/include/interp.h +++ b/include/interp.h @@ -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; diff --git a/include/types.h b/include/types.h index fc7579e..0be0016 100644 --- a/include/types.h +++ b/include/types.h @@ -3,6 +3,7 @@ #include #include +#include /* 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]; diff --git a/src/eval.c b/src/eval.c index a896b92..d9d084b 100644 --- a/src/eval.c +++ b/src/eval.c @@ -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(); diff --git a/src/fileio.c b/src/fileio.c new file mode 100644 index 0000000..b5b0cc8 --- /dev/null +++ b/src/fileio.c @@ -0,0 +1,474 @@ +#include "gwbasic.h" +#include +#include +#include + +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; +} diff --git a/src/interp.c b/src/interp.c index 9ed8ec0..8eec43c 100644 --- a/src/interp.c +++ b/src/interp.c @@ -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(); diff --git a/src/print.c b/src/print.c index 5670529..0e2ed3b 100644 --- a/src/print.c +++ b/src/print.c @@ -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) diff --git a/src/print_using.c b/src/print_using.c new file mode 100644 index 0000000..f1bc6a9 --- /dev/null +++ b/src/print_using.c @@ -0,0 +1,514 @@ +#include "gwbasic.h" +#include +#include +#include +#include + +/* + * 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); +} diff --git a/src/program_io.c b/src/program_io.c new file mode 100644 index 0000000..734ab7f --- /dev/null +++ b/src/program_io.c @@ -0,0 +1,145 @@ +#include "gwbasic.h" +#include +#include +#include + +/* 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); +} diff --git a/tests/programs/file_io.bas b/tests/programs/file_io.bas new file mode 100644 index 0000000..86cb4dd --- /dev/null +++ b/tests/programs/file_io.bas @@ -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$ diff --git a/tests/programs/graphics_stubs.bas b/tests/programs/graphics_stubs.bas new file mode 100644 index 0000000..83fcc65 --- /dev/null +++ b/tests/programs/graphics_stubs.bas @@ -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" diff --git a/tests/programs/mid_assign.bas b/tests/programs/mid_assign.bas new file mode 100644 index 0000000..af123f6 --- /dev/null +++ b/tests/programs/mid_assign.bas @@ -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$ diff --git a/tests/programs/print_using.bas b/tests/programs/print_using.bas new file mode 100644 index 0000000..91465e1 --- /dev/null +++ b/tests/programs/print_using.bas @@ -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 diff --git a/tests/programs/save_load.bas b/tests/programs/save_load.bas new file mode 100644 index 0000000..ee18360 --- /dev/null +++ b/tests/programs/save_load.bas @@ -0,0 +1,3 @@ +10 REM SAVE test +20 PRINT "Program saved" +30 SAVE "/tmp/gwbasic_save_test.bas" diff --git a/tests/programs/write_input.bas b/tests/programs/write_input.bas new file mode 100644 index 0000000..bde67ef --- /dev/null +++ b/tests/programs/write_input.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