diff --git a/CMakeLists.txt b/CMakeLists.txt index 1da2ad0..0ac3366 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,10 @@ set(SOURCES src/tokenizer.c src/error.c src/eval.c + src/interp.c + src/vars.c + src/arrays.c + src/input.c src/math_int.c src/math_float.c src/math_transcend.c diff --git a/include/gwbasic.h b/include/gwbasic.h index 0938048..b77816c 100644 --- a/include/gwbasic.h +++ b/include/gwbasic.h @@ -10,7 +10,7 @@ #include "gw_math.h" #include "strings.h" -#define GW_VERSION "0.1.0" +#define GW_VERSION "0.2.0" #define GW_BANNER "GW-BASIC " GW_VERSION /* Tokenizer */ @@ -22,4 +22,29 @@ void gw_stmt_print(void); void gw_print_value(gw_value_t *v); void gw_print_newline(void); +/* Variables (vars.c) */ +gw_valtype_t gw_parse_varname(char name_out[2]); +var_entry_t *gw_var_find_or_create(const char name[2], gw_valtype_t type); +void gw_var_assign(var_entry_t *var, gw_value_t *val); +void gw_vars_clear(void); +void gw_stmt_deftype(gw_valtype_t type); +void gw_stmt_swap(void); + +/* Arrays (arrays.c) */ +void gw_stmt_dim(void); +gw_value_t *gw_array_element(const char name[2], gw_valtype_t type); +void gw_stmt_erase(void); +void gw_stmt_option(void); +void gw_arrays_clear(void); + +/* Input (input.c) */ +void gw_stmt_input(void); +void gw_stmt_line_input(void); + +/* DEF FN evaluation (interp.c) */ +gw_value_t gw_eval_fn_call(void); + +/* Error recovery for run loop */ +extern jmp_buf gw_run_jmp; + #endif diff --git a/include/interp.h b/include/interp.h index 393760b..49588f1 100644 --- a/include/interp.h +++ b/include/interp.h @@ -24,9 +24,43 @@ typedef struct { /* Default variable types for A-Z (DEFTBL) */ gw_valtype_t def_type[26]; + /* Variable storage */ + var_entry_t vars[256]; + int var_count; + array_entry_t arrays[64]; + int array_count; + int option_base; + + /* Control flow stacks */ + for_entry_t for_stack[16]; + int for_sp; + gosub_entry_t gosub_stack[24]; + int gosub_sp; + while_entry_t while_stack[16]; + int while_sp; + + /* DEF FN */ + fn_def_t fn_defs[26]; + + /* Run state */ + program_line_t *cur_line; + bool running; + + /* CONT state */ + uint8_t *cont_text; + program_line_t *cont_line; + /* DATA pointer */ uint8_t *data_ptr; + program_line_t *data_line_ptr; uint16_t data_line; + + /* Error trapping */ + uint16_t on_error_line; + bool in_error_handler; + uint8_t *err_resume_text; + program_line_t *err_resume_line; + uint16_t err_line_num; } interp_state_t; extern interp_state_t gw; @@ -35,8 +69,15 @@ extern interp_state_t gw; #define LINE_DIRECT 65535 void gw_init(void); -void gw_exec_line(uint8_t *tokens); void gw_exec_direct(const char *line); +void gw_exec_stmt(void); +void gw_run_loop(void); + +/* Program storage */ +void gw_store_line(uint16_t num, uint8_t *tokens, int len); +void gw_delete_line(uint16_t num); +program_line_t *gw_find_line(uint16_t num); +void gw_free_program(void); /* CHRGET/CHRGOT - advance/peek the text pointer */ uint8_t gw_chrget(void); diff --git a/include/types.h b/include/types.h index a2fe04e..fc7579e 100644 --- a/include/types.h +++ b/include/types.h @@ -44,6 +44,48 @@ typedef struct { gw_value_t val; } var_entry_t; +/* Array entry */ +typedef struct { + char name[2]; + gw_valtype_t type; + int ndims; + int dims[8]; /* max dimensions per DIM */ + int total_elements; + gw_value_t *data; /* malloc'd flat array */ +} array_entry_t; + +/* FOR stack entry */ +typedef struct { + var_entry_t *var; + gw_value_t limit, step; + uint8_t *loop_text; + struct program_line *loop_line; + uint16_t line_num; +} for_entry_t; + +/* GOSUB stack entry */ +typedef struct { + uint8_t *ret_text; + struct program_line *ret_line; + uint16_t line_num; +} gosub_entry_t; + +/* WHILE stack entry */ +typedef struct { + uint8_t *while_text; + struct program_line *while_line; + uint16_t line_num; +} while_entry_t; + +/* DEF FN entry */ +typedef struct { + bool defined; + char param_name[2]; + gw_valtype_t param_type, ret_type; + uint8_t *body_text; + struct program_line *body_line; +} fn_def_t; + /* MBF (Microsoft Binary Format) types for file compatibility */ typedef struct { uint8_t mantissa[3]; diff --git a/src/arrays.c b/src/arrays.c new file mode 100644 index 0000000..c0b48e8 --- /dev/null +++ b/src/arrays.c @@ -0,0 +1,186 @@ +#include "gwbasic.h" +#include +#include +#include + +/* + * Array support - reimplements DIMCON/VARGET from GWMAIN.ASM. + * Column-major subscript calculation (leftmost varies fastest). + * Default bounds 0-10 (11 elements) if not DIMmed. + */ + +static array_entry_t *find_array(const char name[2], gw_valtype_t type) +{ + for (int i = 0; i < gw.array_count; i++) { + if (gw.arrays[i].name[0] == name[0] && gw.arrays[i].name[1] == name[1] + && gw.arrays[i].type == type) + return &gw.arrays[i]; + } + return NULL; +} + +static array_entry_t *create_array(const char name[2], gw_valtype_t type, + int ndims, const int *dims) +{ + if (gw.array_count >= 64) + gw_error(ERR_OM); + + array_entry_t *a = &gw.arrays[gw.array_count++]; + a->name[0] = name[0]; + a->name[1] = name[1]; + a->type = type; + a->ndims = ndims; + + int total = 1; + for (int i = 0; i < ndims; i++) { + a->dims[i] = dims[i]; + total *= (dims[i] - gw.option_base + 1); + } + a->total_elements = total; + a->data = calloc(total, sizeof(gw_value_t)); + if (!a->data) + gw_error(ERR_OM); + + for (int i = 0; i < total; i++) + a->data[i].type = type; + + return a; +} + +/* Parse subscripts and return pointer to element */ +gw_value_t *gw_array_element(const char name[2], gw_valtype_t type) +{ + gw_expect('('); + + int subs[8]; + int nsubs = 0; + for (;;) { + if (nsubs >= 8) + gw_error(ERR_BS); + subs[nsubs++] = gw_eval_int(); + gw_skip_spaces(); + if (gw_chrgot() != ',') + break; + gw_chrget(); + } + gw_expect_rparen(); + + array_entry_t *a = find_array(name, type); + if (!a) { + /* Auto-DIM with default bounds 0-10 */ + int dims[8]; + for (int i = 0; i < nsubs; i++) + dims[i] = 10; + a = create_array(name, type, nsubs, dims); + } + + if (nsubs != a->ndims) + gw_error(ERR_BS); + + /* Column-major index calculation */ + int index = 0; + int multiplier = 1; + for (int i = 0; i < nsubs; i++) { + int s = subs[i] - gw.option_base; + int dim_size = a->dims[i] - gw.option_base + 1; + if (s < 0 || s >= dim_size) + gw_error(ERR_BS); + index += s * multiplier; + multiplier *= dim_size; + } + + return &a->data[index]; +} + +void gw_stmt_dim(void) +{ + for (;;) { + char name[2]; + gw_valtype_t type = gw_parse_varname(name); + + gw_skip_spaces(); + gw_expect('('); + + int dims[8]; + int ndims = 0; + for (;;) { + if (ndims >= 8) + gw_error(ERR_BS); + dims[ndims++] = gw_eval_int(); + gw_skip_spaces(); + if (gw_chrgot() != ',') + break; + gw_chrget(); + } + gw_expect_rparen(); + + if (find_array(name, type)) + gw_error(ERR_DD); + + create_array(name, type, ndims, dims); + + gw_skip_spaces(); + if (gw_chrgot() != ',') + break; + gw_chrget(); + } +} + +void gw_stmt_erase(void) +{ + for (;;) { + char name[2]; + gw_valtype_t type = gw_parse_varname(name); + + array_entry_t *a = find_array(name, type); + if (!a) + gw_error(ERR_FC); + + if (a->type == VT_STR) { + for (int i = 0; i < a->total_elements; i++) + gw_str_free(&a->data[i].sval); + } + free(a->data); + + /* Remove from array by swapping with last */ + int idx = a - gw.arrays; + gw.arrays[idx] = gw.arrays[--gw.array_count]; + + gw_skip_spaces(); + if (gw_chrgot() != ',') + break; + gw_chrget(); + } +} + +void gw_stmt_option(void) +{ + /* OPTION BASE 0|1 */ + gw_skip_spaces(); + /* Expect "BASE" - it tokenizes as a keyword or letters */ + uint8_t ch = gw_chrgot(); + /* Skip the word BASE (may be tokenized as letters) */ + if (gw_is_letter(ch)) { + while (gw_is_letter(gw_chrgot())) + gw_chrget(); + } + gw_skip_spaces(); + int base = gw_eval_int(); + if (base != 0 && base != 1) + gw_error(ERR_FC); + if (gw.array_count > 0) + gw_error(ERR_FC); /* can't change after arrays exist */ + gw.option_base = base; +} + +void gw_arrays_clear(void) +{ + for (int i = 0; i < gw.array_count; i++) { + if (gw.arrays[i].type == VT_STR) { + for (int j = 0; j < gw.arrays[i].total_elements; j++) + gw_str_free(&gw.arrays[i].data[j].sval); + } + free(gw.arrays[i].data); + } + gw.array_count = 0; +} diff --git a/src/error.c b/src/error.c index 37da1b2..d4e6c1c 100644 --- a/src/error.c +++ b/src/error.c @@ -1,6 +1,7 @@ #include "error.h" #include "hal.h" #include "interp.h" +#include "gwbasic.h" #include #include @@ -66,6 +67,22 @@ const char *gw_error_msg(int errnum) void gw_error(int errnum) { gw_errno = errnum; + gw.err_line_num = gw.cur_line_num; + + /* ON ERROR GOTO handler */ + if (gw.on_error_line && gw.running && !gw.in_error_handler) { + program_line_t *target = gw_find_line(gw.on_error_line); + if (target) { + gw.in_error_handler = true; + gw.err_resume_text = gw.text_ptr; + gw.err_resume_line = gw.cur_line; + gw.cur_line = target; + gw.text_ptr = target->tokens; + gw.cur_line_num = target->num; + longjmp(gw_run_jmp, 1); + } + } + const char *msg = gw_error_msg(errnum); char buf[80]; @@ -79,5 +96,6 @@ void gw_error(int errnum) else fputs(buf, stderr); + gw.running = false; longjmp(gw_error_jmp, errnum); } diff --git a/src/eval.c b/src/eval.c index b005993..a896b92 100644 --- a/src/eval.c +++ b/src/eval.c @@ -812,6 +812,35 @@ static gw_value_t eval_atom(void) gw.text_ptr = save; } + /* FN call */ + if (tok == TOK_FN) { + gw_chrget(); + return gw_eval_fn_call(); + } + + /* Variable or array element */ + if (gw_is_letter(tok)) { + char name[2]; + gw_valtype_t type = gw_parse_varname(name); + + gw_skip_spaces(); + if (gw_chrgot() == '(') { + /* Array element */ + gw_value_t *elem = gw_array_element(name, type); + gw_value_t v = *elem; + if (v.type == VT_STR && v.sval.data) + v.sval = gw_str_copy(&elem->sval); + return v; + } + + /* Scalar variable */ + var_entry_t *var = gw_var_find_or_create(name, type); + gw_value_t v = var->val; + if (v.type == VT_STR && v.sval.data) + v.sval = gw_str_copy(&var->val.sval); + return v; + } + /* End of expression */ if (tok == 0 || tok == ':' || tok == ')' || tok == ',' || tok == ';' || tok == TOK_THEN || tok == TOK_ELSE diff --git a/src/input.c b/src/input.c new file mode 100644 index 0000000..454acee --- /dev/null +++ b/src/input.c @@ -0,0 +1,212 @@ +#include "gwbasic.h" +#include +#include +#include +#include + +/* + * INPUT and LINE INPUT statements. + * INPUT: prompt, read comma-separated values into variables. + * LINE INPUT: read entire line as string. + */ + +static char *read_input_line(void) +{ + static char buf[256]; + if (fgets(buf, sizeof(buf), stdin) == NULL) + return NULL; + int len = strlen(buf); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) + buf[--len] = '\0'; + return buf; +} + +void gw_stmt_input(void) +{ + gw_skip_spaces(); + + /* Optional semicolon after INPUT (suppresses CR on prompt) */ + /* INPUT [;]["prompt";] var, var, ... */ + bool no_cr = false; + if (gw_chrgot() == ';') { + no_cr = true; + gw_chrget(); + gw_skip_spaces(); + } + (void)no_cr; + + /* Check for prompt string */ + if (gw_chrgot() == '"') { + gw.text_ptr++; + while (*gw.text_ptr && *gw.text_ptr != '"') { + if (gw_hal) gw_hal->putch(*gw.text_ptr); + else putchar(*gw.text_ptr); + gw.text_ptr++; + } + if (*gw.text_ptr == '"') + gw.text_ptr++; + + gw_skip_spaces(); + if (gw_chrgot() == ';') { + gw_chrget(); + } else if (gw_chrgot() == ',') { + gw_chrget(); + } + } + +retry:; + /* Print "? " */ + if (gw_hal) gw_hal->puts("? "); + else fputs("? ", stdout); + fflush(stdout); + + char *line = read_input_line(); + if (!line) return; + + const char *p = line; + + /* Parse variable list and assign values */ + int var_idx = 0; + 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; + 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); + } + + /* Skip whitespace in input */ + while (*p == ' ') p++; + + /* Parse value from input line */ + gw_value_t val; + if (type == VT_STR) { + /* Read string: until comma or end of line */ + const char *start = p; + if (*p == '"') { + /* Quoted string */ + p++; + start = p; + while (*p && *p != '"') p++; + int len = p - start; + val.type = VT_STR; + val.sval = gw_str_alloc(len); + memcpy(val.sval.data, start, len); + if (*p == '"') p++; + } else { + while (*p && *p != ',') p++; + int len = p - start; + /* Trim trailing spaces */ + while (len > 0 && start[len - 1] == ' ') len--; + val.type = VT_STR; + val.sval = gw_str_alloc(len); + memcpy(val.sval.data, start, len); + } + } 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++; + var_idx++; + + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + } else { + break; + } + } + + /* If we have more variables than input, redo */ + gw_skip_spaces(); + if (gw_chrgot() == ',') { + if (gw_hal) gw_hal->puts("?Redo from start\n"); + else fputs("?Redo from start\n", stdout); + /* Need to re-read, but for simplicity continue */ + goto retry; + } +} + +void gw_stmt_line_input(void) +{ + gw_skip_spaces(); + + /* LINE INPUT [;]["prompt";] string-variable */ + if (gw_chrgot() == ';') { + gw_chrget(); + gw_skip_spaces(); + } + + /* Check for prompt string */ + if (gw_chrgot() == '"') { + gw.text_ptr++; + while (*gw.text_ptr && *gw.text_ptr != '"') { + if (gw_hal) gw_hal->putch(*gw.text_ptr); + else putchar(*gw.text_ptr); + gw.text_ptr++; + } + if (*gw.text_ptr == '"') + gw.text_ptr++; + 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); + + fflush(stdout); + char *line = read_input_line(); + if (!line) line = ""; + + gw_value_t val; + val.type = VT_STR; + val.sval = gw_str_from_cstr(line); + + gw_skip_spaces(); + var_entry_t *var; + if (gw_chrgot() == '(') { + gw_value_t *elem = gw_array_element(name, type); + gw_str_free(&elem->sval); + elem->sval = val.sval; + elem->type = VT_STR; + } else { + var = gw_var_find_or_create(name, type); + gw_var_assign(var, &val); + } +} diff --git a/src/interp.c b/src/interp.c new file mode 100644 index 0000000..9ed8ec0 --- /dev/null +++ b/src/interp.c @@ -0,0 +1,1445 @@ +#include "gwbasic.h" +#include +#include +#include +#include +#include + +/* + * Execution loop and control flow - the heart of the interpreter. + * Reimplements NEWSTT, GOTO, GOSUB, FOR/NEXT, IF/THEN/ELSE, etc. + */ + +jmp_buf gw_run_jmp; + +/* ================================================================ + * Program Storage + * ================================================================ */ + +void gw_store_line(uint16_t num, uint8_t *tokens, int len) +{ + /* Delete existing line with this number first */ + gw_delete_line(num); + + if (len == 0) + return; /* just a deletion */ + + program_line_t *line = malloc(sizeof(program_line_t)); + if (!line) gw_error(ERR_OM); + line->num = num; + line->len = len; + line->tokens = malloc(len + 1); + if (!line->tokens) { free(line); gw_error(ERR_OM); } + memcpy(line->tokens, tokens, len); + line->tokens[len] = 0; + + /* Insert in sorted order */ + program_line_t **pp = &gw.prog_head; + while (*pp && (*pp)->num < num) + pp = &(*pp)->next; + line->next = *pp; + *pp = line; +} + +void gw_delete_line(uint16_t num) +{ + program_line_t **pp = &gw.prog_head; + while (*pp) { + if ((*pp)->num == num) { + program_line_t *del = *pp; + *pp = del->next; + free(del->tokens); + free(del); + return; + } + pp = &(*pp)->next; + } +} + +program_line_t *gw_find_line(uint16_t num) +{ + program_line_t *p = gw.prog_head; + while (p) { + if (p->num == num) + return p; + if (p->num > num) + break; + p = p->next; + } + return NULL; +} + +void gw_free_program(void) +{ + program_line_t *p = gw.prog_head; + while (p) { + program_line_t *next = p->next; + free(p->tokens); + free(p); + p = next; + } + gw.prog_head = NULL; +} + +/* ================================================================ + * LIST + * ================================================================ */ + +static void stmt_list(void) +{ + gw_skip_spaces(); + 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(); + } else { + start = gw_eval_uint16(); + 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 + end = 65535; + } + } + } + + char listbuf[512]; + program_line_t *p = gw.prog_head; + while (p) { + if (p->num >= start && p->num <= end) { + gw_list_line(p->tokens, p->len, listbuf, sizeof(listbuf)); + char line[560]; + snprintf(line, sizeof(line), "%u %s\n", p->num, listbuf); + if (gw_hal) gw_hal->puts(line); + else fputs(line, stdout); + } + if (p->num > end) + break; + p = p->next; + } +} + +/* ================================================================ + * DATA/READ/RESTORE + * ================================================================ */ + +/* Skip a DATA statement's items (used during execution) */ +static void skip_data(void) +{ + while (*gw.text_ptr && *gw.text_ptr != ':') + gw.text_ptr++; +} + +/* Find the next DATA statement in the program starting from current data pointer */ +static bool advance_data_ptr(void) +{ + /* If no data pointer set, start from program beginning */ + if (!gw.data_line_ptr) { + gw.data_line_ptr = gw.prog_head; + if (!gw.data_line_ptr) return false; + gw.data_ptr = gw.data_line_ptr->tokens; + } + + for (;;) { + /* Scan current line for DATA token */ + while (*gw.data_ptr) { + if (*gw.data_ptr == TOK_DATA) { + gw.data_ptr++; /* skip DATA token */ + return true; + } + /* Skip string literals */ + if (*gw.data_ptr == '"') { + gw.data_ptr++; + while (*gw.data_ptr && *gw.data_ptr != '"') + gw.data_ptr++; + if (*gw.data_ptr == '"') + gw.data_ptr++; + continue; + } + /* Skip REM to end of line */ + if (*gw.data_ptr == TOK_REM || *gw.data_ptr == TOK_SQUOTE) + break; + gw.data_ptr++; + } + + /* Move to next program line */ + gw.data_line_ptr = gw.data_line_ptr->next; + if (!gw.data_line_ptr) + return false; + gw.data_ptr = gw.data_line_ptr->tokens; + } +} + +/* Read one data item as a string (raw text from DATA statement) */ +static void read_data_item(char *buf, int bufsize) +{ + if (!gw.data_ptr || !*gw.data_ptr || *gw.data_ptr == ':') { + if (!advance_data_ptr()) + gw_error(ERR_OD); + } + + /* Skip spaces */ + while (*gw.data_ptr == ' ') gw.data_ptr++; + + int i = 0; + if (*gw.data_ptr == '"') { + /* Quoted string */ + gw.data_ptr++; + while (*gw.data_ptr && *gw.data_ptr != '"' && i < bufsize - 1) + buf[i++] = *gw.data_ptr++; + if (*gw.data_ptr == '"') gw.data_ptr++; + } else { + /* Unquoted: read until comma, colon, or end */ + while (*gw.data_ptr && *gw.data_ptr != ',' && *gw.data_ptr != ':' + && i < bufsize - 1) { + buf[i++] = *gw.data_ptr++; + } + /* Trim trailing spaces */ + while (i > 0 && buf[i - 1] == ' ') i--; + } + buf[i] = '\0'; + + /* Skip comma separator */ + while (*gw.data_ptr == ' ') gw.data_ptr++; + if (*gw.data_ptr == ',') + gw.data_ptr++; +} + +static void stmt_read(void) +{ + for (;;) { + gw_skip_spaces(); + + char name[2]; + gw_valtype_t type = gw_parse_varname(name); + + gw_skip_spaces(); + gw_value_t *arr_elem = NULL; + var_entry_t *var = NULL; + if (gw_chrgot() == '(') { + arr_elem = gw_array_element(name, type); + } else { + var = gw_var_find_or_create(name, type); + } + + char item[256]; + read_data_item(item, sizeof(item)); + + gw_value_t val; + if (type == VT_STR) { + val.type = VT_STR; + val.sval = gw_str_from_cstr(item); + } else { + char *end; + double d = strtod(item, &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); + } + + gw_skip_spaces(); + if (gw_chrgot() != ',') + break; + gw_chrget(); + } +} + +static void stmt_restore(void) +{ + gw_skip_spaces(); + if (gw_chrgot() != 0 && gw_chrgot() != ':') { + uint16_t num = gw_eval_uint16(); + gw.data_line_ptr = gw_find_line(num); + if (!gw.data_line_ptr) + gw_error(ERR_UL); + gw.data_ptr = gw.data_line_ptr->tokens; + } else { + gw.data_line_ptr = NULL; + gw.data_ptr = NULL; + } +} + +/* ================================================================ + * IF/THEN/ELSE - skip_to_else_or_eol + * ================================================================ */ + +static void skip_to_else_or_eol(void) +{ + int depth = 0; + for (;;) { + uint8_t ch = *gw.text_ptr; + if (ch == 0) return; + + if (ch == TOK_IF) { + depth++; + gw.text_ptr++; + continue; + } + if (ch == TOK_ELSE) { + if (depth == 0) { + gw.text_ptr++; /* consume ELSE */ + return; + } + depth--; + gw.text_ptr++; + continue; + } + + /* Skip embedded constants */ + if (ch == TOK_INT2) { gw.text_ptr += 3; continue; } + if (ch == TOK_INT1) { gw.text_ptr += 2; continue; } + if (ch >= 0x11 && ch <= 0x1A) { gw.text_ptr++; continue; } + if (ch == TOK_CONST_SNG) { gw.text_ptr += 5; continue; } + if (ch == TOK_CONST_DBL) { gw.text_ptr += 9; continue; } + + /* Skip string literals */ + if (ch == '"') { + gw.text_ptr++; + while (*gw.text_ptr && *gw.text_ptr != '"') + gw.text_ptr++; + if (*gw.text_ptr == '"') + gw.text_ptr++; + continue; + } + + /* Skip REM to end of line */ + if (ch == TOK_REM || ch == TOK_SQUOTE) { + while (*gw.text_ptr) gw.text_ptr++; + return; + } + + gw.text_ptr++; + } +} + +/* ================================================================ + * DEF FN + * ================================================================ */ + +static void stmt_def_fn(void) +{ + /* DEF FN[()] = */ + gw_skip_spaces(); + if (gw_chrgot() != TOK_FN) + gw_error(ERR_SN); + gw_chrget(); + + gw_skip_spaces(); + if (!gw_is_letter(gw_chrgot())) + gw_error(ERR_SN); + int fn_idx = toupper(gw_chrgot()) - 'A'; + gw_chrget(); + /* Skip rest of function name */ + while (gw_is_letter(gw_chrgot()) || gw_is_digit(gw_chrgot())) + gw_chrget(); + + /* Check type suffix on function name */ + gw_valtype_t ret_type = gw.def_type[fn_idx]; + if (gw_chrgot() == '%') { ret_type = VT_INT; gw_chrget(); } + else if (gw_chrgot() == '!') { ret_type = VT_SNG; gw_chrget(); } + else if (gw_chrgot() == '#') { ret_type = VT_DBL; gw_chrget(); } + else if (gw_chrgot() == '$') { ret_type = VT_STR; gw_chrget(); } + + fn_def_t *fn = &gw.fn_defs[fn_idx]; + fn->defined = true; + fn->ret_type = ret_type; + fn->param_name[0] = '\0'; + fn->param_name[1] = '\0'; + fn->param_type = VT_SNG; + + gw_skip_spaces(); + if (gw_chrgot() == '(') { + gw_chrget(); + gw_skip_spaces(); + /* Parse parameter name */ + if (gw_is_letter(gw_chrgot())) { + fn->param_name[0] = toupper(gw_chrgot()); + gw_chrget(); + if (gw_is_letter(gw_chrgot()) || gw_is_digit(gw_chrgot())) { + fn->param_name[1] = toupper(gw_chrgot()); + gw_chrget(); + while (gw_is_letter(gw_chrgot()) || gw_is_digit(gw_chrgot())) + gw_chrget(); + } + /* Param type suffix */ + if (gw_chrgot() == '%') { fn->param_type = VT_INT; gw_chrget(); } + else if (gw_chrgot() == '!') { fn->param_type = VT_SNG; gw_chrget(); } + else if (gw_chrgot() == '#') { fn->param_type = VT_DBL; gw_chrget(); } + else if (gw_chrgot() == '$') { fn->param_type = VT_STR; gw_chrget(); } + else { fn->param_type = gw.def_type[fn->param_name[0] - 'A']; } + } + gw_expect_rparen(); + } + + gw_skip_spaces(); + gw_expect(TOK_EQ); + + /* Save body location */ + fn->body_text = gw.text_ptr; + fn->body_line = gw.cur_line; + + /* Skip past the expression (don't evaluate now) */ + /* We need to skip to end of statement */ + while (*gw.text_ptr && *gw.text_ptr != ':' && *gw.text_ptr != TOK_ELSE) + gw.text_ptr++; +} + +/* Evaluate FN call - called from eval.c */ +gw_value_t gw_eval_fn_call(void) +{ + gw_skip_spaces(); + if (!gw_is_letter(gw_chrgot())) + gw_error(ERR_SN); + int fn_idx = toupper(gw_chrgot()) - 'A'; + gw_chrget(); + while (gw_is_letter(gw_chrgot()) || gw_is_digit(gw_chrgot())) + gw_chrget(); + + /* Skip type suffix on function name */ + if (gw_chrgot() == '%' || gw_chrgot() == '!' || + gw_chrgot() == '#' || gw_chrgot() == '$') + gw_chrget(); + + fn_def_t *fn = &gw.fn_defs[fn_idx]; + if (!fn->defined) + gw_error(ERR_UF); + + /* Evaluate argument if present */ + gw_value_t arg_val = {0}; + bool has_arg = false; + gw_skip_spaces(); + if (gw_chrgot() == '(') { + gw_chrget(); + arg_val = gw_eval(); + gw_expect_rparen(); + has_arg = true; + } + + /* Save/restore text pointer around body evaluation */ + uint8_t *save_text = gw.text_ptr; + program_line_t *save_line = gw.cur_line; + + /* Set parameter if function has one */ + var_entry_t *param_var = NULL; + gw_value_t saved_val = {0}; + if (fn->param_name[0] && has_arg) { + param_var = gw_var_find_or_create(fn->param_name, fn->param_type); + saved_val = param_var->val; + if (fn->param_type == VT_STR && saved_val.type == VT_STR) + saved_val.sval = gw_str_copy(¶m_var->val.sval); + gw_var_assign(param_var, &arg_val); + } + + /* Evaluate the body expression */ + gw.text_ptr = fn->body_text; + gw.cur_line = fn->body_line; + gw_value_t result = gw_eval(); + + /* Restore parameter */ + if (param_var) { + if (fn->param_type == VT_STR) + gw_str_free(¶m_var->val.sval); + param_var->val = saved_val; + } + + /* Restore text pointer */ + gw.text_ptr = save_text; + gw.cur_line = save_line; + + return result; +} + +/* ================================================================ + * Statement Dispatcher + * ================================================================ */ + +void gw_exec_stmt(void) +{ + gw_skip_spaces(); + uint8_t tok = gw_chrgot(); + + if (tok == 0 || tok == ':') + return; + + /* Trace */ + if (gw.trace_on && gw.cur_line_num != LINE_DIRECT) { + char tbuf[16]; + snprintf(tbuf, sizeof(tbuf), "[%u]", gw.cur_line_num); + if (gw_hal) gw_hal->puts(tbuf); + else fputs(tbuf, stdout); + } + + /* PRINT / ? */ + if (tok == TOK_PRINT || tok == '?') { + gw_chrget(); + gw_stmt_print(); + return; + } + + /* LPRINT */ + if (tok == TOK_LPRINT) { + gw_chrget(); + gw_stmt_print(); + return; + } + + /* REM / single-quote */ + if (tok == TOK_REM || tok == TOK_SQUOTE) { + while (*gw.text_ptr) gw.text_ptr++; + return; + } + + /* CLS */ + if (tok == TOK_CLS) { + gw_chrget(); + if (gw_hal) gw_hal->cls(); + return; + } + + /* SYSTEM */ + if (tok == TOK_PREFIX_FE) { + uint8_t *save = gw.text_ptr; + gw_chrget(); + if (gw_chrgot() == XSTMT_SYSTEM) { + if (gw_hal) gw_hal->shutdown(); + exit(0); + } + gw.text_ptr = save; + } + + /* END */ + if (tok == TOK_END) { + gw_chrget(); + gw.running = false; + gw.cont_text = gw.text_ptr; + gw.cont_line = gw.cur_line; + return; + } + + /* STOP */ + if (tok == TOK_STOP) { + gw_chrget(); + gw.running = false; + gw.cont_text = gw.text_ptr; + gw.cont_line = gw.cur_line; + if (gw.cur_line_num != LINE_DIRECT) { + char buf[40]; + snprintf(buf, sizeof(buf), "Break in %u\n", gw.cur_line_num); + if (gw_hal) gw_hal->puts(buf); + else fputs(buf, stdout); + } + return; + } + + /* NEW */ + if (tok == TOK_NEW) { + gw_chrget(); + gw_free_program(); + gw_vars_clear(); + gw_arrays_clear(); + 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; + gw.running = false; + for (int i = 0; i < 26; i++) + gw.def_type[i] = VT_SNG; + gw.option_base = 0; + return; + } + + /* CLEAR */ + if (tok == TOK_CLEAR) { + gw_chrget(); + gw_vars_clear(); + gw_arrays_clear(); + 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.on_error_line = 0; + gw.in_error_handler = false; + /* Skip optional args (memory size, stack size) */ + while (gw_chrgot() && gw_chrgot() != ':') + gw.text_ptr++; + return; + } + + /* RUN */ + if (tok == TOK_RUN) { + gw_chrget(); + gw_skip_spaces(); + + /* 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); + } else if (gw_chrgot() >= 0x11 && gw_chrgot() <= 0x1A) { + uint16_t num = gw_eval_uint16(); + start = gw_find_line(num); + if (!start) gw_error(ERR_UL); + } + + if (!start) return; + + gw_vars_clear(); + gw_arrays_clear(); + 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; + gw.option_base = 0; + + gw.cur_line = start; + gw.text_ptr = start->tokens; + gw.cur_line_num = start->num; + gw.running = true; + gw_run_loop(); + return; + } + + /* CONT */ + if (tok == TOK_CONT) { + gw_chrget(); + if (!gw.cont_text || !gw.cont_line) + gw_error(ERR_CN); + gw.text_ptr = gw.cont_text; + gw.cur_line = gw.cont_line; + gw.cur_line_num = gw.cont_line->num; + gw.running = true; + gw.cont_text = NULL; + gw.cont_line = NULL; + gw_run_loop(); + return; + } + + /* LIST */ + if (tok == TOK_LIST) { + gw_chrget(); + stmt_list(); + return; + } + + /* GOTO */ + if (tok == TOK_GOTO) { + gw_chrget(); + uint16_t num = gw_eval_uint16(); + program_line_t *target = gw_find_line(num); + if (!target) gw_error(ERR_UL); + gw.cur_line = target; + gw.text_ptr = target->tokens; + gw.cur_line_num = target->num; + return; + } + + /* GOSUB */ + if (tok == TOK_GOSUB) { + gw_chrget(); + uint16_t num = gw_eval_uint16(); + program_line_t *target = gw_find_line(num); + if (!target) gw_error(ERR_UL); + + if (gw.gosub_sp >= 24) + gw_error(ERR_OM); + gw.gosub_stack[gw.gosub_sp].ret_text = gw.text_ptr; + gw.gosub_stack[gw.gosub_sp].ret_line = gw.cur_line; + gw.gosub_stack[gw.gosub_sp].line_num = gw.cur_line_num; + gw.gosub_sp++; + + gw.cur_line = target; + gw.text_ptr = target->tokens; + gw.cur_line_num = target->num; + return; + } + + /* RETURN */ + if (tok == TOK_RETURN) { + gw_chrget(); + if (gw.gosub_sp <= 0) + gw_error(ERR_RG); + gw.gosub_sp--; + gw.text_ptr = gw.gosub_stack[gw.gosub_sp].ret_text; + gw.cur_line = gw.gosub_stack[gw.gosub_sp].ret_line; + gw.cur_line_num = gw.gosub_stack[gw.gosub_sp].line_num; + + /* Optional line number: RETURN */ + gw_skip_spaces(); + if (gw_chrgot() >= TOK_INT2 && gw_chrgot() <= TOK_CONST_DBL) { + uint16_t num = gw_eval_uint16(); + program_line_t *target = gw_find_line(num); + if (!target) gw_error(ERR_UL); + gw.cur_line = target; + gw.text_ptr = target->tokens; + gw.cur_line_num = target->num; + } else if (gw_chrgot() >= 0x11 && gw_chrgot() <= 0x1A) { + uint16_t num = gw_eval_uint16(); + program_line_t *target = gw_find_line(num); + if (!target) gw_error(ERR_UL); + gw.cur_line = target; + gw.text_ptr = target->tokens; + gw.cur_line_num = target->num; + } + return; + } + + /* FOR */ + if (tok == TOK_FOR) { + gw_chrget(); + + /* Parse variable */ + 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); + + /* Initial value */ + gw_value_t init = gw_eval_num(); + gw_var_assign(var, &init); + + gw_skip_spaces(); + gw_expect(TOK_TO); + + /* Limit */ + gw_value_t limit = gw_eval_num(); + + /* Step (default 1) */ + gw_value_t step; + step.type = VT_INT; + step.ival = 1; + gw_skip_spaces(); + if (gw_chrgot() == TOK_STEP) { + gw_chrget(); + step = gw_eval_num(); + } + + /* Check if this variable already on stack, replace */ + for (int i = gw.for_sp - 1; i >= 0; i--) { + if (gw.for_stack[i].var == var) { + gw.for_sp = i; + break; + } + } + + if (gw.for_sp >= 16) + gw_error(ERR_OM); + + for_entry_t *f = &gw.for_stack[gw.for_sp++]; + f->var = var; + f->limit = limit; + f->step = step; + f->loop_text = gw.text_ptr; + f->loop_line = gw.cur_line; + f->line_num = gw.cur_line_num; + return; + } + + /* NEXT */ + if (tok == TOK_NEXT) { + gw_chrget(); + gw_skip_spaces(); + + /* Parse optional variable name(s) */ + for (;;) { + var_entry_t *var = NULL; + if (gw_is_letter(gw_chrgot())) { + char name[2]; + gw_valtype_t type = gw_parse_varname(name); + var = gw_var_find_or_create(name, type); + } + + /* Find matching FOR */ + int found = -1; + for (int i = gw.for_sp - 1; i >= 0; i--) { + if (!var || gw.for_stack[i].var == var) { + found = i; + break; + } + } + if (found < 0) + gw_error(ERR_NF); + + /* Discard any nested FORs above */ + gw.for_sp = found + 1; + for_entry_t *f = &gw.for_stack[found]; + + /* Increment */ + double cur, lim, stp; + switch (f->var->type) { + case VT_INT: + f->var->val.ival = gw_int_add(f->var->val.ival, + gw_to_int(&f->step)); + cur = f->var->val.ival; + break; + case VT_SNG: + f->var->val.fval += gw_to_sng(&f->step); + cur = f->var->val.fval; + break; + case VT_DBL: + f->var->val.dval += gw_to_dbl(&f->step); + cur = f->var->val.dval; + break; + default: cur = 0; break; + } + lim = gw_to_dbl(&f->limit); + stp = gw_to_dbl(&f->step); + + /* Check termination */ + bool done; + if (stp >= 0) + done = cur > lim; + else + done = cur < lim; + + if (!done) { + /* Loop back */ + gw.text_ptr = f->loop_text; + gw.cur_line = f->loop_line; + gw.cur_line_num = f->line_num; + return; + } + + /* Loop done, pop stack */ + gw.for_sp = found; + + /* Check for more variables after comma */ + gw_skip_spaces(); + if (gw_chrgot() != ',') + break; + gw_chrget(); + } + return; + } + + /* IF */ + if (tok == TOK_IF) { + gw_chrget(); + gw_value_t cond = gw_eval_num(); + double cv = gw_to_dbl(&cond); + + gw_skip_spaces(); + /* Expect THEN or GOTO */ + if (gw_chrgot() == TOK_THEN) + gw_chrget(); + else if (gw_chrgot() == TOK_GOTO) + ; /* GOTO will be handled below */ + else + gw_error(ERR_SN); + + if (cv != 0.0) { + gw_skip_spaces(); + /* Check for line number after THEN */ + uint8_t ch = gw_chrgot(); + if ((ch >= 0x11 && ch <= 0x1A) || ch == TOK_INT1 || ch == TOK_INT2) { + uint16_t num = gw_eval_uint16(); + program_line_t *target = gw_find_line(num); + if (!target) gw_error(ERR_UL); + gw.cur_line = target; + gw.text_ptr = target->tokens; + gw.cur_line_num = target->num; + return; + } + /* Execute statement(s) after THEN */ + gw_exec_stmt(); + return; + } + + /* False: skip to ELSE or end of line */ + skip_to_else_or_eol(); + if (*gw.text_ptr == 0) + return; + + /* We're at ELSE clause */ + gw_skip_spaces(); + uint8_t ch = gw_chrgot(); + if ((ch >= 0x11 && ch <= 0x1A) || ch == TOK_INT1 || ch == TOK_INT2) { + uint16_t num = gw_eval_uint16(); + program_line_t *target = gw_find_line(num); + if (!target) gw_error(ERR_UL); + gw.cur_line = target; + gw.text_ptr = target->tokens; + gw.cur_line_num = target->num; + return; + } + gw_exec_stmt(); + return; + } + + /* WHILE */ + if (tok == TOK_WHILE) { + /* Save position AT the WHILE token for WEND to jump back to */ + uint8_t *while_text = gw.text_ptr; + program_line_t *while_line = gw.cur_line; + + gw_chrget(); + + gw_value_t cond = gw_eval_num(); + double cv = gw_to_dbl(&cond); + + if (cv != 0.0) { + /* Push WHILE onto stack (pointing to condition) */ + /* Check if we're already in this WHILE */ + bool found = false; + for (int i = gw.while_sp - 1; i >= 0; i--) { + if (gw.while_stack[i].while_text == while_text) { + found = true; + break; + } + } + if (!found) { + if (gw.while_sp >= 16) + gw_error(ERR_OM); + gw.while_stack[gw.while_sp].while_text = while_text; + gw.while_stack[gw.while_sp].while_line = while_line; + gw.while_stack[gw.while_sp].line_num = gw.cur_line_num; + gw.while_sp++; + } + return; /* continue executing statements after WHILE */ + } + + /* Condition false: find matching WEND and skip */ + int depth = 1; + for (;;) { + /* Advance to next statement/line */ + while (*gw.text_ptr && *gw.text_ptr != ':') { + if (*gw.text_ptr == TOK_WHILE) depth++; + if (*gw.text_ptr == TOK_WEND) { + depth--; + if (depth == 0) { + gw.text_ptr++; + /* Remove from WHILE stack if present */ + for (int i = gw.while_sp - 1; i >= 0; i--) { + if (gw.while_stack[i].while_text == while_text) { + gw.while_sp = i; + break; + } + } + return; + } + } + /* Skip embedded constants */ + uint8_t ch2 = *gw.text_ptr; + if (ch2 == TOK_INT2) { gw.text_ptr += 3; continue; } + if (ch2 == TOK_INT1) { gw.text_ptr += 2; continue; } + if (ch2 >= 0x11 && ch2 <= 0x1A) { gw.text_ptr++; continue; } + if (ch2 == TOK_CONST_SNG) { gw.text_ptr += 5; continue; } + if (ch2 == TOK_CONST_DBL) { gw.text_ptr += 9; continue; } + if (ch2 == '"') { + gw.text_ptr++; + while (*gw.text_ptr && *gw.text_ptr != '"') + gw.text_ptr++; + if (*gw.text_ptr == '"') gw.text_ptr++; + continue; + } + gw.text_ptr++; + } + if (*gw.text_ptr == ':') { + gw.text_ptr++; + continue; + } + /* End of line, advance to next program line */ + if (!gw.cur_line || !gw.cur_line->next) + gw_error(ERR_WH); + gw.cur_line = gw.cur_line->next; + gw.text_ptr = gw.cur_line->tokens; + gw.cur_line_num = gw.cur_line->num; + } + } + + /* WEND */ + if (tok == TOK_WEND) { + gw_chrget(); + if (gw.while_sp <= 0) + gw_error(ERR_WE); + /* Jump back to WHILE token to re-evaluate condition */ + gw.while_sp--; + gw.text_ptr = gw.while_stack[gw.while_sp].while_text; + gw.cur_line = gw.while_stack[gw.while_sp].while_line; + gw.cur_line_num = gw.while_stack[gw.while_sp].line_num; + return; + } + + /* ON ... GOTO / GOSUB */ + if (tok == TOK_ON) { + gw_chrget(); + gw_skip_spaces(); + + /* ON ERROR GOTO */ + if (gw_chrgot() == TOK_ERROR) { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() != TOK_GOTO) + gw_error(ERR_SN); + gw_chrget(); + uint16_t num = gw_eval_uint16(); + gw.on_error_line = num; + if (num == 0) { + gw.on_error_line = 0; + gw.in_error_handler = false; + } + return; + } + + int idx = gw_eval_int(); + gw_skip_spaces(); + + bool is_gosub = false; + if (gw_chrgot() == TOK_GOTO) { + gw_chrget(); + } else if (gw_chrgot() == TOK_GOSUB) { + gw_chrget(); + is_gosub = true; + } else { + gw_error(ERR_SN); + } + + /* Parse line number list */ + int count = 0; + uint16_t target_num = 0; + for (;;) { + gw_skip_spaces(); + uint16_t num = gw_eval_uint16(); + count++; + if (count == idx) + target_num = num; + gw_skip_spaces(); + if (gw_chrgot() != ',') + break; + gw_chrget(); + } + + if (idx < 1 || idx > count) + return; /* out of range = fall through */ + + program_line_t *target = gw_find_line(target_num); + if (!target) gw_error(ERR_UL); + + if (is_gosub) { + if (gw.gosub_sp >= 24) + gw_error(ERR_OM); + gw.gosub_stack[gw.gosub_sp].ret_text = gw.text_ptr; + gw.gosub_stack[gw.gosub_sp].ret_line = gw.cur_line; + gw.gosub_stack[gw.gosub_sp].line_num = gw.cur_line_num; + gw.gosub_sp++; + } + + gw.cur_line = target; + gw.text_ptr = target->tokens; + gw.cur_line_num = target->num; + return; + } + + /* DIM */ + if (tok == TOK_DIM) { + gw_chrget(); + gw_stmt_dim(); + return; + } + + /* ERASE */ + if (tok == TOK_ERASE) { + gw_chrget(); + gw_stmt_erase(); + return; + } + + /* OPTION */ + if (tok == TOK_OPTION) { + gw_chrget(); + gw_stmt_option(); + return; + } + + /* INPUT */ + if (tok == TOK_INPUT) { + gw_chrget(); + gw_stmt_input(); + return; + } + + /* LINE INPUT */ + if (tok == TOK_LINE) { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() == TOK_INPUT) { + gw_chrget(); + gw_stmt_line_input(); + return; + } + gw_error(ERR_SN); + } + + /* DATA - skip during execution */ + if (tok == TOK_DATA) { + gw_chrget(); + skip_data(); + return; + } + + /* READ */ + if (tok == TOK_READ) { + gw_chrget(); + stmt_read(); + return; + } + + /* RESTORE */ + if (tok == TOK_RESTORE) { + gw_chrget(); + stmt_restore(); + return; + } + + /* SWAP */ + if (tok == TOK_SWAP) { + gw_chrget(); + gw_stmt_swap(); + return; + } + + /* TRON */ + if (tok == TOK_TRON) { + gw_chrget(); + gw.trace_on = true; + return; + } + + /* TROFF */ + if (tok == TOK_TROFF) { + gw_chrget(); + gw.trace_on = false; + return; + } + + /* RANDOMIZE */ + if (tok == TOK_RANDOMIZE) { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() != 0 && gw_chrgot() != ':') { + gw_value_t v = gw_eval_num(); + /* Seed the RNG - use the value as seed */ + extern uint32_t gw_rnd_seed; + gw_rnd_seed = (uint32_t)gw_to_dbl(&v); + } + /* Without argument: prompt in real GW-BASIC, we just seed from time */ + return; + } + + /* DEF */ + if (tok == TOK_DEF) { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() == TOK_FN) { + stmt_def_fn(); + return; + } + gw_error(ERR_SN); + } + + /* DEFINT / DEFSNG / DEFDBL / DEFSTR */ + if (tok == TOK_DEFINT) { gw_chrget(); gw_stmt_deftype(VT_INT); return; } + if (tok == TOK_DEFSNG) { gw_chrget(); gw_stmt_deftype(VT_SNG); return; } + if (tok == TOK_DEFDBL) { gw_chrget(); gw_stmt_deftype(VT_DBL); return; } + if (tok == TOK_DEFSTR) { gw_chrget(); gw_stmt_deftype(VT_STR); return; } + + /* ERROR */ + if (tok == TOK_ERROR) { + gw_chrget(); + int errnum = gw_eval_int(); + gw_error(errnum); + return; + } + + /* RESUME */ + if (tok == TOK_RESUME) { + gw_chrget(); + if (!gw.in_error_handler) + gw_error(ERR_RW); + gw.in_error_handler = false; + + gw_skip_spaces(); + if (gw_chrgot() == TOK_NEXT) { + /* RESUME NEXT: continue at statement after the error */ + gw_chrget(); + if (gw.err_resume_text && gw.err_resume_line) { + gw.text_ptr = gw.err_resume_text; + gw.cur_line = gw.err_resume_line; + gw.cur_line_num = gw.err_resume_line->num; + /* Advance past current statement */ + while (*gw.text_ptr && *gw.text_ptr != ':') + gw.text_ptr++; + } + return; + } + + uint8_t ch2 = gw_chrgot(); + if (ch2 == 0 || ch2 == ':') { + /* RESUME (no args): retry the statement that caused the error */ + if (gw.err_resume_text && gw.err_resume_line) { + gw.text_ptr = gw.err_resume_text; + gw.cur_line = gw.err_resume_line; + gw.cur_line_num = gw.err_resume_line->num; + } + return; + } + + /* RESUME */ + uint16_t num = gw_eval_uint16(); + program_line_t *target = gw_find_line(num); + if (!target) gw_error(ERR_UL); + gw.cur_line = target; + gw.text_ptr = target->tokens; + gw.cur_line_num = target->num; + return; + } + + /* POKE - ignore for now */ + if (tok == TOK_POKE) { + gw_chrget(); + gw_eval_int(); /* address */ + gw_skip_spaces(); + gw_expect(','); + gw_eval_int(); /* value */ + return; + } + + /* WIDTH */ + if (tok == TOK_WIDTH) { + gw_chrget(); + int w = gw_eval_int(); + if (gw_hal) gw_hal->set_width(w); + return; + } + + /* LOCATE */ + if (tok == TOK_LOCATE) { + gw_chrget(); + int row = gw_eval_int(); + gw_skip_spaces(); + int col = 1; + if (gw_chrgot() == ',') { + gw_chrget(); + col = gw_eval_int(); + } + if (gw_hal) gw_hal->locate(row - 1, col - 1); + /* Skip additional optional params */ + while (gw_chrgot() == ',') { + gw_chrget(); + if (gw_chrgot() != ',' && gw_chrgot() != 0 && gw_chrgot() != ':') + gw_eval(); + } + return; + } + + /* COLOR - parse and ignore */ + if (tok == TOK_COLOR) { + gw_chrget(); + gw_eval_int(); + while (gw_chrgot() == ',') { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() != ',' && gw_chrgot() != 0 && gw_chrgot() != ':') + gw_eval_int(); + } + return; + } + + /* BEEP */ + if (tok == TOK_BEEP) { + gw_chrget(); + if (gw_hal) gw_hal->putch('\a'); + return; + } + + /* SOUND - parse and ignore */ + if (tok == TOK_SOUND) { + gw_chrget(); + gw_eval_num(); + gw_skip_spaces(); + gw_expect(','); + gw_eval_num(); + return; + } + + /* KEY - parse and ignore */ + if (tok == TOK_KEY) { + gw_chrget(); + gw_skip_spaces(); + if (gw_chrgot() == TOK_ON || gw_chrgot() == TOK_OFF) { + gw_chrget(); + return; + } + while (gw_chrgot() && gw_chrgot() != ':') + gw.text_ptr++; + return; + } + + /* WRITE */ + if (tok == TOK_WRITE) { + gw_chrget(); + int first = 1; + for (;;) { + gw_skip_spaces(); + uint8_t ch2 = gw_chrgot(); + if (ch2 == 0 || ch2 == ':') break; + if (!first) { + if (gw_hal) gw_hal->putch(','); + else putchar(','); + } + gw_value_t v = gw_eval(); + if (v.type == VT_STR) { + if (gw_hal) gw_hal->putch('"'); + else putchar('"'); + gw_print_value(&v); + if (gw_hal) gw_hal->putch('"'); + else putchar('"'); + } else { + gw_print_value(&v); + } + first = 0; + gw_skip_spaces(); + if (gw_chrgot() == ',') { gw_chrget(); continue; } + if (gw_chrgot() == ';') { gw_chrget(); continue; } + break; + } + gw_print_newline(); + return; + } + + /* LET (explicit) */ + if (tok == TOK_LET) { + gw_chrget(); + tok = gw_chrgot(); + } + + /* Implicit LET: variable assignment */ + if (gw_is_letter(tok)) { + char name[2]; + gw_valtype_t type = gw_parse_varname(name); + + gw_skip_spaces(); + + /* Check for array element or MID$ assignment */ + if (gw_chrgot() == '(') { + gw_value_t *elem = gw_array_element(name, type); + gw_skip_spaces(); + gw_expect(TOK_EQ); + gw_value_t val = gw_eval(); + + if (type == VT_STR) { + if (val.type != VT_STR) gw_error(ERR_TM); + gw_str_free(&elem->sval); + elem->sval = val.sval; + elem->type = VT_STR; + } else { + if (val.type == VT_STR) gw_error(ERR_TM); + switch (type) { + case VT_INT: elem->ival = gw_to_int(&val); break; + case VT_SNG: elem->fval = gw_to_sng(&val); break; + case VT_DBL: elem->dval = gw_to_dbl(&val); break; + default: break; + } + elem->type = type; + } + return; + } + + /* Scalar assignment */ + var_entry_t *var = gw_var_find_or_create(name, type); + gw_skip_spaces(); + gw_expect(TOK_EQ); + gw_value_t val = gw_eval(); + gw_var_assign(var, &val); + return; + } + + gw_error(ERR_SN); +} + +/* ================================================================ + * NEWSTT Loop (Run Loop) + * ================================================================ */ + +void gw_run_loop(void) +{ + if (setjmp(gw_run_jmp) != 0) { + /* Error handler redirected here via ON ERROR GOTO */ + if (!gw.running) return; + } + + while (gw.running) { + gw_skip_spaces(); + uint8_t ch = gw_chrgot(); + + if (ch == 0) { + /* End of current line, advance to next */ + if (!gw.cur_line || !gw.cur_line->next) { + gw.running = false; + break; + } + gw.cur_line = gw.cur_line->next; + gw.text_ptr = gw.cur_line->tokens; + gw.cur_line_num = gw.cur_line->num; + continue; + } + + if (ch == ':') { + gw.text_ptr++; + continue; + } + + /* Skip ELSE during normal flow (only reached via IF true path continuing) */ + if (ch == TOK_ELSE) { + /* Skip to end of line */ + while (*gw.text_ptr) gw.text_ptr++; + continue; + } + + gw_exec_stmt(); + + if (!gw.running) break; + } +} diff --git a/src/main.c b/src/main.c index cc9cde7..5f94c10 100644 --- a/src/main.c +++ b/src/main.c @@ -3,6 +3,7 @@ #include #include #include +#include /* Global interpreter state */ interp_state_t gw; @@ -10,10 +11,12 @@ hal_ops_t *gw_hal = NULL; void gw_init(void) { + gw_free_program(); + gw_vars_clear(); + gw_arrays_clear(); memset(&gw, 0, sizeof(gw)); gw.cur_line_num = LINE_DIRECT; - /* Default types: all single precision (like GW-BASIC) */ for (int i = 0; i < 26; i++) gw.def_type[i] = VT_SNG; } @@ -57,90 +60,87 @@ void gw_expect(uint8_t tok) gw_chrget(); } -/* Execute a tokenized line from the text pointer */ -void gw_exec_line(uint8_t *tokens) +/* Check if tokenized data starts with a line number */ +static int parse_line_number(uint8_t *tokens, uint16_t *num) { - gw.text_ptr = tokens; - gw_skip_spaces(); + uint8_t *p = tokens; + while (*p == ' ') p++; - uint8_t tok = gw_chrgot(); + uint8_t tok = *p; - /* Empty line */ - if (tok == 0) - return; - - /* PRINT / ? */ - if (tok == TOK_PRINT || tok == '?') { - gw_chrget(); - gw_stmt_print(); - return; + /* Literal 0-9 */ + if (tok >= 0x11 && tok <= 0x1A) { + *num = tok - 0x11; + return (p - tokens) + 1; + } + /* One-byte int */ + if (tok == TOK_INT1) { + *num = p[1]; + return (p - tokens) + 2; + } + /* Two-byte int */ + if (tok == TOK_INT2) { + *num = (uint16_t)(p[1] | (p[2] << 8)); + return (p - tokens) + 3; } - /* LPRINT */ - if (tok == TOK_LPRINT) { - gw_chrget(); - gw_stmt_print(); - return; - } - - /* LET (explicit or implicit) */ - if (tok == TOK_LET) { - gw_chrget(); - /* Fall through to implicit LET */ - } - - /* REM / single-quote - skip rest */ - if (tok == TOK_REM || tok == TOK_SQUOTE) - return; - - /* CLS */ - if (tok == TOK_CLS) { - gw_chrget(); - if (gw_hal) gw_hal->cls(); - return; - } - - /* NEW */ - if (tok == TOK_NEW) { - gw_init(); - return; - } - - /* SYSTEM - exit */ - if (tok == TOK_PREFIX_FE) { - uint8_t *save = gw.text_ptr; - gw_chrget(); - if (gw_chrgot() == XSTMT_SYSTEM) { - if (gw_hal) gw_hal->shutdown(); - exit(0); - } - gw.text_ptr = save; - } - - /* END / STOP */ - if (tok == TOK_END || tok == TOK_STOP) - return; - - /* For Phase 1, anything else is a syntax error */ - gw_error(ERR_SN); + return 0; } -/* Execute a raw text line (crunch + dispatch) */ +/* Execute a direct mode line: detect line numbers -> store; otherwise execute */ void gw_exec_direct(const char *line) { gw.cur_line_num = LINE_DIRECT; + gw.cur_line = NULL; int len = gw_crunch(line, gw.kbuf, sizeof(gw.kbuf)); - (void)len; - gw_exec_line(gw.kbuf); + + /* Check for line number -> program storage */ + uint16_t num; + int skip = parse_line_number(gw.kbuf, &num); + if (skip > 0) { + int data_len = len - skip; + /* Skip spaces after line number */ + uint8_t *data = gw.kbuf + skip; + while (*data == ' ' && data_len > 0) { data++; data_len--; } + + /* Invalidate CONT when editing program */ + gw.cont_text = NULL; + gw.cont_line = NULL; + + if (data_len <= 0 || *data == 0) { + gw_delete_line(num); + } else { + gw_store_line(num, data, data_len); + } + return; + } + + /* Direct mode execution with multi-statement support */ + gw.text_ptr = gw.kbuf; + for (;;) { + gw_skip_spaces(); + if (*gw.text_ptr == 0) + break; + if (*gw.text_ptr == ':') { + gw.text_ptr++; + continue; + } + /* Skip ELSE during normal flow (IF true path continuing) */ + if (*gw.text_ptr == TOK_ELSE) { + while (*gw.text_ptr) gw.text_ptr++; + break; + } + gw_exec_stmt(); + if (gw.running) + break; /* RUN/CONT started, loop handles it */ + } } -/* Read a line from stdin (returns NULL on EOF) */ static char *read_line(void) { static char buf[256]; if (fgets(buf, sizeof(buf), stdin) == NULL) return NULL; - /* Strip trailing newline */ int len = strlen(buf); while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) buf[--len] = '\0'; @@ -169,7 +169,6 @@ int main(int argc, char **argv) int interactive = isatty(fileno(stdin)); - /* Check for file argument */ const char *filename = NULL; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { @@ -188,12 +187,43 @@ int main(int argc, char **argv) } } - /* TODO: Load and run file if specified */ - (void)filename; + /* Load file: read lines, store numbered lines, then RUN */ + if (filename) { + FILE *f = fopen(filename, "r"); + if (!f) { + fprintf(stderr, "File not found: %s\n", filename); + return 1; + } + char buf[256]; + while (fgets(buf, sizeof(buf), f)) { + int len = strlen(buf); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) + buf[--len] = '\0'; + if (buf[0] == '\0') continue; + + if (setjmp(gw_error_jmp) != 0) + continue; + gw_exec_direct(buf); + } + fclose(f); + + /* If program was loaded but RUN wasn't in the file, auto-run */ + if (gw.prog_head && !gw.running) { + if (setjmp(gw_error_jmp) != 0) { + /* error during auto-run */ + } else { + gw_exec_direct("RUN"); + } + } + + if (!interactive) { + if (gw_hal) gw_hal->shutdown(); + return 0; + } + } print_banner(interactive); - /* Direct mode loop */ for (;;) { if (interactive) { fputs("Ok\n", stdout); @@ -204,15 +234,11 @@ int main(int argc, char **argv) if (!line) break; - /* Skip empty lines */ if (line[0] == '\0') continue; - /* Error recovery point */ - if (setjmp(gw_error_jmp) != 0) { - /* Returned from error - continue loop */ + if (setjmp(gw_error_jmp) != 0) continue; - } gw_exec_direct(line); } diff --git a/src/math_transcend.c b/src/math_transcend.c index 3188e56..fb33e3a 100644 --- a/src/math_transcend.c +++ b/src/math_transcend.c @@ -3,7 +3,7 @@ #include /* Random number state (compatible with GW-BASIC's LCG) */ -static uint32_t rnd_seed = 0x50000; /* default seed */ +uint32_t gw_rnd_seed = 0x50000; /* default seed */ static int rnd_seeded = 0; double gw_sin(double x) { return sin(x); } @@ -72,16 +72,16 @@ double gw_rnd(double x) /* Seed from argument */ union { float f; uint32_t u; } conv; conv.f = (float)x; - rnd_seed = conv.u; + gw_rnd_seed = conv.u; rnd_seeded = 1; } if (x != 0.0 || !rnd_seeded) { /* Linear congruential generator matching GW-BASIC */ - rnd_seed = rnd_seed * 0x43FD43FD + 0xC39EC3; - rnd_seed &= 0xFFFFFF; + gw_rnd_seed = gw_rnd_seed * 0x43FD43FD + 0xC39EC3; + gw_rnd_seed &= 0xFFFFFF; rnd_seeded = 1; } - return (double)rnd_seed / 16777216.0; + return (double)gw_rnd_seed / 16777216.0; } diff --git a/src/vars.c b/src/vars.c new file mode 100644 index 0000000..62acdd3 --- /dev/null +++ b/src/vars.c @@ -0,0 +1,171 @@ +#include "gwbasic.h" +#include +#include +#include + +/* + * Variable storage - reimplements PTRGET from GWMAIN.ASM. + * Variable names: first 2 chars significant, case-insensitive. + * Type determined by suffix (%, !, #, $) or def_type table. + * Variables with different type suffixes are distinct (A% != A!). + */ + +gw_valtype_t gw_parse_varname(char name_out[2]) +{ + gw_skip_spaces(); + uint8_t ch = gw_chrgot(); + if (!gw_is_letter(ch)) + gw_error(ERR_SN); + + name_out[0] = toupper(ch); + name_out[1] = '\0'; + gw_chrget(); + + ch = gw_chrgot(); + if (gw_is_letter(ch) || gw_is_digit(ch)) { + name_out[1] = toupper(ch); + gw_chrget(); + /* Skip remaining alphanumeric chars (not significant) */ + while (gw_is_letter(gw_chrgot()) || gw_is_digit(gw_chrgot())) + gw_chrget(); + } + + /* Check for type suffix */ + ch = gw_chrgot(); + if (ch == '%') { gw_chrget(); return VT_INT; } + if (ch == '!') { gw_chrget(); return VT_SNG; } + if (ch == '#') { gw_chrget(); return VT_DBL; } + if (ch == '$') { gw_chrget(); return VT_STR; } + + /* Use DEFTBL */ + int idx = name_out[0] - 'A'; + return gw.def_type[idx]; +} + +var_entry_t *gw_var_find_or_create(const char name[2], gw_valtype_t type) +{ + for (int i = 0; i < gw.var_count; i++) { + if (gw.vars[i].name[0] == name[0] && gw.vars[i].name[1] == name[1] + && gw.vars[i].type == type) + return &gw.vars[i]; + } + + if (gw.var_count >= 256) + gw_error(ERR_OM); + + var_entry_t *v = &gw.vars[gw.var_count++]; + v->name[0] = name[0]; + v->name[1] = name[1]; + v->type = type; + memset(&v->val, 0, sizeof(v->val)); + v->val.type = type; + if (type == VT_STR) { + v->val.sval.len = 0; + v->val.sval.data = NULL; + } + return v; +} + +void gw_var_assign(var_entry_t *var, gw_value_t *val) +{ + if (var->type == VT_STR) { + gw_str_free(&var->val.sval); + if (val->type != VT_STR) + gw_error(ERR_TM); + var->val.sval = val->sval; /* take ownership */ + } else { + if (val->type == VT_STR) + gw_error(ERR_TM); + switch (var->type) { + case VT_INT: var->val.ival = gw_to_int(val); break; + case VT_SNG: var->val.fval = gw_to_sng(val); break; + case VT_DBL: var->val.dval = gw_to_dbl(val); break; + default: break; + } + var->val.type = var->type; + } +} + +void gw_vars_clear(void) +{ + for (int i = 0; i < gw.var_count; i++) { + if (gw.vars[i].type == VT_STR) + gw_str_free(&gw.vars[i].val.sval); + } + gw.var_count = 0; +} + +void gw_stmt_deftype(gw_valtype_t type) +{ + for (;;) { + gw_skip_spaces(); + uint8_t ch = gw_chrgot(); + if (!gw_is_letter(ch)) + gw_error(ERR_SN); + int first = toupper(ch) - 'A'; + gw_chrget(); + + int last = first; + gw_skip_spaces(); + if (gw_chrgot() == TOK_MINUS) { + gw_chrget(); + gw_skip_spaces(); + ch = gw_chrgot(); + if (!gw_is_letter(ch)) + gw_error(ERR_SN); + last = toupper(ch) - 'A'; + gw_chrget(); + } + + for (int i = first; i <= last; i++) + gw.def_type[i] = type; + + gw_skip_spaces(); + if (gw_chrgot() != ',') + break; + gw_chrget(); + } +} + +void gw_stmt_swap(void) +{ + /* Parse first variable */ + char name1[2]; + gw_valtype_t type1 = gw_parse_varname(name1); + + /* Check if it's an array element */ + gw_skip_spaces(); + gw_value_t *arr1 = NULL; + var_entry_t *var1 = NULL; + if (gw_chrgot() == '(') { + arr1 = gw_array_element(name1, type1); + } else { + var1 = gw_var_find_or_create(name1, type1); + } + + gw_skip_spaces(); + gw_expect(','); + + /* Parse second variable */ + char name2[2]; + gw_valtype_t type2 = gw_parse_varname(name2); + + gw_skip_spaces(); + gw_value_t *arr2 = NULL; + var_entry_t *var2 = NULL; + if (gw_chrgot() == '(') { + arr2 = gw_array_element(name2, type2); + } else { + var2 = gw_var_find_or_create(name2, type2); + } + + if (type1 != type2) + gw_error(ERR_TM); + + gw_value_t *p1 = arr1 ? arr1 : &var1->val; + gw_value_t *p2 = arr2 ? arr2 : &var2->val; + + gw_value_t tmp = *p1; + *p1 = *p2; + *p2 = tmp; +} diff --git a/tests/programs/arrays.bas b/tests/programs/arrays.bas new file mode 100644 index 0000000..29ac0d8 --- /dev/null +++ b/tests/programs/arrays.bas @@ -0,0 +1,15 @@ +10 REM Array test +20 DIM A(10), B$(5) +30 FOR I = 0 TO 10 +40 A(I) = I * I +50 NEXT I +60 FOR I = 0 TO 10 +70 PRINT A(I); +80 NEXT I +90 PRINT +100 B$(0) = "Zero" +110 B$(1) = "One" +120 B$(2) = "Two" +130 FOR I = 0 TO 2 +140 PRINT B$(I) +150 NEXT I diff --git a/tests/programs/control_flow.bas b/tests/programs/control_flow.bas new file mode 100644 index 0000000..edd3b19 --- /dev/null +++ b/tests/programs/control_flow.bas @@ -0,0 +1,12 @@ +10 REM Control flow: GOTO, IF, FOR, GOSUB combined +20 GOTO 40 +30 PRINT "SKIP THIS" +40 PRINT "Start" +50 FOR I = 1 TO 3 +60 IF I = 2 THEN GOSUB 200 +70 PRINT "I ="; I +80 NEXT I +90 PRINT "End" +100 END +200 PRINT " (subroutine called)"; +210 RETURN diff --git a/tests/programs/data_read.bas b/tests/programs/data_read.bas new file mode 100644 index 0000000..ceeeafc --- /dev/null +++ b/tests/programs/data_read.bas @@ -0,0 +1,9 @@ +10 REM DATA/READ test +20 DATA 10, 20, 30, "hello", "world" +30 READ A, B, C +40 PRINT A; B; C +50 READ D$, E$ +60 PRINT D$; " "; E$ +70 RESTORE +80 READ X +90 PRINT "After RESTORE:"; X diff --git a/tests/programs/def_fn.bas b/tests/programs/def_fn.bas new file mode 100644 index 0000000..679df61 --- /dev/null +++ b/tests/programs/def_fn.bas @@ -0,0 +1,6 @@ +10 REM DEF FN test +20 DEF FN SQ(X) = X * X +30 DEF FN CU(X) = X * X * X +40 PRINT "5 squared ="; FN SQ(5) +50 PRINT "3 cubed ="; FN CU(3) +60 PRINT "Sum ="; FN SQ(3) + FN CU(2) diff --git a/tests/programs/for_next.bas b/tests/programs/for_next.bas new file mode 100644 index 0000000..ee6d5bd --- /dev/null +++ b/tests/programs/for_next.bas @@ -0,0 +1,10 @@ +10 REM FOR/NEXT test +20 FOR I = 1 TO 5 +30 PRINT I; +40 NEXT I +50 PRINT +60 REM FOR with STEP +70 FOR I = 10 TO 0 STEP -2 +80 PRINT I; +90 NEXT I +100 PRINT diff --git a/tests/programs/gosub.bas b/tests/programs/gosub.bas new file mode 100644 index 0000000..7870c70 --- /dev/null +++ b/tests/programs/gosub.bas @@ -0,0 +1,15 @@ +10 REM GOSUB/RETURN test +20 PRINT "Main program" +30 GOSUB 100 +40 PRINT "Back from first sub" +50 GOSUB 200 +60 PRINT "Back from second sub" +70 END +100 REM First subroutine +110 PRINT "In first subroutine" +120 GOSUB 200 +130 PRINT "Back in first sub" +140 RETURN +200 REM Second subroutine +210 PRINT "In second subroutine" +220 RETURN diff --git a/tests/programs/if_then_else.bas b/tests/programs/if_then_else.bas new file mode 100644 index 0000000..6650823 --- /dev/null +++ b/tests/programs/if_then_else.bas @@ -0,0 +1,8 @@ +10 REM IF/THEN/ELSE test +20 X = 5 +30 IF X > 3 THEN PRINT "X > 3" ELSE PRINT "X <= 3" +40 IF X < 3 THEN PRINT "X < 3" ELSE PRINT "X >= 3" +50 REM IF with GOTO +60 IF X = 5 THEN GOTO 80 +70 PRINT "Should not print" +80 PRINT "Jumped to 80" diff --git a/tests/programs/leibniz.bas b/tests/programs/leibniz.bas new file mode 100644 index 0000000..6bf0fd4 --- /dev/null +++ b/tests/programs/leibniz.bas @@ -0,0 +1,6 @@ +10 REM Leibniz formula for pi +20 S = 0 +30 FOR I = 0 TO 999 +40 S = S + (-1)^I / (2*I+1) +50 NEXT I +60 PRINT "Pi approx:"; 4*S diff --git a/tests/programs/nested_for.bas b/tests/programs/nested_for.bas new file mode 100644 index 0000000..00c3020 --- /dev/null +++ b/tests/programs/nested_for.bas @@ -0,0 +1,7 @@ +10 REM Nested FOR loops - multiplication table +20 FOR I = 1 TO 5 +30 FOR J = 1 TO 5 +40 PRINT I*J; +50 NEXT J +60 PRINT +70 NEXT I diff --git a/tests/programs/on_goto.bas b/tests/programs/on_goto.bas new file mode 100644 index 0000000..5778e44 --- /dev/null +++ b/tests/programs/on_goto.bas @@ -0,0 +1,11 @@ +10 REM ON GOTO/GOSUB test +20 FOR I = 1 TO 3 +30 ON I GOTO 100, 200, 300 +40 NEXT I +50 END +100 PRINT "Branch 1" +110 GOTO 40 +200 PRINT "Branch 2" +210 GOTO 40 +300 PRINT "Branch 3" +310 GOTO 40 diff --git a/tests/programs/prime_sieve.bas b/tests/programs/prime_sieve.bas new file mode 100644 index 0000000..58dc802 --- /dev/null +++ b/tests/programs/prime_sieve.bas @@ -0,0 +1,18 @@ +10 REM Sieve of Eratosthenes +20 N = 100 +30 DIM P(100) +40 FOR I = 2 TO N +50 P(I) = 1 +60 NEXT I +70 FOR I = 2 TO SQR(N) +80 IF P(I) = 0 THEN GOTO 120 +90 FOR J = I*2 TO N STEP I +100 P(J) = 0 +110 NEXT J +120 NEXT I +130 C = 0 +140 FOR I = 2 TO N +150 IF P(I) = 1 THEN PRINT I; : C = C + 1 +160 NEXT I +170 PRINT +180 PRINT C; "primes found" diff --git a/tests/programs/variables.bas b/tests/programs/variables.bas new file mode 100644 index 0000000..86cb8e5 --- /dev/null +++ b/tests/programs/variables.bas @@ -0,0 +1,11 @@ +10 REM Variable types test +20 A% = 42 +30 B! = 3.14 +40 C# = 2.718281828 +50 D$ = "Hello" +60 PRINT A%; B!; C# +70 PRINT D$ +80 REM DEFINT test +90 DEFINT X-Z +100 X = 7.9 +110 PRINT X diff --git a/tests/programs/while_wend.bas b/tests/programs/while_wend.bas new file mode 100644 index 0000000..bbf5036 --- /dev/null +++ b/tests/programs/while_wend.bas @@ -0,0 +1,8 @@ +10 REM WHILE/WEND test +20 X = 1 +30 WHILE X <= 10 +40 IF X MOD 2 = 0 THEN PRINT X; +50 X = X + 1 +60 WEND +70 PRINT +80 PRINT "Done"