From d3b57d9f3b6e1a468e98cab4a4aece145468e975 Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Sun, 29 Mar 2026 06:59:42 -0400 Subject: [PATCH] Implement ahead-of-time compiler (Phase 1): BASIC to C via token stream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New tool gwbasic-compile translates tokenized .bas programs to C source, which gcc compiles into native executables linked against libgwrt.a (the interpreter's runtime modules minus the execution loop). Pipeline: .bas → gw_crunch() → analysis pass (line table, variable census, GOTO targets, DATA collection) → C codegen → gcc → native executable. Phase 1 supports: PRINT, LET, IF/THEN/ELSE, GOTO, GOSUB/RETURN, FOR/NEXT, END/STOP/SYSTEM, REM, DATA/READ/RESTORE, CLS, arithmetic/relational/logical operators, core math functions (SIN, COS, SQR, ABS, etc.), string functions (LEFT$, RIGHT$, MID$, CHR$, ASC, VAL, STR$, LEN, etc.), string concatenation. All control flow uses goto/labels (no C for/while) so GOTO into loops works. GOSUB uses a return-label stack with switch dispatch. --- CMakeLists.txt | 46 +++ include/analysis.h | 57 +++ include/codegen.h | 10 + include/gwrt.h | 54 +++ src/analysis.c | 286 +++++++++++++ src/codegen.c | 961 ++++++++++++++++++++++++++++++++++++++++++++ src/compiler_main.c | 233 +++++++++++ src/gwrt.c | 222 ++++++++++ src/print.c | 4 +- 9 files changed, 1871 insertions(+), 2 deletions(-) create mode 100644 include/analysis.h create mode 100644 include/codegen.h create mode 100644 include/gwrt.h create mode 100644 src/analysis.c create mode 100644 src/codegen.c create mode 100644 src/compiler_main.c create mode 100644 src/gwrt.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a5dc9f..2ec1573 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,3 +53,49 @@ if(PULSEAUDIO_FOUND) target_include_directories(gwbasic PRIVATE ${PULSEAUDIO_INCLUDE_DIRS}) target_link_libraries(gwbasic ${PULSEAUDIO_LIBRARIES} pthread) endif() + +# Runtime library for compiled BASIC programs +set(GWRT_SOURCES + src/tokens.c + 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 + src/strings.c + src/print.c + src/fileio.c + src/program_io.c + src/print_using.c + src/graphics.c + src/virmem.c + src/portio.c + src/strpool.c + src/sound.c + src/tui.c + src/gwrt.c + platform/hal_posix.c +) + +add_library(gwrt STATIC ${GWRT_SOURCES}) +target_link_libraries(gwrt m) +if(PULSEAUDIO_FOUND) + target_compile_definitions(gwrt PRIVATE HAVE_PULSEAUDIO) + target_include_directories(gwrt PRIVATE ${PULSEAUDIO_INCLUDE_DIRS}) + target_link_libraries(gwrt ${PULSEAUDIO_LIBRARIES} pthread) +endif() + +# GW-BASIC compiler +add_executable(gwbasic-compile + src/compiler_main.c + src/analysis.c + src/codegen.c + src/tokens.c + src/tokenizer.c +) +target_link_libraries(gwbasic-compile m) diff --git a/include/analysis.h b/include/analysis.h new file mode 100644 index 0000000..00b55dd --- /dev/null +++ b/include/analysis.h @@ -0,0 +1,57 @@ +#ifndef ANALYSIS_H +#define ANALYSIS_H + +#include "types.h" +#include +#include + +#define MAX_LINES 4096 +#define MAX_VARS 256 +#define MAX_GOTOS 256 +#define MAX_DATA 1024 +#define MAX_GOSUB_RET 256 + +typedef struct { + uint16_t line_num; + bool is_target; /* referenced by GOTO/GOSUB/etc. */ + bool has_data; /* contains DATA statement */ + int data_start; /* index into data pool */ +} line_info_t; + +typedef struct { + char name[2]; + gw_valtype_t type; +} var_info_t; + +typedef struct { + line_info_t lines[MAX_LINES]; + int line_count; + + var_info_t vars[MAX_VARS]; + int var_count; + + uint16_t goto_targets[MAX_GOTOS]; + int goto_count; + + char *data_pool[MAX_DATA]; /* collected DATA literals */ + int data_count; + + int data_line_map[MAX_LINES][2]; /* [line_num, data_start_index] */ + int data_line_count; + + gw_valtype_t def_type[26]; /* from DEFINT/DEFSNG/DEFDBL/DEFSTR */ +} analysis_t; + +/* Run analysis pass over the loaded program */ +void analysis_run(analysis_t *a); + +/* Find a variable in the census, return index or -1 */ +int analysis_find_var(analysis_t *a, const char name[2], gw_valtype_t type); + +/* Add a variable to the census if not already present */ +int analysis_add_var(analysis_t *a, const char name[2], gw_valtype_t type); + +/* Check if a line number is a jump target */ +bool analysis_is_target(analysis_t *a, uint16_t line_num); + +#endif diff --git a/include/codegen.h b/include/codegen.h new file mode 100644 index 0000000..f5d24f2 --- /dev/null +++ b/include/codegen.h @@ -0,0 +1,10 @@ +#ifndef CODEGEN_H +#define CODEGEN_H + +#include "analysis.h" +#include + +/* Generate C source from the analyzed program */ +void codegen_emit(FILE *out, analysis_t *a); + +#endif diff --git a/include/gwrt.h b/include/gwrt.h new file mode 100644 index 0000000..32e92a0 --- /dev/null +++ b/include/gwrt.h @@ -0,0 +1,54 @@ +#ifndef GWRT_H +#define GWRT_H + +/* + * Runtime library for compiled GW-BASIC programs. + * + * Provides initialization, DATA/READ support, GOSUB return-label stack, + * and convenience wrappers. Compiled programs link against libgwrt.a + * which contains all the existing interpreter modules except the + * execution loop. + */ + +#include "gwbasic.h" +#include "strpool.h" +#include "portio.h" +#include "sound.h" +#include + +/* Initialization / shutdown */ +void gwrt_init(void); +void gwrt_shutdown(void); + +/* DATA / READ / RESTORE */ +void gwrt_data_set(const char **pool, const int *line_map, int line_count); +void gwrt_data_restore(int index); +const char *gwrt_data_read(void); /* returns next datum or errors ERR_OD */ + +/* GOSUB return-label stack */ +#define GWRT_GOSUB_MAX 24 +void gwrt_gosub_push(int label); +int gwrt_gosub_pop(void); + +/* FOR/NEXT stack (for NEXT without variable matching) */ +#define GWRT_FOR_MAX 16 + +/* Event checking (ON TIMER, ON KEY, Ctrl+Break) + string pool GC */ +void gwrt_check_line(uint16_t line_num); + +/* Error handling */ +extern jmp_buf gwrt_error_jmp; +extern int gwrt_error_target; /* ON ERROR GOTO label, 0 = none */ +extern int gwrt_resume_label; /* label for RESUME NEXT */ + +/* Print helpers (wrappers around existing print.c) */ +void gwrt_print_sng(float v); +void gwrt_print_dbl(double v); +void gwrt_print_int(int16_t v); +void gwrt_print_str(gw_string_t s); +void gwrt_print_cstr(const char *s); +void gwrt_print_newline(void); +void gwrt_print_tab(void); /* comma zone */ +void gwrt_print_spc(int n); + +#endif diff --git a/src/analysis.c b/src/analysis.c new file mode 100644 index 0000000..75e23d0 --- /dev/null +++ b/src/analysis.c @@ -0,0 +1,286 @@ +/* + * Analysis pass for the GW-BASIC compiler. + * + * Walks the tokenized program to collect: + * - Line number table (which lines exist, which are jump targets) + * - Variable census (all variable names and types) + * - DATA literal pool (all DATA statement contents) + * - GOTO/GOSUB target set + */ + +#include "analysis.h" +#include "gwbasic.h" +#include +#include +#include + +/* Read an encoded integer from the token stream, advance *pp */ +static uint16_t read_encoded_int(uint8_t **pp) +{ + uint8_t *p = *pp; + uint16_t val = 0; + if (*p >= 0x11 && *p <= 0x1A) { + val = *p - 0x11; + *pp = p + 1; + } else if (*p == TOK_INT1) { + val = p[1]; + *pp = p + 2; + } else if (*p == TOK_INT2) { + val = (uint16_t)(p[1] | (p[2] << 8)); + *pp = p + 3; + } + return val; +} + +/* Skip an encoded constant, advance *pp */ +static void skip_constant(uint8_t **pp) +{ + uint8_t *p = *pp; + if (*p >= 0x11 && *p <= 0x1A) { *pp = p + 1; } + else if (*p == TOK_INT1) { *pp = p + 2; } + else if (*p == TOK_INT2) { *pp = p + 3; } + else if (*p == TOK_CONST_SNG) { *pp = p + 5; } + else if (*p == TOK_CONST_DBL) { *pp = p + 9; } +} + +static bool is_constant(uint8_t tok) +{ + return (tok >= 0x11 && tok <= 0x1A) || tok == TOK_INT1 || tok == TOK_INT2 + || tok == TOK_CONST_SNG || tok == TOK_CONST_DBL; +} + +static bool is_letter(uint8_t ch) +{ + return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'); +} + +/* Add a GOTO/GOSUB target line number */ +static void add_target(analysis_t *a, uint16_t line_num) +{ + for (int i = 0; i < a->goto_count; i++) + if (a->goto_targets[i] == line_num) return; + if (a->goto_count < MAX_GOTOS) + a->goto_targets[a->goto_count++] = line_num; +} + +/* Mark a line as a jump target */ +static void mark_target(analysis_t *a, uint16_t line_num) +{ + add_target(a, line_num); + for (int i = 0; i < a->line_count; i++) { + if (a->lines[i].line_num == line_num) { + a->lines[i].is_target = true; + return; + } + } +} + +int analysis_find_var(analysis_t *a, const char name[2], gw_valtype_t type) +{ + for (int i = 0; i < a->var_count; i++) { + if (a->vars[i].name[0] == name[0] && a->vars[i].name[1] == name[1] + && a->vars[i].type == type) + return i; + } + return -1; +} + +int analysis_add_var(analysis_t *a, const char name[2], gw_valtype_t type) +{ + int idx = analysis_find_var(a, name, type); + if (idx >= 0) return idx; + if (a->var_count >= MAX_VARS) return -1; + idx = a->var_count++; + a->vars[idx].name[0] = name[0]; + a->vars[idx].name[1] = name[1]; + a->vars[idx].type = type; + return idx; +} + +bool analysis_is_target(analysis_t *a, uint16_t line_num) +{ + for (int i = 0; i < a->goto_count; i++) + if (a->goto_targets[i] == line_num) return true; + return false; +} + +/* Resolve variable type from name suffix and DEF table */ +static gw_valtype_t resolve_var_type(analysis_t *a, uint8_t first_char, uint8_t suffix) +{ + if (suffix == '$') return VT_STR; + if (suffix == '%') return VT_INT; + if (suffix == '!') return VT_SNG; + if (suffix == '#') return VT_DBL; + int idx = toupper(first_char) - 'A'; + if (idx >= 0 && idx < 26) + return a->def_type[idx]; + return VT_SNG; +} + +/* Scan a token stream for variable references, GOTO targets, DATA */ +static void scan_tokens(analysis_t *a, uint8_t *tokens, int len) +{ + uint8_t *p = tokens; + uint8_t *end = tokens + len; + + while (p < end && *p) { + uint8_t tok = *p; + + /* Skip spaces */ + if (tok == ' ') { p++; continue; } + + /* Skip constants */ + if (is_constant(tok)) { skip_constant(&p); continue; } + + /* Skip string literals */ + if (tok == '"') { + p++; + while (p < end && *p && *p != '"') p++; + if (p < end && *p == '"') p++; + continue; + } + + /* GOTO/GOSUB/THEN/RESTORE/RESUME/RUN — mark targets */ + if (tok == TOK_GOTO || tok == TOK_GOSUB || tok == TOK_THEN || + tok == TOK_RESTORE || tok == TOK_RESUME || tok == TOK_RUN) { + p++; + while (p < end && *p == ' ') p++; + /* Read line number(s) */ + while (p < end && is_constant(*p)) { + uint8_t *save = p; + uint16_t target = read_encoded_int(&p); + if (p != save) + mark_target(a, target); + while (p < end && *p == ' ') p++; + if (p < end && *p == ',') { + p++; + while (p < end && *p == ' ') p++; + } else { + break; + } + } + continue; + } + + /* ON ERROR GOTO — the GOTO is followed by a line number */ + if (tok == TOK_ON) { + p++; + /* ON ERROR GOTO, ON n GOTO/GOSUB handled by scanning for GOTO/GOSUB above */ + continue; + } + + /* DATA — collect literals */ + if (tok == TOK_DATA) { + p++; + while (p < end && *p == ' ') p++; + /* Read comma-separated DATA items as raw strings */ + while (p < end && *p && *p != ':') { + while (p < end && *p == ' ') p++; + char buf[256]; + int bi = 0; + if (*p == '"') { + p++; /* skip opening quote */ + while (p < end && *p && *p != '"' && bi < 255) + buf[bi++] = *p++; + if (p < end && *p == '"') p++; + } else { + while (p < end && *p && *p != ',' && *p != ':' && bi < 255) + buf[bi++] = *p++; + /* Trim trailing spaces */ + while (bi > 0 && buf[bi-1] == ' ') bi--; + } + buf[bi] = '\0'; + if (a->data_count < MAX_DATA) + a->data_pool[a->data_count++] = strdup(buf); + while (p < end && *p == ' ') p++; + if (p < end && *p == ',') { p++; continue; } + break; + } + continue; + } + + /* REM — skip to end */ + if (tok == TOK_REM || tok == TOK_SQUOTE) { + while (p < end && *p) p++; + continue; + } + + /* Variable reference: letter followed by optional second letter, optional suffix */ + if (is_letter(tok)) { + char name[2] = {(char)toupper(tok), 0}; + p++; + if (p < end && (is_letter(*p) || (*p >= '0' && *p <= '9'))) { + name[1] = (char)toupper(*p); + p++; + /* Skip remaining chars of long name (GW-BASIC only uses first 2) */ + while (p < end && (is_letter(*p) || (*p >= '0' && *p <= '9'))) + p++; + } + uint8_t suffix = (p < end) ? *p : 0; + if (suffix == '$' || suffix == '%' || suffix == '!' || suffix == '#') + p++; + else + suffix = 0; + gw_valtype_t type = resolve_var_type(a, (uint8_t)name[0], suffix); + analysis_add_var(a, name, type); + continue; + } + + /* Extended tokens (0xFD, 0xFE, 0xFF prefix) — skip the prefix byte */ + if (tok == TOK_PREFIX_FD || tok == TOK_PREFIX_FE || tok == TOK_PREFIX_FF) { + p += 2; + continue; + } + + /* Everything else: single byte token */ + p++; + } +} + +void analysis_run(analysis_t *a) +{ + memset(a, 0, sizeof(*a)); + for (int i = 0; i < 26; i++) + a->def_type[i] = VT_SNG; + + /* Pass 1: collect line numbers and scan tokens */ + for (program_line_t *line = gw.prog_head; line; line = line->next) { + if (a->line_count >= MAX_LINES) break; + + int li = a->line_count++; + a->lines[li].line_num = line->num; + a->lines[li].is_target = false; + a->lines[li].has_data = false; + a->lines[li].data_start = a->data_count; + + /* Check for DATA in this line */ + uint8_t *p = line->tokens; + while (*p) { + if (*p == TOK_DATA) { + a->lines[li].has_data = true; + break; + } + p++; + } + + /* Record data line mapping */ + if (a->lines[li].has_data) { + int before = a->data_count; + /* DATA scanning happens in scan_tokens below */ + a->data_line_map[a->data_line_count][0] = line->num; + a->data_line_map[a->data_line_count][1] = before; + } + + scan_tokens(a, line->tokens, line->len); + + /* Finalize data line mapping */ + if (a->lines[li].has_data) { + a->data_line_map[a->data_line_count][1] = a->lines[li].data_start; + a->data_line_count++; + } + } + + /* Mark the first line as a target (program entry point) */ + if (a->line_count > 0) + a->lines[0].is_target = true; +} diff --git a/src/codegen.c b/src/codegen.c new file mode 100644 index 0000000..9f897ba --- /dev/null +++ b/src/codegen.c @@ -0,0 +1,961 @@ +/* + * C code generator for the GW-BASIC compiler. + * + * Walks the tokenized program (program_line_t linked list) and emits C + * source that calls into the gwrt runtime library. Expressions are + * compiled inline using the same precedence-climbing structure as eval.c. + * All control flow uses goto/labels (no C for/while) so GOTO into loops works. + */ + +#include "codegen.h" +#include "gwbasic.h" +#include +#include +#include + +/* ---- Emit helpers ---- */ + +static FILE *out; +static analysis_t *ana; +static uint8_t *tp; /* token pointer (mirrors gw.text_ptr) */ +static int ret_label_counter; +static int for_label_counter; + +#define EMIT(...) fprintf(out, __VA_ARGS__) + +static uint8_t cur(void) { return *tp; } +static uint8_t advance(void) +{ + tp++; + while (*tp == ' ') tp++; + return *tp; +} +static void skip_spaces(void) { while (*tp == ' ') tp++; } + +/* ---- Token stream readers ---- */ + +static uint16_t read_int(void) +{ + uint8_t tok = *tp; + if (tok >= 0x11 && tok <= 0x1A) { tp++; return tok - 0x11; } + if (tok == TOK_INT1) { uint16_t v = tp[1]; tp += 2; return v; } + if (tok == TOK_INT2) { uint16_t v = tp[1] | (tp[2] << 8); tp += 3; return v; } + return 0; +} + +static float read_sng(void) +{ + float v; + tp++; /* skip TOK_CONST_SNG */ + memcpy(&v, tp, 4); + tp += 4; + return v; +} + +static double read_dbl(void) +{ + double v; + tp++; /* skip TOK_CONST_DBL */ + memcpy(&v, tp, 8); + tp += 8; + return v; +} + +static bool is_const(uint8_t tok) +{ + return (tok >= 0x11 && tok <= 0x1A) || tok == TOK_INT1 || tok == TOK_INT2 + || tok == TOK_CONST_SNG || tok == TOK_CONST_DBL; +} + +static bool is_letter(uint8_t ch) +{ + return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'); +} + +/* ---- Variable naming ---- */ + +static const char *type_suffix(gw_valtype_t t) +{ + switch (t) { + case VT_INT: return "int"; + case VT_SNG: return "sng"; + case VT_DBL: return "dbl"; + case VT_STR: return "str"; + default: return "sng"; + } +} + +static const char *c_type(gw_valtype_t t) +{ + switch (t) { + case VT_INT: return "int16_t"; + case VT_SNG: return "float"; + case VT_DBL: return "double"; + case VT_STR: return "gw_string_t"; + default: return "float"; + } +} + +/* Emit C variable name: var_AB_int */ +static void emit_varname(const char name[2], gw_valtype_t type) +{ + if (name[1]) + EMIT("var_%c%c_%s", toupper(name[0]), toupper(name[1]), type_suffix(type)); + else + EMIT("var_%c_%s", toupper(name[0]), type_suffix(type)); +} + +/* Parse a variable name from the token stream, return its type */ +static gw_valtype_t parse_var(char name_out[2]) +{ + name_out[0] = toupper(cur()); + name_out[1] = 0; + advance(); + if (is_letter(cur()) || (cur() >= '0' && cur() <= '9')) { + name_out[1] = toupper(cur()); + advance(); + while (is_letter(cur()) || (cur() >= '0' && cur() <= '9')) + advance(); + } + uint8_t suffix = cur(); + if (suffix == '$') { advance(); return VT_STR; } + if (suffix == '%') { advance(); return VT_INT; } + if (suffix == '!') { advance(); return VT_SNG; } + if (suffix == '#') { advance(); return VT_DBL; } + int idx = name_out[0] - 'A'; + return (idx >= 0 && idx < 26) ? ana->def_type[idx] : VT_SNG; +} + +/* ---- Expression compilation ---- */ + +/* Forward declarations */ +static void emit_expr(void); +static void emit_str_expr(void); +static void emit_num_expr(void); + +static int op_prec(uint8_t tok) +{ + switch (tok) { + case TOK_IMP: return 40; + case TOK_EQV: return 42; + case TOK_XOR: return 44; + case TOK_OR: return 46; + case TOK_AND: return 48; + case TOK_NOT: return 50; + case TOK_GT: case TOK_EQ: case TOK_LT: return 64; + case TOK_PLUS: case TOK_MINUS: return 121; + case TOK_MOD: return 122; + case TOK_IDIV: return 123; + case TOK_MUL: case TOK_DIV: return 124; + case TOK_POW: return 127; + default: return -1; + } +} + +static const char *binop_c(uint8_t tok) +{ + switch (tok) { + case TOK_PLUS: return "+"; + case TOK_MINUS: return "-"; + case TOK_MUL: return "*"; + case TOK_DIV: return "/"; + case TOK_GT: return ">"; + case TOK_LT: return "<"; + case TOK_EQ: return "=="; + case TOK_AND: return "&"; + case TOK_OR: return "|"; + case TOK_XOR: return "^"; + case TOK_MOD: return "%"; + case TOK_IDIV: return "/"; + default: return "?"; + } +} + +/* Emit a numeric atom */ +static void emit_atom(void) +{ + skip_spaces(); + uint8_t tok = cur(); + + /* Numeric constant */ + if (tok >= 0x11 && tok <= 0x1A) { + EMIT("%d", tok - 0x11); + tp++; + return; + } + if (tok == TOK_INT1) { + EMIT("%d", tp[1]); + tp += 2; + return; + } + if (tok == TOK_INT2) { + int16_t v = (int16_t)(tp[1] | (tp[2] << 8)); + EMIT("%d", v); + tp += 3; + return; + } + if (tok == TOK_CONST_SNG) { + float v; + memcpy(&v, tp + 1, 4); + tp += 5; + EMIT("%.9gf", v); + return; + } + if (tok == TOK_CONST_DBL) { + double v; + memcpy(&v, tp + 1, 8); + tp += 9; + EMIT("%.17g", v); + return; + } + + /* Parenthesized expression */ + if (tok == '(') { + EMIT("("); + advance(); + emit_num_expr(); + if (cur() == ')') advance(); + EMIT(")"); + return; + } + + /* Unary minus */ + if (tok == TOK_MINUS) { + EMIT("(-"); + advance(); + emit_atom(); + EMIT(")"); + return; + } + + /* NOT */ + if (tok == TOK_NOT) { + EMIT("(~(int16_t)("); + advance(); + emit_num_expr(); + EMIT("))"); + return; + } + + /* Built-in functions (0xFF prefix) */ + if (tok == TOK_PREFIX_FF) { + uint8_t func = tp[1]; + tp += 2; + skip_spaces(); + switch (func) { + case FUNC_ABS: + EMIT("fabs("); advance(); /* ( */ emit_num_expr(); + if (cur() == ')') advance(); EMIT(")"); return; + case FUNC_INT: + EMIT("floor("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT(")"); return; + case FUNC_SQR: + EMIT("sqrt("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT(")"); return; + case FUNC_SIN: + EMIT("sin("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT(")"); return; + case FUNC_COS: + EMIT("cos("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT(")"); return; + case FUNC_TAN: + EMIT("tan("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT(")"); return; + case FUNC_ATN: + EMIT("atan("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT(")"); return; + case FUNC_LOG: + EMIT("log("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT(")"); return; + case FUNC_EXP: + EMIT("exp("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT(")"); return; + case FUNC_SGN: { + EMIT("({ double _v = ("); + advance(); emit_num_expr(); + if (cur() == ')') advance(); + EMIT("); (_v > 0) - (_v < 0); })"); + return; + } + case FUNC_RND: { + /* RND may or may not have parens */ + if (cur() == '(') { + advance(); emit_num_expr(); + if (cur() == ')') advance(); + } + EMIT("((float)rand() / RAND_MAX)"); + return; + } + case FUNC_CINT: + EMIT("((int16_t)gw_round("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT("))"); return; + case FUNC_CSNG: + EMIT("((float)("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT("))"); return; + case FUNC_CDBL: + EMIT("((double)("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT("))"); return; + case FUNC_FIX: + EMIT("((int16_t)("); advance(); emit_num_expr(); + if (cur() == ')') advance(); EMIT("))"); return; + case FUNC_LEN: { + EMIT("gw_fn_len(&(gw_value_t){.type=VT_STR,.sval="); + advance(); emit_str_expr(); + if (cur() == ')') advance(); + EMIT("}).ival"); + return; + } + case FUNC_ASC: { + EMIT("gw_fn_asc(&(gw_value_t){.type=VT_STR,.sval="); + advance(); emit_str_expr(); + if (cur() == ')') advance(); + EMIT("}).ival"); + return; + } + case FUNC_VAL: { + EMIT("gw_fn_val(&(gw_value_t){.type=VT_STR,.sval="); + advance(); emit_str_expr(); + if (cur() == ')') advance(); + EMIT("}).dval"); + return; + } + case 0xD6 /* INSTR */: { + /* INSTR([start,] haystack$, needle$) */ + EMIT("gw_fn_instr(1, "); + advance(); /* ( */ + /* TODO: handle optional start parameter */ + EMIT("&(gw_value_t){.type=VT_STR,.sval="); + emit_str_expr(); + EMIT("}, "); + if (cur() == ',') advance(); + EMIT("&(gw_value_t){.type=VT_STR,.sval="); + emit_str_expr(); + if (cur() == ')') advance(); + EMIT("}).ival"); + return; + } + case FUNC_POS: + EMIT("(gw_hal ? gw_hal->get_cursor_col() + 1 : 1)"); + if (cur() == '(') { advance(); emit_num_expr(); if (cur() == ')') advance(); } + return; + case FUNC_FRE: + EMIT("((float)strpool_free())"); + if (cur() == '(') { + advance(); + /* consume arg but ignore */ + while (cur() && cur() != ')') tp++; + if (cur() == ')') advance(); + } + return; + case FUNC_PEEK: + EMIT("virmem_peek(gw.def_seg, "); + advance(); emit_num_expr(); + if (cur() == ')') advance(); + EMIT(")"); + return; + case FUNC_INP: + EMIT("portio_inp("); + advance(); emit_num_expr(); + if (cur() == ')') advance(); + EMIT(")"); + return; + default: + /* Fallback: emit 0 for unhandled functions */ + EMIT("0 /* unhandled func 0x%02x */", func); + if (cur() == '(') { advance(); while (cur() && cur() != ')') tp++; if (cur() == ')') advance(); } + return; + } + } + + /* Variable */ + if (is_letter(tok)) { + char name[2]; + gw_valtype_t type = parse_var(name); + if (type == VT_STR) { + /* String var used in numeric context — likely LEN or similar */ + EMIT("0 /* str var in num ctx */"); + } else { + emit_varname(name, type); + } + return; + } + + /* Fallback */ + EMIT("0 /* unknown tok 0x%02x */", tok); + tp++; +} + +/* Emit numeric expression with precedence climbing */ +static void emit_num_prec(int min_prec) +{ + emit_atom(); + for (;;) { + skip_spaces(); + int prec = op_prec(cur()); + if (prec < min_prec) break; + uint8_t op = cur(); + advance(); + EMIT(" %s ", binop_c(op)); + emit_num_prec(prec + 1); + } +} + +static void emit_num_expr(void) +{ + emit_num_prec(0); +} + +/* Emit a string expression */ +static void emit_str_expr(void) +{ + skip_spaces(); + uint8_t tok = cur(); + + /* String literal */ + if (tok == '"') { + EMIT("gw_str_from_cstr(\""); + tp++; + while (*tp && *tp != '"') { + if (*tp == '\\' || *tp == '"') EMIT("\\"); + EMIT("%c", *tp); + tp++; + } + if (*tp == '"') tp++; + EMIT("\")"); + /* Handle concatenation */ + skip_spaces(); + while (cur() == TOK_PLUS) { + advance(); + EMIT("; _cat = gw_str_concat(&(gw_value_t){.type=VT_STR,.sval=_cat.sval}, &(gw_value_t){.type=VT_STR,.sval="); + emit_str_expr(); + EMIT("})"); + } + return; + } + + /* String functions (0xFF prefix) */ + if (tok == TOK_PREFIX_FF) { + uint8_t func = tp[1]; + tp += 2; + skip_spaces(); + switch (func) { + case FUNC_CHR: + EMIT("gw_fn_chr("); + advance(); emit_num_expr(); + if (cur() == ')') advance(); + EMIT(").sval"); + return; + case FUNC_STR: + EMIT("gw_fn_str(&(gw_value_t){.type=VT_DBL,.dval=(double)("); + advance(); emit_num_expr(); + if (cur() == ')') advance(); + EMIT(")}).sval"); + return; + case FUNC_LEFT: { + EMIT("gw_fn_left(&(gw_value_t){.type=VT_STR,.sval="); + advance(); emit_str_expr(); + if (cur() == ',') advance(); + EMIT("}, "); + emit_num_expr(); + if (cur() == ')') advance(); + EMIT(").sval"); + return; + } + case FUNC_RIGHT: { + EMIT("gw_fn_right(&(gw_value_t){.type=VT_STR,.sval="); + advance(); emit_str_expr(); + if (cur() == ',') advance(); + EMIT("}, "); + emit_num_expr(); + if (cur() == ')') advance(); + EMIT(").sval"); + return; + } + case FUNC_MID: { + EMIT("gw_fn_mid(&(gw_value_t){.type=VT_STR,.sval="); + advance(); emit_str_expr(); + if (cur() == ',') advance(); + EMIT("}, "); + emit_num_expr(); + EMIT(", 255"); + if (cur() == ',') { advance(); EMIT("); /* mid with len */ "); } + if (cur() == ')') advance(); + EMIT(").sval"); + return; + } + case FUNC_SPACE: + EMIT("gw_fn_space("); + advance(); emit_num_expr(); + if (cur() == ')') advance(); + EMIT(").sval"); + return; + case 0xD4 /* STRING$ */: + EMIT("gw_fn_strings("); + advance(); emit_num_expr(); + if (cur() == ',') advance(); + EMIT(", "); + emit_num_expr(); + if (cur() == ')') advance(); + EMIT(").sval"); + return; + case FUNC_HEX: + EMIT("gw_fn_hex("); + advance(); emit_num_expr(); + if (cur() == ')') advance(); + EMIT(").sval"); + return; + case FUNC_OCT: + EMIT("gw_fn_oct("); + advance(); emit_num_expr(); + if (cur() == ')') advance(); + EMIT(").sval"); + return; + default: + EMIT("gw_str_from_cstr(\"\") /* unhandled str func */"); + return; + } + } + + /* String variable */ + if (is_letter(tok)) { + char name[2]; + gw_valtype_t type = parse_var(name); + if (type == VT_STR) { + EMIT("gw_str_copy(&"); + emit_varname(name, type); + EMIT(")"); + } else { + EMIT("gw_str_from_cstr(\"\") /* num var in str ctx */"); + } + return; + } + + EMIT("gw_str_from_cstr(\"\") /* unknown str expr */"); +} + +/* Emit a general expression (caller determines context) */ +static void emit_expr(void) +{ + emit_num_expr(); +} + +/* ---- Statement compilation ---- */ + +static void emit_stmt(void); + +static void emit_print(void) +{ + skip_spaces(); + bool need_newline = true; + + while (cur() && cur() != ':' && cur() != TOK_ELSE) { + skip_spaces(); + + if (cur() == ';') { + need_newline = false; + advance(); + continue; + } + if (cur() == ',') { + EMIT(" gwrt_print_tab();\n"); + need_newline = false; + advance(); + continue; + } + + /* Detect string vs numeric expression */ + /* String: starts with " or a string variable or string function */ + uint8_t tok = cur(); + bool is_str = (tok == '"'); + if (is_letter(tok)) { + /* Peek ahead for $ suffix */ + uint8_t *save = tp; + char name[2]; + gw_valtype_t type = parse_var(name); + tp = save; /* restore */ + is_str = (type == VT_STR); + } + if (tok == TOK_PREFIX_FF) { + uint8_t func = tp[1]; + is_str = (func == FUNC_CHR || func == FUNC_STR || func == FUNC_LEFT + || func == FUNC_RIGHT || func == FUNC_MID || func == FUNC_SPACE + || func == 0xD4 /* STRING$ */ || func == FUNC_HEX || func == FUNC_OCT); + } + + if (is_str) { + EMIT(" { gw_string_t _s = "); + emit_str_expr(); + EMIT("; gwrt_print_str(_s); gw_str_free(&_s); }\n"); + } else { + EMIT(" gwrt_print_sng((float)("); + emit_num_expr(); + EMIT("));\n"); + } + need_newline = true; + } + + if (need_newline) + EMIT(" gwrt_print_newline();\n"); +} + +static void emit_assignment(void) +{ + char name[2]; + gw_valtype_t type = parse_var(name); + skip_spaces(); + + /* Array element? */ + if (cur() == '(') { + /* TODO: array assignment */ + EMIT(" /* TODO: array assignment */\n"); + while (cur() && cur() != ':') tp++; + return; + } + + if (cur() != TOK_EQ) { + EMIT(" /* expected = */\n"); + return; + } + advance(); + + if (type == VT_STR) { + EMIT(" gw_str_free(&"); + emit_varname(name, type); + EMIT(");\n"); + EMIT(" "); + emit_varname(name, type); + EMIT(" = "); + emit_str_expr(); + EMIT(";\n"); + } else { + EMIT(" "); + emit_varname(name, type); + EMIT(" = (%s)(", c_type(type)); + emit_num_expr(); + EMIT(");\n"); + } +} + +/* Compile one statement */ +static void emit_stmt(void) +{ + skip_spaces(); + uint8_t tok = cur(); + + if (tok == 0 || tok == ':') return; + + /* REM */ + if (tok == TOK_REM || tok == TOK_SQUOTE) { + while (*tp) tp++; + return; + } + + /* PRINT */ + if (tok == TOK_PRINT || tok == '?') { + advance(); + emit_print(); + return; + } + + /* GOTO */ + if (tok == TOK_GOTO) { + advance(); + skip_spaces(); + uint16_t target = read_int(); + EMIT(" goto L_%u;\n", target); + return; + } + + /* GOSUB */ + if (tok == TOK_GOSUB) { + advance(); + skip_spaces(); + uint16_t target = read_int(); + int rl = ret_label_counter++; + EMIT(" gwrt_gosub_push(%d); goto L_%u;\n", rl, target); + EMIT("ret_%d: ;\n", rl); + return; + } + + /* RETURN */ + if (tok == TOK_RETURN) { + advance(); + EMIT(" switch(gwrt_gosub_pop()) {\n"); + for (int i = 0; i < ret_label_counter; i++) + EMIT(" case %d: goto ret_%d;\n", i, i); + EMIT(" }\n"); + return; + } + + /* FOR */ + if (tok == TOK_FOR) { + advance(); + char name[2]; + gw_valtype_t type = parse_var(name); + skip_spaces(); + if (cur() == TOK_EQ) advance(); + EMIT(" "); + emit_varname(name, type); + EMIT(" = (%s)(", c_type(type)); + emit_num_expr(); + EMIT(");\n"); + skip_spaces(); + if (cur() == TOK_TO) advance(); + /* Store limit in a temp */ + EMIT(" { %s _for_limit_%d = (%s)(", c_type(type), for_label_counter, c_type(type)); + emit_num_expr(); + EMIT(");\n"); + /* Step (default 1) */ + EMIT(" %s _for_step_%d = 1;\n", c_type(type), for_label_counter); + skip_spaces(); + if (cur() == TOK_STEP) { + advance(); + EMIT(" _for_step_%d = (%s)(", for_label_counter, c_type(type)); + emit_num_expr(); + EMIT(");\n"); + } + EMIT(" for_top_%d:\n", for_label_counter); + EMIT(" if (_for_step_%d >= 0 ? ", for_label_counter); + emit_varname(name, type); + EMIT(" > _for_limit_%d : ", for_label_counter); + emit_varname(name, type); + EMIT(" < _for_limit_%d) goto for_done_%d;\n", for_label_counter, for_label_counter); + for_label_counter++; + return; + } + + /* NEXT */ + if (tok == TOK_NEXT) { + advance(); + skip_spaces(); + int fc = for_label_counter - 1; /* match most recent FOR */ + if (is_letter(cur())) { + char name[2]; + gw_valtype_t type = parse_var(name); + EMIT(" "); + emit_varname(name, type); + EMIT(" += _for_step_%d;\n", fc); + } + EMIT(" goto for_top_%d;\n", fc); + EMIT(" for_done_%d: ;\n", fc); + EMIT(" }\n"); + return; + } + + /* IF / THEN / ELSE */ + if (tok == TOK_IF) { + advance(); + EMIT(" if (("); + emit_num_expr(); + EMIT(") != 0) {\n"); + skip_spaces(); + if (cur() == TOK_THEN) advance(); + skip_spaces(); + /* THEN followed by line number? */ + if (is_const(cur())) { + uint16_t target = read_int(); + EMIT(" goto L_%u;\n", target); + } else { + /* THEN followed by statements */ + while (cur() && cur() != TOK_ELSE && cur() != 0) { + if (cur() == ':') { advance(); continue; } + emit_stmt(); + } + } + if (cur() == TOK_ELSE) { + advance(); + EMIT(" } else {\n"); + skip_spaces(); + if (is_const(cur())) { + uint16_t target = read_int(); + EMIT(" goto L_%u;\n", target); + } else { + while (cur() && cur() != 0) { + if (cur() == ':') { advance(); continue; } + emit_stmt(); + } + } + } + EMIT(" }\n"); + return; + } + + /* END */ + if (tok == TOK_END) { + advance(); + EMIT(" gwrt_shutdown(); return 0;\n"); + return; + } + + /* STOP */ + if (tok == TOK_STOP) { + advance(); + EMIT(" gwrt_shutdown(); return 0;\n"); + return; + } + + /* Extended statements (0xFE prefix) */ + if (tok == TOK_PREFIX_FE) { + uint8_t xstmt = tp[1]; + tp += 2; + skip_spaces(); + + if (xstmt == XSTMT_SYSTEM) { + EMIT(" gwrt_shutdown(); exit(0);\n"); + return; + } + + /* Fallback for unhandled extended statements */ + EMIT(" /* TODO: xstmt 0x%02x */\n", xstmt); + while (cur() && cur() != ':') tp++; + return; + } + + /* DATA — skip (collected at analysis time) */ + if (tok == TOK_DATA) { + while (*tp && *tp != ':') tp++; + return; + } + + /* READ */ + if (tok == TOK_READ) { + advance(); + while (cur() && cur() != ':' && cur() != 0) { + skip_spaces(); + if (is_letter(cur())) { + char name[2]; + gw_valtype_t type = parse_var(name); + if (type == VT_STR) { + EMIT(" gw_str_free(&"); + emit_varname(name, type); + EMIT(");\n"); + EMIT(" "); + emit_varname(name, type); + EMIT(" = gw_str_from_cstr(gwrt_data_read());\n"); + } else { + EMIT(" "); + emit_varname(name, type); + EMIT(" = (%s)atof(gwrt_data_read());\n", c_type(type)); + } + } + skip_spaces(); + if (cur() == ',') advance(); + else break; + } + return; + } + + /* RESTORE */ + if (tok == TOK_RESTORE) { + advance(); + skip_spaces(); + if (is_const(cur())) { + uint16_t line = read_int(); + /* Find data index for this line */ + EMIT(" gwrt_data_restore(data_line_%u);\n", line); + } else { + EMIT(" gwrt_data_restore(0);\n"); + } + return; + } + + /* LET (explicit) */ + if (tok == TOK_LET) { + advance(); + emit_assignment(); + return; + } + + /* CLS */ + if (tok == TOK_CLS) { + advance(); + EMIT(" if (gw_hal) gw_hal->cls();\n"); + return; + } + + /* Implicit assignment (variable at start of statement) */ + if (is_letter(tok)) { + emit_assignment(); + return; + } + + /* Skip unknown tokens */ + EMIT(" /* skip tok 0x%02x */\n", tok); + tp++; +} + +/* ---- Main code generation ---- */ + +void codegen_emit(FILE *f, analysis_t *a) +{ + out = f; + ana = a; + ret_label_counter = 0; + for_label_counter = 0; + + /* Header */ + EMIT("/* Generated by gwbasic-compile */\n"); + EMIT("#include \"gwrt.h\"\n"); + EMIT("#include \n"); + EMIT("#include \n"); + EMIT("#include \n\n"); + + /* Variable declarations */ + for (int i = 0; i < a->var_count; i++) { + EMIT("static %s ", c_type(a->vars[i].type)); + emit_varname(a->vars[i].name, a->vars[i].type); + if (a->vars[i].type == VT_STR) + EMIT(" = {0, NULL}"); + else + EMIT(" = 0"); + EMIT(";\n"); + } + EMIT("\n"); + + /* DATA pool */ + if (a->data_count > 0) { + EMIT("static const char *_data_pool[] = {\n"); + for (int i = 0; i < a->data_count; i++) + EMIT(" \"%s\",\n", a->data_pool[i]); + EMIT(" NULL\n};\n"); + + /* Data line map (for RESTORE n) */ + for (int i = 0; i < a->data_line_count; i++) + EMIT("static const int data_line_%d = %d;\n", + a->data_line_map[i][0], a->data_line_map[i][1]); + EMIT("\n"); + } + + /* Main function */ + EMIT("int main(int argc, char **argv) {\n"); + EMIT(" (void)argc; (void)argv;\n"); + EMIT(" gwrt_init();\n"); + if (a->data_count > 0) + EMIT(" gwrt_data_set(_data_pool, NULL, 0);\n"); + EMIT("\n"); + + /* Emit code for each program line */ + for (program_line_t *line = gw.prog_head; line; line = line->next) { + /* Label (always emit for targets, comment for others) */ + if (analysis_is_target(a, line->num)) + EMIT("L_%u:\n", line->num); + else + EMIT("/* %u */ ", line->num); + + EMIT(" gwrt_check_line(%u);\n", line->num); + + /* Walk statements on this line */ + tp = line->tokens; + while (*tp) { + skip_spaces(); + if (*tp == ':') { tp++; continue; } + if (*tp == 0) break; + emit_stmt(); + } + } + + /* Implicit END at bottom */ + EMIT(" gwrt_shutdown();\n"); + EMIT(" return 0;\n"); + EMIT("}\n"); +} diff --git a/src/compiler_main.c b/src/compiler_main.c new file mode 100644 index 0000000..12a938c --- /dev/null +++ b/src/compiler_main.c @@ -0,0 +1,233 @@ +/* + * gwbasic-compile: Ahead-of-time compiler for GW-BASIC programs. + * + * Tokenizes a .bas file using the existing tokenizer, runs an analysis + * pass to collect variables/targets/DATA, then emits C source that links + * against libgwrt to produce a native executable. + */ + +#include "gwbasic.h" +#include "analysis.h" +#include "codegen.h" +#include "strpool.h" +#include +#include +#include + +/* Global interpreter state (needed by tokenizer and analysis) */ +interp_state_t gw; +hal_ops_t *gw_hal = NULL; + +/* Stubs for symbols referenced by shared modules but not needed by compiler */ +jmp_buf gw_error_jmp; +jmp_buf gw_run_jmp; +int print_col = 0; + +void gw_init(void) +{ + memset(&gw, 0, sizeof(gw)); + for (int i = 0; i < 26; i++) + gw.def_type[i] = VT_SNG; +} + +/* Minimal main.c functions the compiler needs */ +uint8_t gw_chrget(void) +{ + gw.text_ptr++; + while (*gw.text_ptr == ' ') gw.text_ptr++; + return *gw.text_ptr; +} + +uint8_t gw_chrgot(void) { return *gw.text_ptr; } + +void gw_skip_spaces(void) +{ + while (*gw.text_ptr == ' ') gw.text_ptr++; +} + +/* Store a tokenized line into the program */ +static void store_line(uint16_t num, uint8_t *tokens, int len) +{ + program_line_t *line = malloc(sizeof(program_line_t)); + line->num = num; + line->len = len; + line->tokens = malloc(len + 1); + memcpy(line->tokens, tokens, len); + line->tokens[len] = 0; + line->next = NULL; + + /* Insert in order */ + if (!gw.prog_head || num < gw.prog_head->num) { + line->next = gw.prog_head; + gw.prog_head = line; + return; + } + program_line_t *prev = gw.prog_head; + while (prev->next && prev->next->num < num) + prev = prev->next; + if (prev->next && prev->next->num == num) { + /* Replace existing line */ + program_line_t *old = prev->next; + line->next = old->next; + prev->next = line; + free(old->tokens); + free(old); + } else { + line->next = prev->next; + prev->next = line; + } +} + +/* Parse a line number from the start of a text line */ +static int parse_line_num(const char *text, uint16_t *num) +{ + const char *p = text; + while (*p == ' ') p++; + if (*p < '0' || *p > '9') return 0; + *num = 0; + while (*p >= '0' && *p <= '9') { + *num = *num * 10 + (*p - '0'); + p++; + } + return (int)(p - text); +} + +/* Load a .bas file (ASCII format) */ +static int load_file(const char *filename) +{ + FILE *f = fopen(filename, "r"); + if (!f) { + fprintf(stderr, "Cannot open: %s\n", filename); + return 1; + } + + char buf[256]; + uint8_t kbuf[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; + + uint16_t num; + int skip = parse_line_num(buf, &num); + if (skip == 0) continue; /* skip non-numbered lines */ + + /* Tokenize the line content (after the line number) */ + const char *content = buf + skip; + while (*content == ' ') content++; + int tok_len = gw_crunch(content, kbuf, sizeof(kbuf)); + store_line(num, kbuf, tok_len); + } + + fclose(f); + return 0; +} + +static void usage(void) +{ + fprintf(stderr, + "Usage: gwbasic-compile [options] input.bas\n" + "Options:\n" + " -o FILE Output C source file (default: stdout)\n" + " -c Compile to executable (invoke gcc)\n" + " -O LEVEL GCC optimization level (default: 2)\n" + " --keep-c Keep generated C file (with -c)\n" + " --runtime DIR Path to runtime headers/library\n" + ); +} + +int main(int argc, char **argv) +{ + const char *input = NULL; + const char *output = NULL; + bool compile_exe = false; + int opt_level = 2; + bool keep_c = false; + const char *runtime_dir = NULL; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) + output = argv[++i]; + else if (strcmp(argv[i], "-c") == 0) + compile_exe = true; + else if (strcmp(argv[i], "-O") == 0 && i + 1 < argc) + opt_level = atoi(argv[++i]); + else if (strcmp(argv[i], "--keep-c") == 0) + keep_c = true; + else if (strcmp(argv[i], "--runtime") == 0 && i + 1 < argc) + runtime_dir = argv[++i]; + else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + usage(); + return 0; + } + else if (argv[i][0] != '-') + input = argv[i]; + } + + if (!input) { + usage(); + return 1; + } + + gw_init(); + + if (load_file(input) != 0) + return 1; + + if (!gw.prog_head) { + fprintf(stderr, "No program lines found in %s\n", input); + return 1; + } + + /* Analysis pass */ + analysis_t analysis; + analysis_run(&analysis); + + /* Code generation */ + const char *c_file = output; + char c_file_buf[512]; + if (compile_exe && !output) { + snprintf(c_file_buf, sizeof(c_file_buf), "%s.c", input); + c_file = c_file_buf; + } + + FILE *f = c_file ? fopen(c_file, "w") : stdout; + if (!f) { + fprintf(stderr, "Cannot write: %s\n", c_file); + return 1; + } + + codegen_emit(f, &analysis); + + if (f != stdout) + fclose(f); + + /* Compile to executable if requested */ + if (compile_exe) { + char cmd[2048]; + const char *rt = runtime_dir ? runtime_dir : "."; + /* Derive executable name from input */ + char exe_name[512]; + strncpy(exe_name, input, sizeof(exe_name) - 1); + char *dot = strrchr(exe_name, '.'); + if (dot) *dot = '\0'; + + snprintf(cmd, sizeof(cmd), + "gcc -O%d -o %s %s -I%s/include -L%s/build -lgwrt -lm -lpthread 2>&1", + opt_level, exe_name, c_file, rt, rt); + + int rc = system(cmd); + if (rc != 0) { + fprintf(stderr, "gcc failed (exit %d)\n", rc); + return 1; + } + + if (!keep_c && c_file != output) + remove(c_file); + + fprintf(stderr, "Compiled: %s\n", exe_name); + } + + return 0; +} diff --git a/src/gwrt.c b/src/gwrt.c new file mode 100644 index 0000000..6dc2240 --- /dev/null +++ b/src/gwrt.c @@ -0,0 +1,222 @@ +/* + * Runtime library for compiled GW-BASIC programs. + * + * Provides the global interpreter state (gw, gw_hal) and core functions + * (gw_chrget, gw_chrgot, etc.) that the existing modules reference. + * When used in the interpreter, these are in main.c; in the runtime + * library, they live here instead. + */ + +#include "gwrt.h" +#include "tui.h" +#include +#include +#include +#include + +/* + * Global state + core functions that main.c provides in the interpreter. + * In the runtime library, gwrt.c provides them instead. + */ +interp_state_t gw; +hal_ops_t *gw_hal = NULL; + +uint8_t gw_chrget(void) +{ + gw.text_ptr++; + while (*gw.text_ptr == ' ') gw.text_ptr++; + return *gw.text_ptr; +} + +uint8_t gw_chrgot(void) { return *gw.text_ptr; } + +void gw_skip_spaces(void) { while (*gw.text_ptr == ' ') gw.text_ptr++; } + +bool gw_is_digit(uint8_t ch) { return ch >= '0' && ch <= '9'; } + +bool gw_is_letter(uint8_t ch) +{ + return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'); +} + +void gw_expect(uint8_t expected) +{ + gw_skip_spaces(); + if (gw_chrgot() != expected) + gw_error(ERR_SN); + gw_chrget(); +} + +/* gw_init — same as in main.c, needed by runtime modules */ +void gw_init(void) +{ + gw_free_program(); + gw_vars_clear(); + gw_arrays_clear(); + memset(&gw, 0, sizeof(gw)); + gw.cur_line_num = LINE_DIRECT; + for (int i = 0; i < 26; i++) + gw.def_type[i] = VT_SNG; +} + +/* Stub for gw_exec_direct — not used in compiled programs but referenced by tui.c */ +void gw_exec_direct(const char *line) { (void)line; } + +/* DATA pool */ +static const char **data_pool; +static int data_index; +static const int *data_line_map; /* line_num → data_index pairs */ +static int data_line_count; + +/* GOSUB return-label stack */ +static int gosub_stack[GWRT_GOSUB_MAX]; +static int gosub_sp; + +/* Error handling */ +jmp_buf gwrt_error_jmp; +int gwrt_error_target; +int gwrt_resume_label; + +void gwrt_init(void) +{ + gw_hal = hal_posix_create(); + gw_hal->init(); + memset(&gw, 0, sizeof(gw)); + for (int i = 0; i < 26; i++) + gw.def_type[i] = VT_SNG; + strpool_init(STRPOOL_DEFAULT_SIZE); + snd_init(); + portio_reset(); + gw.running = true; + + data_pool = NULL; + data_index = 0; + data_line_map = NULL; + data_line_count = 0; + gosub_sp = 0; + gwrt_error_target = 0; + gwrt_resume_label = 0; +} + +void gwrt_shutdown(void) +{ + gw_lpt_close(); + snd_shutdown(); + strpool_shutdown(); + if (gw_hal) gw_hal->shutdown(); +} + +/* --- DATA / READ / RESTORE --- */ + +void gwrt_data_set(const char **pool, const int *line_map, int line_count) +{ + data_pool = pool; + data_line_map = line_map; + data_line_count = line_count; + data_index = 0; +} + +void gwrt_data_restore(int index) +{ + data_index = index; +} + +const char *gwrt_data_read(void) +{ + if (!data_pool || !data_pool[data_index]) + gw_error(ERR_OD); + return data_pool[data_index++]; +} + +/* --- GOSUB stack --- */ + +void gwrt_gosub_push(int label) +{ + if (gosub_sp >= GWRT_GOSUB_MAX) + gw_error(ERR_OM); + gosub_stack[gosub_sp++] = label; +} + +int gwrt_gosub_pop(void) +{ + if (gosub_sp <= 0) + gw_error(ERR_RG); + return gosub_stack[--gosub_sp]; +} + +/* --- Event + GC check (called at each line boundary) --- */ + +void gwrt_check_line(uint16_t line_num) +{ + gw.cur_line_num = line_num; + + /* String pool GC */ + if (strpool_free() < STRPOOL_GC_THRESHOLD) + strpool_gc(); + + /* Ctrl+Break check */ + if (tui.active) + tui_check_break(); +} + +/* --- Print helpers --- */ + +void gwrt_print_int(int16_t v) +{ + gw_value_t val = {.type = VT_INT, .ival = v}; + gw_print_value(&val); +} + +void gwrt_print_sng(float v) +{ + gw_value_t val = {.type = VT_SNG, .fval = v}; + gw_print_value(&val); +} + +void gwrt_print_dbl(double v) +{ + gw_value_t val = {.type = VT_DBL, .dval = v}; + gw_print_value(&val); +} + +void gwrt_print_str(gw_string_t s) +{ + gw_value_t val = {.type = VT_STR, .sval = s}; + /* gw_print_value frees the string — pass a copy so caller keeps theirs */ + val.sval = gw_str_copy(&s); + gw_print_value(&val); +} + +void gwrt_print_cstr(const char *s) +{ + if (gw_hal) { + gw_hal->puts(s); + } else { + fputs(s, stdout); + } +} + +void gwrt_print_newline(void) +{ + gw_print_newline(); +} + +void gwrt_print_tab(void) +{ + /* Advance to next 14-column zone (GW-BASIC comma zone) */ + extern int print_col; + int target = ((print_col / 14) + 1) * 14; + while (print_col < target) { + if (gw_hal) gw_hal->putch(' '); + else putchar(' '); + print_col++; + } +} + +void gwrt_print_spc(int n) +{ + for (int i = 0; i < n; i++) { + if (gw_hal) gw_hal->putch(' '); + else putchar(' '); + } +} diff --git a/src/print.c b/src/print.c index 0a1b9e9..8a674d6 100644 --- a/src/print.c +++ b/src/print.c @@ -2,8 +2,8 @@ #include #include -/* Screen print column tracking */ -static int print_col = 0; +/* Screen print column tracking (non-static: accessed by gwrt.c for compiled programs) */ +int print_col = 0; /* Printer (LPT1) state */ static FILE *lpt_fp;