diff --git a/README.md b/README.md index bd99f32..4082982 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Interactive mode launches the authentic GW-BASIC full-screen editor: ``` $ ./gwbasic -GW-BASIC 2026 0.6.0 +GW-BASIC 2026 0.7.0 (C) Eremey Valetov 2026. MIT License. Based on Microsoft GW-BASIC assembly source. Ok @@ -67,10 +67,10 @@ SPACE$, STRING$, HEX$, OCT$, INSTR, INPUT$ | Variables | LET, DIM, ERASE, SWAP, DEFINT/SNG/DBL/STR | | Control flow | GOTO, GOSUB/RETURN, FOR/NEXT, IF/THEN/ELSE, WHILE/WEND, ON...GOTO/GOSUB | | Input | INPUT, LINE INPUT, DATA/READ/RESTORE, INKEY$ | -| Program | RUN, RUN "file", CONT, STOP, END, NEW, LIST, CLEAR | +| Program | RUN, RUN "file", CONT, STOP, END, NEW, LIST, CLEAR, AUTO, RENUM, DELETE | | Sequential I/O | OPEN, CLOSE, PRINT#, WRITE#, INPUT#, LINE INPUT# | | Random-access I/O | FIELD, LSET, RSET, PUT, GET, CVI/CVS/CVD, MKI$/MKS$/MKD$ | -| Program I/O | SAVE, LOAD, MERGE, CHAIN | +| Program I/O | SAVE, LOAD, MERGE, CHAIN, COMMON | | Error handling | ON ERROR GOTO, RESUME, ERROR, ERR, ERL | | User functions | DEF FN, RANDOMIZE | | File management | KILL, NAME, FILES, MKDIR, RMDIR, CHDIR, SHELL | @@ -146,7 +146,7 @@ Key design differences from the original: ## Tests -52 test programs in `tests/programs/`, with CI via GitHub Actions: +53 test programs in `tests/programs/`, with CI via GitHub Actions: ```bash bash tests/run_tests.sh diff --git a/include/gwbasic.h b/include/gwbasic.h index aa4ed47..4962f8e 100644 --- a/include/gwbasic.h +++ b/include/gwbasic.h @@ -10,7 +10,7 @@ #include "gw_math.h" #include "strings.h" -#define GW_VERSION "0.6.0" +#define GW_VERSION "0.7.0" #define GW_BANNER "GW-BASIC " GW_VERSION /* Tokenizer */ diff --git a/include/interp.h b/include/interp.h index 7c27fff..8b9aad9 100644 --- a/include/interp.h +++ b/include/interp.h @@ -64,6 +64,15 @@ typedef struct { uint8_t *err_resume_text; program_line_t *err_resume_line; uint16_t err_line_num; + + /* AUTO mode */ + bool auto_mode; + uint16_t auto_line; + uint16_t auto_inc; + + /* COMMON variable list for CHAIN */ + int common_count; + struct { char name[2]; gw_valtype_t type; } common_vars[64]; } interp_state_t; extern interp_state_t gw; diff --git a/src/interp.c b/src/interp.c index 78d9599..7438e4a 100644 --- a/src/interp.c +++ b/src/interp.c @@ -96,6 +96,30 @@ void gw_free_program(void) gw.prog_head = NULL; } +/* Read a tokenized line number literal without expression evaluation. + * Returns true if a number was read, advances gw.text_ptr. */ +static bool read_linenum(uint16_t *out) +{ + gw_skip_spaces(); + uint8_t tok = gw_chrgot(); + if (tok >= 0x11 && tok <= 0x1A) { + *out = tok - 0x11; + gw_chrget(); + return true; + } + if (tok == TOK_INT1) { + *out = gw.text_ptr[1]; + gw.text_ptr += 2; + return true; + } + if (tok == TOK_INT2) { + *out = (uint16_t)(gw.text_ptr[1] | (gw.text_ptr[2] << 8)); + gw.text_ptr += 3; + return true; + } + return false; +} + /* ================================================================ * LIST * ================================================================ */ @@ -106,21 +130,21 @@ static void stmt_list(void) uint16_t start = 0, end = 65535; if (gw_chrgot() != 0 && gw_chrgot() != ':') { - /* Check for -end */ if (gw_chrgot() == TOK_MINUS) { gw_chrget(); - end = gw_eval_uint16(); + if (!read_linenum(&end)) gw_error(ERR_SN); } else { - start = gw_eval_uint16(); + if (!read_linenum(&start)) gw_error(ERR_SN); end = start; gw_skip_spaces(); if (gw_chrgot() == TOK_MINUS) { gw_chrget(); gw_skip_spaces(); - if (gw_chrgot() != 0 && gw_chrgot() != ':') - end = gw_eval_uint16(); - else + if (gw_chrgot() != 0 && gw_chrgot() != ':') { + if (!read_linenum(&end)) gw_error(ERR_SN); + } else { end = 65535; + } } } } @@ -629,15 +653,48 @@ void gw_stmt_chain(void) } } - /* Load the new program */ - gw_stmt_load_internal(filename, !merge); + /* Save COMMON state before load clears things */ + int saved_common_count = gw.common_count; + + if (!merge) { + /* Clear program but not variables — CHAIN manages variables below */ + gw_free_program(); + gw_file_close_all(); + memset(gw.fn_defs, 0, sizeof(gw.fn_defs)); + } + + /* Load the new program without clearing */ + gw_stmt_load_internal(filename, false); free(filename); if (!keep_all && !merge) { - /* Clear variables unless ALL specified */ - gw_vars_clear(); - gw_arrays_clear(); + if (saved_common_count > 0) { + /* Preserve only COMMON variables, clear the rest */ + for (int i = gw.var_count - 1; i >= 0; i--) { + bool keep = false; + for (int j = 0; j < saved_common_count; j++) { + if (gw.vars[i].name[0] == gw.common_vars[j].name[0] && + gw.vars[i].name[1] == gw.common_vars[j].name[1] && + gw.vars[i].type == gw.common_vars[j].type) { + keep = true; + break; + } + } + if (!keep) { + if (gw.vars[i].type == VT_STR) + gw_str_free(&gw.vars[i].val.sval); + if (i < gw.var_count - 1) + gw.vars[i] = gw.vars[gw.var_count - 1]; + gw.var_count--; + } + } + gw_arrays_clear(); + } else { + gw_vars_clear(); + gw_arrays_clear(); + } } + gw.common_count = 0; /* Start execution */ program_line_t *start = gw.prog_head; @@ -733,10 +790,30 @@ void gw_exec_stmt(void) 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++; + for (;;) { + gw_skip_spaces(); + if (!gw_chrgot() || gw_chrgot() == ':' || gw_chrgot() == TOK_ELSE) + break; + char name[2]; + gw_valtype_t type = gw_parse_varname(name); + /* Skip array parens if present: COMMON A() */ + gw_skip_spaces(); + if (gw_chrgot() == '(') { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() == ')') gw_chrget(); + } + if (gw.common_count < 64) { + gw.common_vars[gw.common_count].name[0] = name[0]; + gw.common_vars[gw.common_count].name[1] = name[1]; + gw.common_vars[gw.common_count].type = type; + gw.common_count++; + } + gw_skip_spaces(); + if (gw_chrgot() == ',') { gw_chrget(); continue; } + break; + } return; } if (xstmt == XSTMT_FIELD) { @@ -1163,6 +1240,256 @@ void gw_exec_stmt(void) return; } + /* DELETE [start]-[end] */ + if (tok == TOK_DELETE) { + gw_chrget(); + gw_skip_spaces(); + uint16_t start = 0, end = 65535; + if (gw_chrgot() == TOK_MINUS) { + gw_chrget(); + if (!read_linenum(&end)) gw_error(ERR_SN); + } else { + if (!read_linenum(&start)) gw_error(ERR_SN); + end = start; + gw_skip_spaces(); + if (gw_chrgot() == TOK_MINUS) { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() != 0 && gw_chrgot() != ':') { + if (!read_linenum(&end)) gw_error(ERR_SN); + } else { + end = 65535; + } + } + } + if (!gw_find_line(start) && start == end) + gw_error(ERR_FC); + program_line_t **pp = &gw.prog_head; + while (*pp) { + if ((*pp)->num >= start && (*pp)->num <= end) { + program_line_t *del = *pp; + *pp = del->next; + free(del->tokens); + free(del); + } else { + pp = &(*pp)->next; + } + } + gw.cont_text = NULL; + gw.cont_line = NULL; + return; + } + + /* AUTO [start[,increment]] */ + if (tok == TOK_AUTO) { + gw_chrget(); + gw_skip_spaces(); + uint16_t start = 10, inc = 10; + if (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != ',' && gw_chrgot() != TOK_ELSE) { + if (!read_linenum(&start)) gw_error(ERR_SN); + } + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + if (!read_linenum(&inc)) gw_error(ERR_SN); + } + if (inc == 0) gw_error(ERR_FC); + gw.auto_mode = true; + gw.auto_line = start; + gw.auto_inc = inc; + return; + } + + /* RENUM [new[,old[,increment]]] */ + if (tok == TOK_RENUM) { + gw_chrget(); + gw_skip_spaces(); + uint16_t new_start = 10, old_start = 0, inc = 10; + if (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != TOK_ELSE && + gw_chrgot() != ',') { + if (!read_linenum(&new_start)) gw_error(ERR_SN); + } + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() != ',' && gw_chrgot() != 0 && gw_chrgot() != ':') + if (!read_linenum(&old_start)) gw_error(ERR_SN); + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + if (!read_linenum(&inc)) gw_error(ERR_SN); + } + } + if (inc == 0) gw_error(ERR_FC); + + /* Count lines to renumber and build mapping table */ + int count = 0; + for (program_line_t *p = gw.prog_head; p; p = p->next) + if (p->num >= old_start) count++; + if (count == 0) return; + + /* Check if new numbers would overflow */ + uint32_t last = (uint32_t)new_start + (uint32_t)(count - 1) * inc; + if (last > 65529) gw_error(ERR_FC); + + /* Build old->new mapping */ + uint16_t *old_nums = malloc(count * sizeof(uint16_t)); + uint16_t *new_nums = malloc(count * sizeof(uint16_t)); + if (!old_nums || !new_nums) { free(old_nums); free(new_nums); gw_error(ERR_OM); } + + int idx = 0; + uint16_t nn = new_start; + for (program_line_t *p = gw.prog_head; p; p = p->next) { + if (p->num >= old_start) { + old_nums[idx] = p->num; + new_nums[idx] = nn; + nn += inc; + idx++; + } + } + + /* Patch line number references in all program lines */ + for (program_line_t *p = gw.prog_head; p; p = p->next) { + uint8_t *t = p->tokens; + while (*t) { + uint8_t tok_b = *t; + /* Tokens that take a line number argument */ + if (tok_b == TOK_GOTO || tok_b == TOK_GOSUB || + tok_b == TOK_THEN || tok_b == TOK_RESTORE || + tok_b == TOK_RUN || tok_b == TOK_RESUME) { + t++; + while (*t == ' ') t++; + /* Read the encoded line number */ + uint16_t ref = 0; + uint8_t *numpos = t; + int numlen = 0; + if (*t >= 0x11 && *t <= 0x1A) { + ref = *t - 0x11; + numlen = 1; + } else if (*t == TOK_INT1) { + ref = t[1]; + numlen = 2; + } else if (*t == TOK_INT2) { + ref = (uint16_t)(t[1] | (t[2] << 8)); + numlen = 3; + } else { + continue; + } + /* Look up in mapping */ + for (int i = 0; i < count; i++) { + if (old_nums[i] == ref) { + /* Rewrite the number in the token stream */ + uint16_t nv = new_nums[i]; + int new_numlen; + uint8_t nbuf[3]; + if (nv <= 9) { + nbuf[0] = 0x11 + nv; + new_numlen = 1; + } else if (nv <= 255) { + nbuf[0] = TOK_INT1; + nbuf[1] = nv; + new_numlen = 2; + } else { + nbuf[0] = TOK_INT2; + nbuf[1] = nv & 0xFF; + nbuf[2] = (nv >> 8) & 0xFF; + new_numlen = 3; + } + if (new_numlen == numlen) { + memcpy(numpos, nbuf, new_numlen); + } else { + /* Need to resize the token buffer */ + int old_len = p->len; + int diff = new_numlen - numlen; + int offset = numpos - p->tokens; + uint8_t *newbuf = malloc(old_len + diff + 1); + if (!newbuf) { free(old_nums); free(new_nums); gw_error(ERR_OM); } + memcpy(newbuf, p->tokens, offset); + memcpy(newbuf + offset, nbuf, new_numlen); + memcpy(newbuf + offset + new_numlen, + p->tokens + offset + numlen, + old_len - offset - numlen + 1); + free(p->tokens); + p->tokens = newbuf; + p->len = old_len + diff; + t = p->tokens + offset + new_numlen; + continue; + } + break; + } + } + t = numpos + numlen; + /* ON x GOTO/GOSUB can have comma-separated line numbers */ + while (*t == ' ') t++; + while (*t == ',') { + t++; + while (*t == ' ') t++; + numpos = t; + numlen = 0; + ref = 0; + if (*t >= 0x11 && *t <= 0x1A) { + ref = *t - 0x11; numlen = 1; + } else if (*t == TOK_INT1) { + ref = t[1]; numlen = 2; + } else if (*t == TOK_INT2) { + ref = (uint16_t)(t[1] | (t[2] << 8)); numlen = 3; + } else break; + for (int i = 0; i < count; i++) { + if (old_nums[i] == ref) { + uint16_t nv = new_nums[i]; + int new_numlen; + uint8_t nbuf[3]; + if (nv <= 9) { nbuf[0] = 0x11 + nv; new_numlen = 1; } + else if (nv <= 255) { nbuf[0] = TOK_INT1; nbuf[1] = nv; new_numlen = 2; } + else { nbuf[0] = TOK_INT2; nbuf[1] = nv & 0xFF; nbuf[2] = (nv >> 8) & 0xFF; new_numlen = 3; } + if (new_numlen == numlen) { + memcpy(numpos, nbuf, new_numlen); + } else { + int old_len = p->len; + int diff = new_numlen - numlen; + int offset = numpos - p->tokens; + uint8_t *newbuf = malloc(old_len + diff + 1); + if (!newbuf) { free(old_nums); free(new_nums); gw_error(ERR_OM); } + memcpy(newbuf, p->tokens, offset); + memcpy(newbuf + offset, nbuf, new_numlen); + memcpy(newbuf + offset + new_numlen, + p->tokens + offset + numlen, + old_len - offset - numlen + 1); + free(p->tokens); + p->tokens = newbuf; + p->len = old_len + diff; + t = p->tokens + offset + new_numlen; + numpos = t - new_numlen; + numlen = new_numlen; + } + break; + } + } + t = numpos + numlen; + while (*t == ' ') t++; + } + continue; + } + t++; + } + } + + /* Assign new line numbers */ + idx = 0; + for (program_line_t *p = gw.prog_head; p; p = p->next) { + if (p->num >= old_start) { + p->num = new_nums[idx++]; + } + } + + free(old_nums); + free(new_nums); + gw.cont_text = NULL; + gw.cont_line = NULL; + return; + } + /* GOTO */ if (tok == TOK_GOTO) { gw_chrget(); diff --git a/src/main.c b/src/main.c index cb935ee..da29c1a 100644 --- a/src/main.c +++ b/src/main.c @@ -233,12 +233,47 @@ int main(int argc, char **argv) print_banner(interactive); for (;;) { - if (interactive) { + if (interactive && !gw.auto_mode) { gw_hal->puts("Ok\n"); } - if (setjmp(gw_error_jmp) != 0) + if (setjmp(gw_error_jmp) != 0) { + gw.auto_mode = false; continue; + } + + /* AUTO mode: display line number prompt and prepend it to input */ + if (gw.auto_mode && interactive) { + char prompt[16]; + bool exists = gw_find_line(gw.auto_line) != NULL; + snprintf(prompt, sizeof(prompt), "%u%s", gw.auto_line, + exists ? "*" : " "); + gw_hal->puts(prompt); + + char *line = tui_read_line(); + if (!line || tui.break_flag) { + gw.auto_mode = false; + tui.break_flag = false; + continue; + } + /* Empty input exits AUTO mode */ + if (line[0] == '\0') { + gw.auto_mode = false; + continue; + } + + char full[300]; + snprintf(full, sizeof(full), "%u %s", gw.auto_line, line); + gw_exec_direct(full); + + uint32_t next = (uint32_t)gw.auto_line + gw.auto_inc; + if (next > 65529) { + gw.auto_mode = false; + } else { + gw.auto_line = (uint16_t)next; + } + continue; + } char *line = interactive ? tui_read_line() : read_line(); if (!line) diff --git a/tests/programs/common_target.bas b/tests/programs/common_target.bas new file mode 100644 index 0000000..822617d --- /dev/null +++ b/tests/programs/common_target.bas @@ -0,0 +1,5 @@ +10 REM Target for COMMON test +20 IF X = 42 THEN PRINT "X preserved" ELSE PRINT "FAIL: X lost" +30 IF N$ = "hello" THEN PRINT "N$ preserved" ELSE PRINT "FAIL: N$ lost" +40 IF Y = 0 THEN PRINT "Y cleared" ELSE PRINT "FAIL: Y kept" +50 PRINT "COMMON test passed" diff --git a/tests/programs/common_test.bas b/tests/programs/common_test.bas new file mode 100644 index 0000000..658b697 --- /dev/null +++ b/tests/programs/common_test.bas @@ -0,0 +1,6 @@ +10 REM COMMON test - verify variables preserved across CHAIN +20 COMMON X, N$ +30 X = 42 +40 N$ = "hello" +50 Y = 99 +60 CHAIN "tests/programs/common_target.bas" diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 1dc8642..ca5ae03 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -21,8 +21,9 @@ compat_fail=0 for bas in "$SCRIPT_DIR"/programs/*.bas; do name="$(basename "$bas")" stem="${name%.bas}" - # chain_target.bas is not standalone + # chain/common targets are not standalone [ "$name" = "chain_target.bas" ] && continue + [ "$name" = "common_target.bas" ] && continue actual=$(mktemp) if timeout 5 "$GWBASIC" "$bas" > "$actual" 2>&1; then