From d8e83753667d1114db5e670c6e80078f6ffbf895 Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Tue, 10 Feb 2026 10:25:08 -0500 Subject: [PATCH] Phase 1: expression calculator with direct mode GW-BASIC reimplementation in C11, using Microsoft's open-sourced 8088 assembly as the authoritative reference. Tokenizer (CRUNCH/LIST), expression evaluator with operator precedence, all math functions (SIN, COS, TAN, ATN, SQR, LOG, EXP, RND, etc.), string functions (LEFT$, RIGHT$, MID$, CHR$, ASC, VAL, STR$, etc.), PRINT statement with comma/semicolon zones, TAB(), SPC(). Handles integer/single/double types with correct promotion, D exponent for double-precision literals, type suffixes (%, !, #), &H hex/&O octal literals, MBF conversion routines, and GW-BASIC-compatible number formatting. Platform-independent via HAL vtable; POSIX backend included. --- .gitignore | 3 + CMakeLists.txt | 29 ++ LICENSE | 21 + include/error.h | 60 +++ include/eval.h | 21 + include/gw_math.h | 54 ++ include/gwbasic.h | 25 + include/hal.h | 34 ++ include/interp.h | 51 ++ include/strings.h | 32 ++ include/tokens.h | 231 +++++++++ include/types.h | 58 +++ platform/hal_posix.c | 117 +++++ src/error.c | 83 ++++ src/eval.c | 900 ++++++++++++++++++++++++++++++++++ src/main.c | 223 +++++++++ src/math_float.c | 333 +++++++++++++ src/math_int.c | 50 ++ src/math_transcend.c | 87 ++++ src/print.c | 125 +++++ src/strings.c | 250 ++++++++++ src/tokenizer.c | 362 ++++++++++++++ src/tokens.c | 213 ++++++++ tests/programs/hello.bas | 1 + tests/programs/math_ops.bas | 19 + tests/programs/string_ops.bas | 14 + 26 files changed, 3396 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 include/error.h create mode 100644 include/eval.h create mode 100644 include/gw_math.h create mode 100644 include/gwbasic.h create mode 100644 include/hal.h create mode 100644 include/interp.h create mode 100644 include/strings.h create mode 100644 include/tokens.h create mode 100644 include/types.h create mode 100644 platform/hal_posix.c create mode 100644 src/error.c create mode 100644 src/eval.c create mode 100644 src/main.c create mode 100644 src/math_float.c create mode 100644 src/math_int.c create mode 100644 src/math_transcend.c create mode 100644 src/print.c create mode 100644 src/strings.c create mode 100644 src/tokenizer.c create mode 100644 src/tokens.c create mode 100644 tests/programs/hello.bas create mode 100644 tests/programs/math_ops.bas create mode 100644 tests/programs/string_ops.bas diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e3f78a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +*.o +*.d diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1da2ad0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.10) +project(gwbasic C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Warnings +add_compile_options(-Wall -Wextra -Wno-unused-parameter) + +# Include paths +include_directories(include) + +# Source files +set(SOURCES + src/main.c + src/tokens.c + src/tokenizer.c + src/error.c + src/eval.c + src/math_int.c + src/math_float.c + src/math_transcend.c + src/strings.c + src/print.c + platform/hal_posix.c +) + +add_executable(gwbasic ${SOURCES}) +target_link_libraries(gwbasic m) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d7099d3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Eremey Valetov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/include/error.h b/include/error.h new file mode 100644 index 0000000..1d10c89 --- /dev/null +++ b/include/error.h @@ -0,0 +1,60 @@ +#ifndef GW_ERROR_H +#define GW_ERROR_H + +#include + +/* Error codes matching GW-BASIC's ERRTAB */ +#define ERR_NF 1 /* NEXT without FOR */ +#define ERR_SN 2 /* Syntax error */ +#define ERR_RG 3 /* RETURN without GOSUB */ +#define ERR_OD 4 /* Out of DATA */ +#define ERR_FC 5 /* Illegal function call */ +#define ERR_OV 6 /* Overflow */ +#define ERR_OM 7 /* Out of memory */ +#define ERR_UL 8 /* Undefined line number */ +#define ERR_BS 9 /* Subscript out of range */ +#define ERR_DD 10 /* Duplicate Definition */ +#define ERR_DZ 11 /* Division by zero */ +#define ERR_ID 12 /* Illegal direct */ +#define ERR_TM 13 /* Type mismatch */ +#define ERR_OS 14 /* Out of string space */ +#define ERR_LS 15 /* String too long */ +#define ERR_ST 16 /* String formula too complex */ +#define ERR_CN 17 /* Can't continue */ +#define ERR_UF 18 /* Undefined user function */ +#define ERR_NR 19 /* No RESUME */ +#define ERR_RW 20 /* RESUME without error */ +#define ERR_UE 21 /* Unprintable error */ +#define ERR_MO 22 /* Missing operand */ +#define ERR_BO 23 /* Line buffer overflow */ +#define ERR_DT 24 /* Device Timeout */ +#define ERR_DF 25 /* Device Fault */ +#define ERR_FW 26 /* FOR without NEXT */ +#define ERR_OP 27 /* Out of Paper */ +#define ERR_WH 29 /* WHILE without WEND */ +#define ERR_WE 30 /* WEND without WHILE */ + +/* Disk errors (starting at 50) */ +#define ERR_FO 50 /* FIELD overflow */ +#define ERR_IE 51 /* Internal error */ +#define ERR_BN 52 /* Bad file number */ +#define ERR_FF 53 /* File not found */ +#define ERR_BM 54 /* Bad file mode */ +#define ERR_AO 56 /* File already open */ +#define ERR_IO 58 /* Device I/O Error */ +#define ERR_FE 60 /* File already exists */ +#define ERR_FL 62 /* Disk full */ +#define ERR_EF 63 /* Input past end */ +#define ERR_RN 64 /* Bad record number */ +#define ERR_NM 65 /* Bad file name */ +#define ERR_DS 67 /* Direct statement in file */ +#define ERR_TF 68 /* Too many files */ + +const char *gw_error_msg(int errnum); +void gw_error(int errnum); + +/* Error recovery jump target - set in main loop */ +extern jmp_buf gw_error_jmp; +extern int gw_errno; + +#endif diff --git a/include/eval.h b/include/eval.h new file mode 100644 index 0000000..044a621 --- /dev/null +++ b/include/eval.h @@ -0,0 +1,21 @@ +#ifndef GW_EVAL_H +#define GW_EVAL_H + +#include "types.h" + +/* Expression evaluator - recursive descent with precedence climbing */ +gw_value_t gw_eval(void); /* evaluate any expression */ +gw_value_t gw_eval_num(void); /* evaluate, require numeric */ +gw_value_t gw_eval_str(void); /* evaluate, require string */ +int16_t gw_eval_int(void); /* evaluate, require integer result */ +uint16_t gw_eval_uint16(void); + +/* Force type conversions */ +int16_t gw_to_int(gw_value_t *v); +float gw_to_sng(gw_value_t *v); +double gw_to_dbl(gw_value_t *v); + +/* Check for closing paren */ +void gw_expect_rparen(void); + +#endif diff --git a/include/gw_math.h b/include/gw_math.h new file mode 100644 index 0000000..a85488e --- /dev/null +++ b/include/gw_math.h @@ -0,0 +1,54 @@ +#ifndef GW_MATH_H +#define GW_MATH_H + +#include "types.h" + +/* Integer arithmetic with overflow detection */ +int16_t gw_int_add(int16_t a, int16_t b); +int16_t gw_int_sub(int16_t a, int16_t b); +int16_t gw_int_mul(int16_t a, int16_t b); +int16_t gw_int_div(int16_t a, int16_t b); +int16_t gw_int_mod(int16_t a, int16_t b); +int16_t gw_int_idiv(int16_t a, int16_t b); +int16_t gw_int_neg(int16_t a); + +/* Float operations */ +double gw_fadd(double a, double b); +double gw_fsub(double a, double b); +double gw_fmul(double a, double b); +double gw_fdiv(double a, double b); +double gw_fpow(double base, double exp); +double gw_fneg(double a); + +/* Type promotion for binary ops */ +void gw_promote(gw_value_t *a, gw_value_t *b); + +/* Transcendental functions */ +double gw_sin(double x); +double gw_cos(double x); +double gw_tan(double x); +double gw_atn(double x); +double gw_sqr(double x); +double gw_log(double x); +double gw_exp(double x); +double gw_abs(double x); +double gw_sgn(double x); +double gw_int_fn(double x); +double gw_fix(double x); +double gw_rnd(double x); + +/* Conversion functions */ +int16_t gw_cint(double x); +float gw_csng(double x); +double gw_cdbl(gw_value_t *v); + +/* MBF conversion (for file I/O compatibility) */ +float gw_mbf_to_ieee_single(mbf_single_t mbf); +double gw_mbf_to_ieee_double(mbf_double_t mbf); +mbf_single_t gw_ieee_to_mbf_single(float f); +mbf_double_t gw_ieee_to_mbf_double(double d); + +/* Number formatting for PRINT */ +void gw_format_number(gw_value_t *v, char *buf, int bufsize); + +#endif diff --git a/include/gwbasic.h b/include/gwbasic.h new file mode 100644 index 0000000..0938048 --- /dev/null +++ b/include/gwbasic.h @@ -0,0 +1,25 @@ +#ifndef GWBASIC_H +#define GWBASIC_H + +#include "types.h" +#include "tokens.h" +#include "error.h" +#include "hal.h" +#include "interp.h" +#include "eval.h" +#include "gw_math.h" +#include "strings.h" + +#define GW_VERSION "0.1.0" +#define GW_BANNER "GW-BASIC " GW_VERSION + +/* Tokenizer */ +int gw_crunch(const char *text, uint8_t *out, int outsize); +void gw_list_line(uint8_t *tokens, int len, char *out, int outsize); + +/* PRINT statement */ +void gw_stmt_print(void); +void gw_print_value(gw_value_t *v); +void gw_print_newline(void); + +#endif diff --git a/include/hal.h b/include/hal.h new file mode 100644 index 0000000..dd3b638 --- /dev/null +++ b/include/hal.h @@ -0,0 +1,34 @@ +#ifndef GW_HAL_H +#define GW_HAL_H + +#include +#include + +/* Hardware Abstraction Layer vtable */ +typedef struct hal_ops { + /* Terminal I/O */ + void (*putch)(int ch); + void (*puts)(const char *s); + int (*getch)(void); /* blocking read */ + bool (*kbhit)(void); /* key available? */ + void (*locate)(int row, int col); + int (*get_cursor_row)(void); + int (*get_cursor_col)(void); + void (*cls)(void); + void (*set_width)(int cols); + + /* Terminal properties */ + int screen_width; + int screen_height; + + /* Lifecycle */ + void (*init)(void); + void (*shutdown)(void); +} hal_ops_t; + +extern hal_ops_t *gw_hal; + +/* Platform implementations */ +hal_ops_t *hal_posix_create(void); + +#endif diff --git a/include/interp.h b/include/interp.h new file mode 100644 index 0000000..393760b --- /dev/null +++ b/include/interp.h @@ -0,0 +1,51 @@ +#ifndef GW_INTERP_H +#define GW_INTERP_H + +#include "types.h" +#include + +/* Interpreter state - the global context */ +typedef struct { + /* Text pointer (TXTPTR equivalent) */ + uint8_t *text_ptr; + + /* Current accumulator (FAC equivalent) */ + gw_value_t fac; + + /* Program state */ + uint16_t cur_line_num; /* CURLIN - 65535 = direct mode */ + program_line_t *prog_head; /* first line of program */ + bool trace_on; /* TRON/TROFF */ + + /* Tokenizer buffers */ + uint8_t kbuf[300]; /* crunch buffer */ + char buf[256]; /* input line buffer */ + + /* Default variable types for A-Z (DEFTBL) */ + gw_valtype_t def_type[26]; + + /* DATA pointer */ + uint8_t *data_ptr; + uint16_t data_line; +} interp_state_t; + +extern interp_state_t gw; + +/* Direct mode line number sentinel */ +#define LINE_DIRECT 65535 + +void gw_init(void); +void gw_exec_line(uint8_t *tokens); +void gw_exec_direct(const char *line); + +/* CHRGET/CHRGOT - advance/peek the text pointer */ +uint8_t gw_chrget(void); +uint8_t gw_chrgot(void); +void gw_skip_spaces(void); +bool gw_is_letter(uint8_t ch); +bool gw_is_digit(uint8_t ch); + +/* Expect a specific token, else syntax error */ +void gw_expect(uint8_t tok); + +#endif diff --git a/include/strings.h b/include/strings.h new file mode 100644 index 0000000..d9ddc8b --- /dev/null +++ b/include/strings.h @@ -0,0 +1,32 @@ +#ifndef GW_STRINGS_H +#define GW_STRINGS_H + +#include "types.h" +#include + +/* String creation/management */ +gw_string_t gw_str_alloc(int len); +gw_string_t gw_str_from_cstr(const char *s); +gw_string_t gw_str_copy(gw_string_t *s); +void gw_str_free(gw_string_t *s); +char *gw_str_to_cstr(gw_string_t *s); /* caller must free */ + +/* String functions */ +gw_value_t gw_fn_len(gw_value_t *s); +gw_value_t gw_fn_left(gw_value_t *s, int n); +gw_value_t gw_fn_right(gw_value_t *s, int n); +gw_value_t gw_fn_mid(gw_value_t *s, int start, int len); +gw_value_t gw_fn_chr(int code); +gw_value_t gw_fn_asc(gw_value_t *s); +gw_value_t gw_fn_val(gw_value_t *s); +gw_value_t gw_fn_str(gw_value_t *v); +gw_value_t gw_fn_space(int n); +gw_value_t gw_fn_strings(int n, int code); +gw_value_t gw_fn_instr(int start, gw_value_t *haystack, gw_value_t *needle); +gw_value_t gw_fn_hex(int16_t n); +gw_value_t gw_fn_oct(int16_t n); + +/* Concatenation */ +gw_value_t gw_str_concat(gw_value_t *a, gw_value_t *b); + +#endif diff --git a/include/tokens.h b/include/tokens.h new file mode 100644 index 0000000..ce90e5d --- /dev/null +++ b/include/tokens.h @@ -0,0 +1,231 @@ +#ifndef GW_TOKENS_H +#define GW_TOKENS_H + +#include + +/* + * Token values derived from IBMRES.ASM. + * Single-byte tokens: 0x80-0xF9 + * Two-byte tokens: 0xFD xx (extended functions), 0xFE xx (extended stmts), + * 0xFF xx (built-in functions) + */ + +/* Statement tokens (STMDSP) - starting at 128 */ +#define TOK_END 0x80 /* 128 */ +#define TOK_FOR 0x81 /* 129 */ +#define TOK_NEXT 0x82 /* 130 */ +#define TOK_DATA 0x83 /* 131 */ +#define TOK_INPUT 0x84 /* 132 */ +#define TOK_DIM 0x85 /* 133 */ +#define TOK_READ 0x86 /* 134 */ +#define TOK_LET 0x87 /* 135 */ +#define TOK_GOTO 0x88 /* 136 */ +#define TOK_RUN 0x89 /* 137 */ +#define TOK_IF 0x8A /* 138 */ +#define TOK_RESTORE 0x8B /* 139 */ +#define TOK_GOSUB 0x8C /* 140 */ +#define TOK_RETURN 0x8D /* 141 */ +#define TOK_REM 0x8E /* 142 */ +#define TOK_STOP 0x8F /* 143 */ +#define TOK_PRINT 0x90 /* 144 */ +#define TOK_CLEAR 0x91 /* 145 */ +#define TOK_LIST 0x92 /* 146 */ +#define TOK_NEW 0x93 /* 147 */ +#define TOK_ON 0x94 /* 148 */ +#define TOK_WAIT 0x95 /* 149 */ +#define TOK_DEF 0x96 /* 150 */ +#define TOK_POKE 0x97 /* 151 */ +#define TOK_CONT 0x98 /* 152 */ +/* 153-154: padding (SNERR) */ +#define TOK_OUT 0x9B /* 155 */ +#define TOK_LPRINT 0x9C /* 156 */ +#define TOK_LLIST 0x9D /* 157 */ +/* 158: padding */ +#define TOK_WIDTH 0x9F /* 159 */ +#define TOK_ELSE 0xA0 /* 160 */ +#define TOK_TRON 0xA1 /* 161 */ +#define TOK_TROFF 0xA2 /* 162 */ +#define TOK_SWAP 0xA3 /* 163 */ +#define TOK_ERASE 0xA4 /* 164 */ +#define TOK_EDIT 0xA5 /* 165 */ +#define TOK_ERROR 0xA6 /* 166 */ +#define TOK_RESUME 0xA7 /* 167 */ +#define TOK_DELETE 0xA8 /* 168 */ +#define TOK_AUTO 0xA9 /* 169 */ +#define TOK_RENUM 0xAA /* 170 */ +#define TOK_DEFSTR 0xAB /* 171 */ +#define TOK_DEFINT 0xAC /* 172 */ +#define TOK_DEFSNG 0xAD /* 173 */ +#define TOK_DEFDBL 0xAE /* 174 */ +#define TOK_LINE 0xAF /* 175 */ +#define TOK_WHILE 0xB0 /* 176 */ +#define TOK_WEND 0xB1 /* 177 */ +#define TOK_CALL 0xB2 /* 178 */ +/* 179-181: padding */ +#define TOK_WRITE 0xB6 /* 182 */ +#define TOK_OPTION 0xB7 /* 183 */ +#define TOK_RANDOMIZE 0xB8 /* 184 */ +#define TOK_OPEN 0xB9 /* 185 */ +#define TOK_CLOSE 0xBA /* 186 */ +#define TOK_LOAD 0xBB /* 187 */ +#define TOK_MERGE 0xBC /* 188 */ +#define TOK_SAVE 0xBD /* 189 */ +#define TOK_COLOR 0xBE /* 190 */ +#define TOK_CLS 0xBF /* 191 */ +#define TOK_MOTOR 0xC0 /* 192 */ +#define TOK_BSAVE 0xC1 /* 193 */ +#define TOK_BLOAD 0xC2 /* 194 */ +#define TOK_SOUND 0xC3 /* 195 */ +#define TOK_BEEP 0xC4 /* 196 */ +#define TOK_PSET 0xC5 /* 197 */ +#define TOK_PRESET 0xC6 /* 198 */ +#define TOK_SCREEN 0xC7 /* 199 */ +#define TOK_KEY 0xC8 /* 200 */ +#define TOK_LOCATE 0xC9 /* 201 */ + +/* Non-statement keyword tokens */ +#define TOK_TO 0xCA /* 202 */ +#define TOK_THEN 0xCB /* 203 */ +#define TOK_TAB 0xCC /* 204 */ +#define TOK_STEP 0xCD /* 205 */ +#define TOK_USR 0xCE /* 206 */ +#define TOK_FN 0xCF /* 207 */ +#define TOK_SPC 0xD0 /* 208 */ +#define TOK_NOT 0xD1 /* 209 */ +#define TOK_ERL 0xD2 /* 210 */ +#define TOK_ERR 0xD3 /* 211 */ +#define TOK_STRINGS 0xD4 /* 212 - STRING$ */ +#define TOK_USING 0xD5 /* 213 */ +#define TOK_INSTR 0xD6 /* 214 */ +#define TOK_SQUOTE 0xD7 /* 215 - ' (single quote = REM) */ +#define TOK_VARPTR 0xD8 /* 216 */ +#define TOK_CSRLIN 0xD9 /* 217 */ +#define TOK_POINT 0xDA /* 218 */ +#define TOK_OFF 0xDB /* 219 */ +#define TOK_INKEYS 0xDC /* 220 - INKEY$ */ + +/* Operator tokens (after 7-byte gap at 221-227) */ +#define TOK_GT 0xE4 /* 228 - > */ +#define TOK_EQ 0xE5 /* 229 - = */ +#define TOK_LT 0xE6 /* 230 - < */ +#define TOK_PLUS 0xE7 /* 231 - + */ +#define TOK_MINUS 0xE8 /* 232 - - */ +#define TOK_MUL 0xE9 /* 233 - * */ +#define TOK_DIV 0xEA /* 234 - / */ +#define TOK_POW 0xEB /* 235 - ^ */ +#define TOK_AND 0xEC /* 236 */ +#define TOK_OR 0xED /* 237 */ +#define TOK_XOR 0xEE /* 238 */ +#define TOK_EQV 0xEF /* 239 */ +#define TOK_IMP 0xF0 /* 240 */ +#define TOK_MOD 0xF1 /* 241 */ +#define TOK_IDIV 0xF2 /* 242 - \ (integer division) */ + +/* Multi-byte token prefixes */ +#define TOK_PREFIX_FD 0xFD /* extended functions */ +#define TOK_PREFIX_FE 0xFE /* extended statements */ +#define TOK_PREFIX_FF 0xFF /* built-in functions */ + +/* Built-in function tokens (prefix 0xFF) */ +#define FUNC_LEFT 0x80 /* 128 */ +#define FUNC_RIGHT 0x81 /* 129 */ +#define FUNC_MID 0x82 /* 130 */ +#define FUNC_SGN 0x83 /* 131 */ +#define FUNC_INT 0x84 /* 132 */ +#define FUNC_ABS 0x85 /* 133 */ +#define FUNC_SQR 0x86 /* 134 */ +#define FUNC_RND 0x87 /* 135 */ +#define FUNC_SIN 0x88 /* 136 */ +#define FUNC_LOG 0x89 /* 137 */ +#define FUNC_EXP 0x8A /* 138 */ +#define FUNC_COS 0x8B /* 139 */ +#define FUNC_TAN 0x8C /* 140 */ +#define FUNC_ATN 0x8D /* 141 */ +#define FUNC_FRE 0x8E /* 142 */ +#define FUNC_INP 0x8F /* 143 */ +#define FUNC_POS 0x90 /* 144 */ +#define FUNC_LEN 0x91 /* 145 */ +#define FUNC_STR 0x92 /* 146 */ +#define FUNC_VAL 0x93 /* 147 */ +#define FUNC_ASC 0x94 /* 148 */ +#define FUNC_CHR 0x95 /* 149 */ +#define FUNC_PEEK 0x96 /* 150 */ +#define FUNC_SPACE 0x97 /* 151 */ +#define FUNC_OCT 0x98 /* 152 */ +#define FUNC_HEX 0x99 /* 153 */ +#define FUNC_LPOS 0x9A /* 154 */ +#define FUNC_CINT 0x9B /* 155 */ +#define FUNC_CSNG 0x9C /* 156 */ +#define FUNC_CDBL 0x9D /* 157 */ +#define FUNC_FIX 0x9E /* 158 */ +#define FUNC_PEN 0x9F /* 159 */ +#define FUNC_STICK 0xA0 /* 160 */ +#define FUNC_STRIG 0xA1 /* 161 */ +#define FUNC_EOF 0xA2 /* 162 */ +#define FUNC_LOC 0xA3 /* 163 */ +#define FUNC_LOF 0xA4 /* 164 */ + +/* Extended statement tokens (prefix 0xFE) */ +#define XSTMT_FILES 0x80 +#define XSTMT_FIELD 0x81 +#define XSTMT_SYSTEM 0x82 +#define XSTMT_NAME 0x83 +#define XSTMT_LSET 0x84 +#define XSTMT_RSET 0x85 +#define XSTMT_KILL 0x86 +#define XSTMT_PUT 0x87 +#define XSTMT_GET 0x88 +#define XSTMT_RESET 0x89 +#define XSTMT_COMMON 0x8A +#define XSTMT_CHAIN 0x8B +#define XSTMT_DATE 0x8C +#define XSTMT_TIME 0x8D +#define XSTMT_PAINT 0x8E +#define XSTMT_COM 0x8F +#define XSTMT_CIRCLE 0x90 +#define XSTMT_DRAW 0x91 +#define XSTMT_PLAY 0x92 +#define XSTMT_TIMER 0x93 +#define XSTMT_ERDEV 0x94 +#define XSTMT_IOCTL 0x95 +#define XSTMT_CHDIR 0x96 +#define XSTMT_MKDIR 0x97 +#define XSTMT_RMDIR 0x98 +#define XSTMT_SHELL 0x99 +#define XSTMT_ENVIRON 0x9A +#define XSTMT_VIEW 0x9B +#define XSTMT_WINDOW 0x9C +#define XSTMT_PMAP 0x9D +#define XSTMT_PALETTE 0x9E +#define XSTMT_LCOPY 0x9F +#define XSTMT_CALLS 0xA0 + +/* Extended function tokens (prefix 0xFD) */ +#define XFUNC_CVI 0x80 +#define XFUNC_CVS 0x81 +#define XFUNC_CVD 0x82 +#define XFUNC_MKI 0x83 +#define XFUNC_MKS 0x84 +#define XFUNC_MKD 0x85 + +/* Special token constants - embedded constant prefixes */ +#define TOK_COLON ':' /* statement separator (not tokenized) */ +#define TOK_INT2 0x0E /* 2-byte integer (int16_t follows) */ +#define TOK_INT1 0x0F /* 1-byte integer 0-255 (uint8_t follows) */ +/* 0x11-0x1A: literal integers 0-9 (no data bytes) */ +#define TOK_CONST_SNG 0x1C /* single-precision (4 bytes IEEE follow) */ +#define TOK_CONST_DBL 0x1F /* double-precision (8 bytes IEEE follow) */ + +/* Keyword table entry */ +typedef struct { + const char *name; + uint8_t token; /* token value (or second byte for multi-byte) */ + uint8_t prefix; /* 0 for single-byte, 0xFD/0xFE/0xFF for multi */ +} keyword_entry_t; + +extern const keyword_entry_t gw_keywords[]; +extern int gw_keyword_count; + +const char *gw_token_name(uint8_t prefix, uint8_t token); + +#endif diff --git a/include/types.h b/include/types.h new file mode 100644 index 0000000..a2fe04e --- /dev/null +++ b/include/types.h @@ -0,0 +1,58 @@ +#ifndef GW_TYPES_H +#define GW_TYPES_H + +#include +#include + +/* Value types matching GW-BASIC's VALTYP */ +typedef enum { + VT_INT = 2, /* 16-bit integer (%) */ + VT_SNG = 4, /* single precision (!) */ + VT_DBL = 8, /* double precision (#) */ + VT_STR = 3, /* string ($) */ +} gw_valtype_t; + +/* String descriptor - matches original's 3-byte layout concept */ +typedef struct { + uint8_t len; + char *data; /* malloc'd, NOT null-terminated */ +} gw_string_t; + +/* Unified value - the FAC (floating accumulator) equivalent */ +typedef struct { + gw_valtype_t type; + union { + int16_t ival; + float fval; + double dval; + gw_string_t sval; + }; +} gw_value_t; + +/* Program line stored in memory */ +typedef struct program_line { + struct program_line *next; + uint16_t num; /* line number 0-65529 */ + uint16_t len; /* token data length */ + uint8_t *tokens; /* tokenized line data */ +} program_line_t; + +/* Variable entry */ +typedef struct { + char name[2]; /* 2-char name (original GW-BASIC limit) */ + gw_valtype_t type; + gw_value_t val; +} var_entry_t; + +/* MBF (Microsoft Binary Format) types for file compatibility */ +typedef struct { + uint8_t mantissa[3]; + uint8_t exponent; +} mbf_single_t; + +typedef struct { + uint8_t mantissa[7]; + uint8_t exponent; +} mbf_double_t; + +#endif diff --git a/platform/hal_posix.c b/platform/hal_posix.c new file mode 100644 index 0000000..d1c7bc7 --- /dev/null +++ b/platform/hal_posix.c @@ -0,0 +1,117 @@ +#include "hal.h" +#include +#include +#include +#include +#include +#include + +static struct termios orig_termios; +static int raw_mode = 0; +static int cursor_row = 0; +static int cursor_col = 0; + +static void posix_putch(int ch) +{ + putchar(ch); + if (ch == '\n') { + cursor_row++; + cursor_col = 0; + } else if (ch == '\r') { + cursor_col = 0; + } else { + cursor_col++; + } +} + +static void posix_puts(const char *s) +{ + while (*s) + posix_putch(*s++); +} + +static int posix_getch(void) +{ + fflush(stdout); + return getchar(); +} + +static bool posix_kbhit(void) +{ + /* In cooked mode, always return false */ + if (!raw_mode) + return false; + + int ch = getchar(); + if (ch != EOF) { + ungetc(ch, stdin); + return true; + } + return false; +} + +static void posix_locate(int row, int col) +{ + printf("\033[%d;%dH", row, col); + fflush(stdout); + cursor_row = row - 1; + cursor_col = col - 1; +} + +static int posix_get_cursor_row(void) { return cursor_row; } +static int posix_get_cursor_col(void) { return cursor_col; } + +static void posix_cls(void) +{ + printf("\033[2J\033[H"); + fflush(stdout); + cursor_row = 0; + cursor_col = 0; +} + +static void posix_set_width(int cols) +{ + /* Not much we can do on POSIX without ncurses */ + (void)cols; +} + +static void posix_init(void) +{ + /* Disable output buffering for interactive use */ + setbuf(stdout, NULL); +} + +static void posix_shutdown(void) +{ + if (raw_mode) { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); + raw_mode = 0; + } +} + +static hal_ops_t posix_hal = { + .putch = posix_putch, + .puts = posix_puts, + .getch = posix_getch, + .kbhit = posix_kbhit, + .locate = posix_locate, + .get_cursor_row = posix_get_cursor_row, + .get_cursor_col = posix_get_cursor_col, + .cls = posix_cls, + .set_width = posix_set_width, + .screen_width = 80, + .screen_height = 25, + .init = posix_init, + .shutdown = posix_shutdown, +}; + +hal_ops_t *hal_posix_create(void) +{ + /* Try to detect terminal size */ + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { + if (ws.ws_col > 0) posix_hal.screen_width = ws.ws_col; + if (ws.ws_row > 0) posix_hal.screen_height = ws.ws_row; + } + return &posix_hal; +} diff --git a/src/error.c b/src/error.c new file mode 100644 index 0000000..37da1b2 --- /dev/null +++ b/src/error.c @@ -0,0 +1,83 @@ +#include "error.h" +#include "hal.h" +#include "interp.h" +#include +#include + +jmp_buf gw_error_jmp; +int gw_errno = 0; + +static const struct { int num; const char *msg; } error_table[] = { + { 1, "NEXT without FOR" }, + { 2, "Syntax error" }, + { 3, "RETURN without GOSUB" }, + { 4, "Out of DATA" }, + { 5, "Illegal function call" }, + { 6, "Overflow" }, + { 7, "Out of memory" }, + { 8, "Undefined line number" }, + { 9, "Subscript out of range" }, + { 10, "Duplicate Definition" }, + { 11, "Division by zero" }, + { 12, "Illegal direct" }, + { 13, "Type mismatch" }, + { 14, "Out of string space" }, + { 15, "String too long" }, + { 16, "String formula too complex" }, + { 17, "Can't continue" }, + { 18, "Undefined user function" }, + { 19, "No RESUME" }, + { 20, "RESUME without error" }, + { 21, "Unprintable error" }, + { 22, "Missing operand" }, + { 23, "Line buffer overflow" }, + { 24, "Device Timeout" }, + { 25, "Device Fault" }, + { 26, "FOR without NEXT" }, + { 27, "Out of Paper" }, + { 29, "WHILE without WEND" }, + { 30, "WEND without WHILE" }, + { 50, "FIELD overflow" }, + { 51, "Internal error" }, + { 52, "Bad file number" }, + { 53, "File not found" }, + { 54, "Bad file mode" }, + { 56, "File already open" }, + { 58, "Device I/O Error" }, + { 60, "File already exists" }, + { 62, "Disk full" }, + { 63, "Input past end" }, + { 64, "Bad record number" }, + { 65, "Bad file name" }, + { 67, "Direct statement in file" }, + { 68, "Too many files" }, + { 0, NULL } +}; + +const char *gw_error_msg(int errnum) +{ + for (int i = 0; error_table[i].msg; i++) { + if (error_table[i].num == errnum) + return error_table[i].msg; + } + return "Unprintable error"; +} + +void gw_error(int errnum) +{ + gw_errno = errnum; + const char *msg = gw_error_msg(errnum); + char buf[80]; + + if (gw.cur_line_num != LINE_DIRECT) + snprintf(buf, sizeof(buf), "\n%s in %u\n", msg, gw.cur_line_num); + else + snprintf(buf, sizeof(buf), "\n%s\n", msg); + + if (gw_hal) + gw_hal->puts(buf); + else + fputs(buf, stderr); + + longjmp(gw_error_jmp, errnum); +} diff --git a/src/eval.c b/src/eval.c new file mode 100644 index 0000000..b005993 --- /dev/null +++ b/src/eval.c @@ -0,0 +1,900 @@ +#include "gwbasic.h" +#include +#include +#include +#include + +/* + * Expression evaluator - reimplements GWEVAL.ASM's FRMEVL. + * Uses recursive descent with precedence climbing. + * + * Operator precedence (from GWDATA.ASM): + * ^ 127 + * * / 124 + * \ 123 + * MOD 122 + * + - 121 + * relational 64 + * NOT 90 + * AND 70 + * OR 60 + * XOR 50 + * EQV 40 + * IMP 40 + */ + +/* Forward declarations */ +static gw_value_t eval_expr(int min_prec); +static gw_value_t eval_unary(void); +static gw_value_t eval_atom(void); +static gw_value_t eval_function(uint8_t prefix, uint8_t func_tok); +static gw_value_t eval_paren(void); +static gw_value_t eval_number(void); +static gw_value_t eval_string_literal(void); + +/* Operator precedence lookup */ +static int op_prec(uint8_t tok) +{ + switch (tok) { + case TOK_IMP: return 40; + case TOK_EQV: return 40; + case TOK_XOR: return 50; + case TOK_OR: return 60; + case TOK_AND: return 70; + 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; + } +} + +/* Apply a binary operator */ +static gw_value_t apply_binop(uint8_t op, gw_value_t left, gw_value_t right) +{ + gw_value_t result; + + /* String concatenation */ + if (op == TOK_PLUS && left.type == VT_STR && right.type == VT_STR) + return gw_str_concat(&left, &right); + + /* Relational operators can compare strings */ + if ((op == TOK_GT || op == TOK_EQ || op == TOK_LT) + && left.type == VT_STR && right.type == VT_STR) { + char *ls = gw_str_to_cstr(&left.sval); + char *rs = gw_str_to_cstr(&right.sval); + int cmp = strcmp(ls, rs); + free(ls); free(rs); + gw_str_free(&left.sval); + gw_str_free(&right.sval); + result.type = VT_INT; + switch (op) { + case TOK_GT: result.ival = cmp > 0 ? -1 : 0; break; + case TOK_EQ: result.ival = cmp == 0 ? -1 : 0; break; + case TOK_LT: result.ival = cmp < 0 ? -1 : 0; break; + } + return result; + } + + /* Type mismatch if mixed string/numeric */ + if (left.type == VT_STR || right.type == VT_STR) + gw_error(ERR_TM); + + /* Logical/bitwise operators: force to integer */ + if (op == TOK_AND || op == TOK_OR || op == TOK_XOR + || op == TOK_EQV || op == TOK_IMP) { + int16_t a = gw_to_int(&left); + int16_t b = gw_to_int(&right); + result.type = VT_INT; + switch (op) { + case TOK_AND: result.ival = a & b; break; + case TOK_OR: result.ival = a | b; break; + case TOK_XOR: result.ival = a ^ b; break; + case TOK_EQV: result.ival = ~(a ^ b); break; + case TOK_IMP: result.ival = (~a) | b; break; + } + return result; + } + + /* Integer division and MOD: force to integer */ + if (op == TOK_IDIV || op == TOK_MOD) { + int16_t a = gw_to_int(&left); + int16_t b = gw_to_int(&right); + if (b == 0) gw_error(ERR_DZ); + result.type = VT_INT; + if (op == TOK_IDIV) + result.ival = a / b; + else + result.ival = a % b; + return result; + } + + /* Promote to common numeric type */ + gw_promote(&left, &right); + + /* Integer arithmetic */ + if (left.type == VT_INT) { + result.type = VT_INT; + switch (op) { + case TOK_PLUS: result.ival = gw_int_add(left.ival, right.ival); break; + case TOK_MINUS: result.ival = gw_int_sub(left.ival, right.ival); break; + case TOK_MUL: result.ival = gw_int_mul(left.ival, right.ival); break; + case TOK_DIV: + /* Integer / integer -> single in GW-BASIC */ + if (right.ival == 0) gw_error(ERR_DZ); + result.type = VT_SNG; + result.fval = (float)left.ival / (float)right.ival; + break; + case TOK_POW: + result.type = VT_SNG; + result.fval = (float)gw_fpow(left.ival, right.ival); + break; + case TOK_GT: result.ival = left.ival > right.ival ? -1 : 0; break; + case TOK_EQ: result.ival = left.ival == right.ival ? -1 : 0; break; + case TOK_LT: result.ival = left.ival < right.ival ? -1 : 0; break; + default: gw_error(ERR_SN); + } + return result; + } + + /* Float/double arithmetic */ + double a = (left.type == VT_SNG) ? left.fval : left.dval; + double b = (right.type == VT_SNG) ? right.fval : right.dval; + double r; + int is_relational = 0; + + switch (op) { + case TOK_PLUS: r = gw_fadd(a, b); break; + case TOK_MINUS: r = gw_fsub(a, b); break; + case TOK_MUL: r = gw_fmul(a, b); break; + case TOK_DIV: r = gw_fdiv(a, b); break; + case TOK_POW: r = gw_fpow(a, b); break; + case TOK_GT: result.type = VT_INT; result.ival = a > b ? -1 : 0; is_relational = 1; break; + case TOK_EQ: result.type = VT_INT; result.ival = a == b ? -1 : 0; is_relational = 1; break; + case TOK_LT: result.type = VT_INT; result.ival = a < b ? -1 : 0; is_relational = 1; break; + default: gw_error(ERR_SN); r = 0; + } + + if (!is_relational) { + if (left.type == VT_DBL || right.type == VT_DBL) { + result.type = VT_DBL; + result.dval = r; + } else { + result.type = VT_SNG; + result.fval = (float)r; + } + } + return result; +} + +/* Combined relational: handle >=, <=, <> as combined tokens */ +static int try_combined_relational(uint8_t first, gw_value_t left, gw_value_t *result) +{ + uint8_t *save = gw.text_ptr; + gw_skip_spaces(); + uint8_t next = gw_chrgot(); + + if (first == TOK_LT && next == TOK_GT) { + /* <> (not equal) */ + gw.text_ptr++; /* consume '>' */ + gw_value_t right = eval_expr(65); + *result = apply_binop(TOK_EQ, left, right); + result->ival = ~result->ival; + return 1; + } + if (first == TOK_LT && next == TOK_EQ) { + /* <= */ + gw.text_ptr++; + gw_value_t right = eval_expr(65); + gw_value_t gt_result = apply_binop(TOK_GT, left, right); + result->type = VT_INT; + result->ival = ~gt_result.ival; + return 1; + } + if (first == TOK_GT && next == TOK_EQ) { + /* >= */ + gw.text_ptr++; + gw_value_t right = eval_expr(65); + gw_value_t lt_result = apply_binop(TOK_LT, left, right); + result->type = VT_INT; + result->ival = ~lt_result.ival; + return 1; + } + if (first == TOK_EQ && (next == TOK_LT || next == TOK_GT)) { + /* =< or => (unusual but valid) */ + gw.text_ptr++; + gw_value_t right = eval_expr(65); + if (next == TOK_LT) { + gw_value_t gt_result = apply_binop(TOK_GT, left, right); + result->type = VT_INT; + result->ival = ~gt_result.ival; + } else { + gw_value_t lt_result = apply_binop(TOK_LT, left, right); + result->type = VT_INT; + result->ival = ~lt_result.ival; + } + return 1; + } + + /* Not a combined relational, put back */ + gw.text_ptr = save; + return 0; +} + +/* Main expression evaluator entry point */ +static gw_value_t eval_expr(int min_prec) +{ + gw_value_t left = eval_unary(); + + for (;;) { + gw_skip_spaces(); + uint8_t tok = gw_chrgot(); + + int prec = op_prec(tok); + if (prec < min_prec) + break; + + gw_chrget(); /* consume operator */ + + /* Handle combined relationals like >=, <=, <> */ + if (tok == TOK_GT || tok == TOK_LT || tok == TOK_EQ) { + gw_value_t combined; + if (try_combined_relational(tok, left, &combined)) { + left = combined; + continue; + } + } + + gw_value_t right = eval_expr(prec + 1); + left = apply_binop(tok, left, right); + } + + return left; +} + +/* Unary operators: NOT, unary +, unary - */ +static gw_value_t eval_unary(void) +{ + gw_skip_spaces(); + uint8_t tok = gw_chrgot(); + + if (tok == TOK_MINUS) { + gw_chrget(); + gw_value_t v = eval_unary(); + if (v.type == VT_STR) gw_error(ERR_TM); + if (v.type == VT_INT) { + v.ival = gw_int_neg(v.ival); + } else if (v.type == VT_SNG) { + v.fval = -v.fval; + } else { + v.dval = -v.dval; + } + return v; + } + + if (tok == TOK_PLUS) { + gw_chrget(); + gw_value_t v = eval_unary(); + if (v.type == VT_STR) gw_error(ERR_TM); + return v; + } + + if (tok == TOK_NOT) { + gw_chrget(); + gw_value_t v = eval_expr(90); + int16_t i = gw_to_int(&v); + gw_value_t result; + result.type = VT_INT; + result.ival = ~i; + return result; + } + + return eval_atom(); +} + +/* Parse a parenthesized expression */ +static gw_value_t eval_paren(void) +{ + gw_chrget(); /* skip '(' */ + gw_value_t v = eval_expr(0); + gw_skip_spaces(); + if (gw_chrgot() != ')') + gw_error(ERR_SN); + gw_chrget(); /* skip ')' */ + return v; +} + +/* Parse a numeric literal from token stream */ +static gw_value_t eval_number(void) +{ + gw_value_t v; + uint8_t tok = gw_chrgot(); + + /* Single-byte integer constant 0-9 */ + if (tok >= 0x11 && tok <= 0x1A) { + gw.text_ptr++; + v.type = VT_INT; + v.ival = tok - 0x11; + return v; + } + + /* One-byte integer constant (TOK_INT1 followed by 1 data byte) */ + if (tok == TOK_INT1) { + gw.text_ptr++; + v.type = VT_INT; + v.ival = *gw.text_ptr++; + return v; + } + + /* Two-byte integer constant (TOK_INT2 followed by 2 data bytes) */ + if (tok == TOK_INT2) { + gw.text_ptr++; + uint8_t lo = *gw.text_ptr++; + uint8_t hi = *gw.text_ptr++; + v.type = VT_INT; + v.ival = (int16_t)(lo | (hi << 8)); + return v; + } + + /* Single precision constant (0x1C followed by 4 data bytes) */ + if (tok == TOK_CONST_SNG) { + gw.text_ptr++; + float fval; + memcpy(&fval, gw.text_ptr, 4); + gw.text_ptr += 4; + v.type = VT_SNG; + v.fval = fval; + return v; + } + + /* Double precision constant (0x1F followed by 8 data bytes) */ + if (tok == TOK_CONST_DBL) { + gw.text_ptr++; + double dval; + memcpy(&dval, gw.text_ptr, 8); + gw.text_ptr += 8; + v.type = VT_DBL; + v.dval = dval; + return v; + } + + gw_error(ERR_SN); + v.type = VT_INT; + v.ival = 0; + return v; +} + +/* Parse a string literal */ +static gw_value_t eval_string_literal(void) +{ + /* Advance past the opening quote - don't skip spaces */ + gw.text_ptr++; + uint8_t *start = gw.text_ptr; + int len = 0; + + while (*gw.text_ptr && *gw.text_ptr != '"') { + gw.text_ptr++; + len++; + } + + gw_value_t v; + v.type = VT_STR; + v.sval = gw_str_alloc(len); + memcpy(v.sval.data, start, len); + + if (*gw.text_ptr == '"') + gw.text_ptr++; /* skip closing " - don't use chrget, next token reads normally */ + + return v; +} + +/* Evaluate built-in functions (0xFF prefix) */ +static gw_value_t eval_function(uint8_t prefix, uint8_t func_tok) +{ + gw_value_t v, arg; + int n; + + /* Most functions: FUNCNAME(expr) */ + switch (func_tok) { + /* Numeric -> Numeric functions */ + case FUNC_SGN: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_INT; + v.ival = (int16_t)gw_sgn(gw_to_dbl(&arg)); + return v; + + case FUNC_INT: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = (arg.type == VT_DBL) ? VT_DBL : VT_SNG; + if (v.type == VT_DBL) v.dval = gw_int_fn(arg.dval); + else v.fval = (float)gw_int_fn(gw_to_dbl(&arg)); + return v; + + case FUNC_ABS: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = arg.type; + if (arg.type == VT_INT) v.ival = arg.ival < 0 ? gw_int_neg(arg.ival) : arg.ival; + else if (arg.type == VT_SNG) v.fval = (float)gw_abs(arg.fval); + else v.dval = gw_abs(arg.dval); + return v; + + case FUNC_SQR: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_DBL; + v.dval = gw_sqr(gw_to_dbl(&arg)); + if (arg.type != VT_DBL) { v.type = VT_SNG; v.fval = (float)v.dval; } + return v; + + case FUNC_SIN: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_DBL; + v.dval = gw_sin(gw_to_dbl(&arg)); + if (arg.type != VT_DBL) { v.type = VT_SNG; v.fval = (float)v.dval; } + return v; + + case FUNC_COS: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_DBL; + v.dval = gw_cos(gw_to_dbl(&arg)); + if (arg.type != VT_DBL) { v.type = VT_SNG; v.fval = (float)v.dval; } + return v; + + case FUNC_TAN: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_DBL; + v.dval = gw_tan(gw_to_dbl(&arg)); + if (arg.type != VT_DBL) { v.type = VT_SNG; v.fval = (float)v.dval; } + return v; + + case FUNC_ATN: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_DBL; + v.dval = gw_atn(gw_to_dbl(&arg)); + if (arg.type != VT_DBL) { v.type = VT_SNG; v.fval = (float)v.dval; } + return v; + + case FUNC_LOG: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_DBL; + v.dval = gw_log(gw_to_dbl(&arg)); + if (arg.type != VT_DBL) { v.type = VT_SNG; v.fval = (float)v.dval; } + return v; + + case FUNC_EXP: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_DBL; + v.dval = gw_exp(gw_to_dbl(&arg)); + if (arg.type != VT_DBL) { v.type = VT_SNG; v.fval = (float)v.dval; } + return v; + + case FUNC_RND: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_SNG; + v.fval = (float)gw_rnd(gw_to_dbl(&arg)); + return v; + + case FUNC_FIX: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = arg.type; + if (arg.type == VT_INT) v.ival = arg.ival; + else if (arg.type == VT_SNG) v.fval = (float)gw_fix(arg.fval); + else v.dval = gw_fix(arg.dval); + return v; + + case FUNC_CINT: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_INT; + v.ival = gw_cint(gw_to_dbl(&arg)); + return v; + + case FUNC_CSNG: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_SNG; + v.fval = gw_csng(gw_to_dbl(&arg)); + return v; + + case FUNC_CDBL: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + v.type = VT_DBL; + v.dval = gw_cdbl(&arg); + return v; + + /* String functions */ + case FUNC_LEN: + gw_expect('('); + arg = gw_eval_str(); + gw_expect_rparen(); + return gw_fn_len(&arg); + + case FUNC_ASC: + gw_expect('('); + arg = gw_eval_str(); + gw_expect_rparen(); + return gw_fn_asc(&arg); + + case FUNC_CHR: + gw_expect('('); + n = gw_eval_int(); + gw_expect_rparen(); + return gw_fn_chr(n); + + case FUNC_VAL: + gw_expect('('); + arg = gw_eval_str(); + gw_expect_rparen(); + return gw_fn_val(&arg); + + case FUNC_STR: + gw_expect('('); + arg = gw_eval_num(); + gw_expect_rparen(); + return gw_fn_str(&arg); + + case FUNC_SPACE: + gw_expect('('); + n = gw_eval_int(); + gw_expect_rparen(); + return gw_fn_space(n); + + case FUNC_LEFT: + { + gw_expect('('); + gw_value_t s = gw_eval_str(); + gw_skip_spaces(); + gw_expect(','); + n = gw_eval_int(); + gw_expect_rparen(); + return gw_fn_left(&s, n); + } + + case FUNC_RIGHT: + { + gw_expect('('); + gw_value_t s = gw_eval_str(); + gw_skip_spaces(); + gw_expect(','); + n = gw_eval_int(); + gw_expect_rparen(); + return gw_fn_right(&s, n); + } + + case FUNC_MID: + { + gw_expect('('); + gw_value_t s = gw_eval_str(); + gw_skip_spaces(); + gw_expect(','); + int start = gw_eval_int(); + int len = -1; + gw_skip_spaces(); + if (gw_chrgot() == ',') { + gw_chrget(); + len = gw_eval_int(); + } + gw_expect_rparen(); + if (len < 0) + len = 255; /* default: rest of string */ + return gw_fn_mid(&s, start, len); + } + + case FUNC_HEX: + gw_expect('('); + n = gw_eval_int(); + gw_expect_rparen(); + return gw_fn_hex(n); + + case FUNC_OCT: + gw_expect('('); + n = gw_eval_int(); + gw_expect_rparen(); + return gw_fn_oct(n); + + case FUNC_FRE: + gw_expect('('); + arg = gw_eval(); /* can be string or numeric */ + gw_expect_rparen(); + if (arg.type == VT_STR) gw_str_free(&arg.sval); + /* Return fake free memory value */ + v.type = VT_SNG; + v.fval = 60000.0f; + return v; + + case FUNC_POS: + gw_expect('('); + gw_eval(); /* dummy argument */ + gw_expect_rparen(); + v.type = VT_INT; + v.ival = gw_hal ? gw_hal->get_cursor_col() + 1 : 1; + return v; + + case FUNC_LPOS: + gw_expect('('); + gw_eval(); + gw_expect_rparen(); + v.type = VT_INT; + v.ival = 1; + return v; + + case FUNC_INP: + case FUNC_PEEK: + case FUNC_PEN: + case FUNC_STICK: + case FUNC_STRIG: + case FUNC_EOF: + case FUNC_LOC: + case FUNC_LOF: + gw_expect('('); + gw_eval(); + gw_expect_rparen(); + v.type = VT_INT; + v.ival = 0; + return v; + + default: + gw_error(ERR_SN); + v.type = VT_INT; + v.ival = 0; + return v; + } +} + +/* Atom: number, string, function, or parenthesized expression */ +static gw_value_t eval_atom(void) +{ + gw_skip_spaces(); + uint8_t tok = gw_chrgot(); + + /* Parenthesized expression */ + if (tok == '(') + return eval_paren(); + + /* String literal */ + if (tok == '"') + return eval_string_literal(); + + /* Numeric constant tokens: 0x0E-0x1F covers all embedded constants */ + if (tok >= TOK_INT2 && tok <= TOK_CONST_DBL) + return eval_number(); + + /* Built-in function (0xFF prefix) */ + if (tok == TOK_PREFIX_FF) { + gw_chrget(); /* consume 0xFF */ + uint8_t func = gw_chrgot(); + gw_chrget(); /* consume function token */ + return eval_function(TOK_PREFIX_FF, func); + } + + /* Extended function (0xFD prefix) */ + if (tok == TOK_PREFIX_FD) { + gw_chrget(); + uint8_t func = gw_chrgot(); + gw_chrget(); + return eval_function(TOK_PREFIX_FD, func); + } + + /* STRING$ function (single-byte token but acts like function) */ + if (tok == TOK_STRINGS) { + gw_chrget(); + gw_expect('('); + int n = gw_eval_int(); + gw_skip_spaces(); + gw_expect(','); + gw_skip_spaces(); + gw_value_t arg2 = gw_eval(); + gw_expect_rparen(); + int code; + if (arg2.type == VT_STR) { + if (arg2.sval.len == 0) gw_error(ERR_FC); + code = (uint8_t)arg2.sval.data[0]; + gw_str_free(&arg2.sval); + } else { + code = gw_to_int(&arg2); + } + return gw_fn_strings(n, code); + } + + /* INSTR function */ + if (tok == TOK_INSTR) { + gw_chrget(); + gw_expect('('); + /* First arg could be start position or string */ + gw_value_t first = gw_eval(); + int start = 1; + gw_value_t haystack, needle; + gw_skip_spaces(); + gw_expect(','); + if (first.type != VT_STR) { + start = gw_to_int(&first); + haystack = gw_eval_str(); + gw_skip_spaces(); + gw_expect(','); + needle = gw_eval_str(); + } else { + haystack = first; + needle = gw_eval_str(); + } + gw_expect_rparen(); + return gw_fn_instr(start, &haystack, &needle); + } + + /* ERL, ERR (error info pseudo-variables) */ + if (tok == TOK_ERL) { + gw_chrget(); + gw_value_t v; + v.type = VT_INT; + v.ival = 0; + return v; + } + if (tok == TOK_ERR) { + gw_chrget(); + gw_value_t v; + v.type = VT_INT; + v.ival = gw_errno; + return v; + } + + /* CSRLIN pseudo-variable */ + if (tok == TOK_CSRLIN) { + gw_chrget(); + gw_value_t v; + v.type = VT_INT; + v.ival = gw_hal ? gw_hal->get_cursor_row() + 1 : 1; + return v; + } + + /* INKEY$ pseudo-variable */ + if (tok == TOK_INKEYS) { + gw_chrget(); + gw_value_t v; + v.type = VT_STR; + if (gw_hal && gw_hal->kbhit()) { + int ch = gw_hal->getch(); + v.sval = gw_str_alloc(1); + v.sval.data[0] = ch; + } else { + v.sval = gw_str_alloc(0); + } + return v; + } + + /* Extended statement tokens that work as functions (DATE$, TIME$) */ + if (tok == TOK_PREFIX_FE) { + uint8_t *save = gw.text_ptr; + gw_chrget(); + uint8_t xtok = gw_chrgot(); + if (xtok == XSTMT_DATE || xtok == XSTMT_TIME) { + gw_chrget(); + gw_value_t v; + v.type = VT_STR; + if (xtok == XSTMT_DATE) { + v.sval = gw_str_from_cstr("01-01-2026"); + } else { + v.sval = gw_str_from_cstr("00:00:00"); + } + return v; + } + gw.text_ptr = save; + } + + /* End of expression */ + if (tok == 0 || tok == ':' || tok == ')' || tok == ',' + || tok == ';' || tok == TOK_THEN || tok == TOK_ELSE + || tok == TOK_TO || tok == TOK_STEP) + gw_error(ERR_MO); + + gw_error(ERR_SN); + gw_value_t dummy = { .type = VT_INT, .ival = 0 }; + return dummy; +} + +/* Public API */ +gw_value_t gw_eval(void) +{ + return eval_expr(0); +} + +gw_value_t gw_eval_num(void) +{ + gw_value_t v = eval_expr(0); + if (v.type == VT_STR) + gw_error(ERR_TM); + return v; +} + +gw_value_t gw_eval_str(void) +{ + gw_value_t v = eval_expr(0); + if (v.type != VT_STR) + gw_error(ERR_TM); + return v; +} + +int16_t gw_eval_int(void) +{ + gw_value_t v = gw_eval_num(); + return gw_to_int(&v); +} + +uint16_t gw_eval_uint16(void) +{ + gw_value_t v = gw_eval_num(); + double d = gw_to_dbl(&v); + if (d < 0 || d > 65535) + gw_error(ERR_FC); + return (uint16_t)d; +} + +void gw_expect_rparen(void) +{ + gw_skip_spaces(); + if (gw_chrgot() != ')') + gw_error(ERR_SN); + gw_chrget(); +} + +/* Type conversion helpers */ +int16_t gw_to_int(gw_value_t *v) +{ + switch (v->type) { + case VT_INT: return v->ival; + case VT_SNG: return gw_cint(v->fval); + case VT_DBL: return gw_cint(v->dval); + default: gw_error(ERR_TM); return 0; + } +} + +float gw_to_sng(gw_value_t *v) +{ + switch (v->type) { + case VT_INT: return (float)v->ival; + case VT_SNG: return v->fval; + case VT_DBL: return (float)v->dval; + default: gw_error(ERR_TM); return 0; + } +} + +double gw_to_dbl(gw_value_t *v) +{ + switch (v->type) { + case VT_INT: return (double)v->ival; + case VT_SNG: return (double)v->fval; + case VT_DBL: return v->dval; + default: gw_error(ERR_TM); return 0; + } +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..cc9cde7 --- /dev/null +++ b/src/main.c @@ -0,0 +1,223 @@ +#include "gwbasic.h" +#include +#include +#include +#include + +/* Global interpreter state */ +interp_state_t gw; +hal_ops_t *gw_hal = NULL; + +void gw_init(void) +{ + 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; +} + +/* CHRGET: advance text pointer, skip spaces, return current byte */ +uint8_t gw_chrget(void) +{ + gw.text_ptr++; + while (*gw.text_ptr == ' ') + gw.text_ptr++; + return *gw.text_ptr; +} + +/* CHRGOT: return current byte without advancing */ +uint8_t gw_chrgot(void) +{ + return *gw.text_ptr; +} + +void gw_skip_spaces(void) +{ + while (*gw.text_ptr == ' ') + gw.text_ptr++; +} + +bool gw_is_letter(uint8_t ch) +{ + return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'); +} + +bool gw_is_digit(uint8_t ch) +{ + return ch >= '0' && ch <= '9'; +} + +void gw_expect(uint8_t tok) +{ + gw_skip_spaces(); + if (gw_chrgot() != tok) + gw_error(ERR_SN); + gw_chrget(); +} + +/* Execute a tokenized line from the text pointer */ +void gw_exec_line(uint8_t *tokens) +{ + gw.text_ptr = tokens; + gw_skip_spaces(); + + uint8_t tok = gw_chrgot(); + + /* Empty line */ + if (tok == 0) + return; + + /* PRINT / ? */ + if (tok == TOK_PRINT || tok == '?') { + gw_chrget(); + gw_stmt_print(); + return; + } + + /* 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); +} + +/* Execute a raw text line (crunch + dispatch) */ +void gw_exec_direct(const char *line) +{ + gw.cur_line_num = LINE_DIRECT; + int len = gw_crunch(line, gw.kbuf, sizeof(gw.kbuf)); + (void)len; + gw_exec_line(gw.kbuf); +} + +/* 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'; + return buf; +} + +static void print_banner(int interactive) +{ + if (!interactive) + return; + const char *banner = + "GW-BASIC " GW_VERSION "\n" + "(C) Eremey Valetov 2026. MIT License.\n" + "Based on Microsoft GW-BASIC assembly source.\n"; + if (gw_hal) + gw_hal->puts(banner); + else + fputs(banner, stdout); +} + +int main(int argc, char **argv) +{ + gw_hal = hal_posix_create(); + gw_hal->init(); + gw_init(); + + 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) { + printf("Usage: gwbasic [options] [file.bas]\n" + "Options:\n" + " -h, --help Show this help\n" + " -v, --version Show version\n"); + return 0; + } + if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) { + printf("GW-BASIC %s\n", GW_VERSION); + return 0; + } + if (argv[i][0] != '-') { + filename = argv[i]; + } + } + + /* TODO: Load and run file if specified */ + (void)filename; + + print_banner(interactive); + + /* Direct mode loop */ + for (;;) { + if (interactive) { + fputs("Ok\n", stdout); + fflush(stdout); + } + + char *line = read_line(); + 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 */ + continue; + } + + gw_exec_direct(line); + } + + if (gw_hal) + gw_hal->shutdown(); + return 0; +} diff --git a/src/math_float.c b/src/math_float.c new file mode 100644 index 0000000..561a3e6 --- /dev/null +++ b/src/math_float.c @@ -0,0 +1,333 @@ +#include "gwbasic.h" +#include +#include +#include +#include + +static void check_overflow(double r) +{ + if (isinf(r) || isnan(r)) + gw_error(ERR_OV); +} + +double gw_fadd(double a, double b) +{ + double r = a + b; + check_overflow(r); + return r; +} + +double gw_fsub(double a, double b) +{ + double r = a - b; + check_overflow(r); + return r; +} + +double gw_fmul(double a, double b) +{ + double r = a * b; + check_overflow(r); + return r; +} + +double gw_fdiv(double a, double b) +{ + if (b == 0.0) + gw_error(ERR_DZ); + double r = a / b; + check_overflow(r); + return r; +} + +double gw_fpow(double base, double exp) +{ + if (base == 0.0 && exp < 0.0) + gw_error(ERR_DZ); + if (base < 0.0 && exp != floor(exp)) + gw_error(ERR_FC); + double r = pow(base, exp); + check_overflow(r); + return r; +} + +double gw_fneg(double a) +{ + return -a; +} + +/* Type promotion: bring both values to the wider type */ +void gw_promote(gw_value_t *a, gw_value_t *b) +{ + if (a->type == VT_STR || b->type == VT_STR) + gw_error(ERR_TM); + + if (a->type == b->type) + return; + + /* Promote to the wider type: INT < SNG < DBL */ + gw_valtype_t target = (a->type > b->type) ? a->type : b->type; + + if (a->type != target) { + if (target == VT_SNG) { + a->fval = gw_to_sng(a); + a->type = VT_SNG; + } else { + a->dval = gw_to_dbl(a); + a->type = VT_DBL; + } + } + if (b->type != target) { + if (target == VT_SNG) { + b->fval = gw_to_sng(b); + b->type = VT_SNG; + } else { + b->dval = gw_to_dbl(b); + b->type = VT_DBL; + } + } +} + +/* CINT: convert to integer with rounding */ +int16_t gw_cint(double x) +{ + if (x > 32767.0 || x < -32768.0) + gw_error(ERR_OV); + /* Banker's rounding like the original */ + double r = rint(x); + return (int16_t)r; +} + +float gw_csng(double x) +{ + float f = (float)x; + if ((isinf(f) || isnan(f)) && !isinf(x) && !isnan(x)) + gw_error(ERR_OV); + return f; +} + +double gw_cdbl(gw_value_t *v) +{ + return gw_to_dbl(v); +} + +/* Number formatting for PRINT - matches GW-BASIC's FOUT */ +void gw_format_number(gw_value_t *v, char *buf, int bufsize) +{ + switch (v->type) { + case VT_INT: + snprintf(buf, bufsize, "% d", v->ival); + break; + case VT_SNG: { + /* GW-BASIC prints up to 7 significant digits for single */ + double d = v->fval; + if (d == 0.0) { + snprintf(buf, bufsize, " 0"); + break; + } + double ad = fabs(d); + if (ad >= 0.01 && ad < 1e7) { + /* Try fixed notation */ + char tmp[32]; + for (int prec = 7; prec >= 0; prec--) { + snprintf(tmp, sizeof(tmp), "%.*f", prec, d); + /* Count significant digits */ + int sig = 0; + for (char *p = tmp; *p; p++) { + if (*p == '-' || *p == ' ') continue; + if (*p == '.') continue; + if (sig == 0 && *p == '0') continue; + sig++; + } + if (sig <= 7) { + /* Remove trailing zeros after decimal point */ + char *dot = strchr(tmp, '.'); + if (dot) { + char *end = tmp + strlen(tmp) - 1; + while (end > dot && *end == '0') *end-- = '\0'; + if (end == dot) *end = '\0'; + } + if (d >= 0) + snprintf(buf, bufsize, " %s", tmp); + else + snprintf(buf, bufsize, "%s", tmp); + return; + } + } + } + /* Scientific notation - GW-BASIC uses format like 1.234E+10 */ + { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%.6E", fabs(d)); + /* Strip trailing zeros before E: 1.000000E+10 -> 1E+10 */ + char *e = strchr(tmp, 'E'); + if (e) { + char *p = e - 1; + while (p > tmp && *p == '0') p--; + if (*p == '.') p--; /* remove dot if no fractional part */ + memmove(p + 1, e, strlen(e) + 1); + e = strchr(tmp, 'E'); + } + /* Convert 3-digit exponent to 2-digit: E+010 -> E+10 */ + if (e && strlen(e) >= 5 && e[2] == '0') { + memmove(e + 2, e + 3, strlen(e + 3) + 1); + } + if (d < 0) + snprintf(buf, bufsize, "-%s", tmp); + else + snprintf(buf, bufsize, " %s", tmp); + } + break; + } + case VT_DBL: { + double d = v->dval; + if (d == 0.0) { + snprintf(buf, bufsize, " 0"); + break; + } + double ad = fabs(d); + if (ad >= 0.01 && ad < 1e16) { + char tmp[32]; + for (int prec = 16; prec >= 0; prec--) { + snprintf(tmp, sizeof(tmp), "%.*f", prec, d); + int sig = 0; + for (char *p = tmp; *p; p++) { + if (*p == '-' || *p == ' ') continue; + if (*p == '.') continue; + if (sig == 0 && *p == '0') continue; + sig++; + } + if (sig <= 16) { + char *dot = strchr(tmp, '.'); + if (dot) { + char *end = tmp + strlen(tmp) - 1; + while (end > dot && *end == '0') *end-- = '\0'; + if (end == dot) *end = '\0'; + } + if (d >= 0) + snprintf(buf, bufsize, " %s", tmp); + else + snprintf(buf, bufsize, "%s", tmp); + return; + } + } + } + /* Scientific with D exponent for double precision */ + { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%.15E", fabs(d)); + /* Strip trailing zeros before E: 1.000000000000000E+10 -> 1E+10 */ + char *e = strchr(tmp, 'E'); + if (e) { + char *p = e - 1; + while (p > tmp && *p == '0') p--; + if (*p == '.') p--; + memmove(p + 1, e, strlen(e) + 1); + e = strchr(tmp, 'E'); + } + if (e) *e = 'D'; /* GW-BASIC uses D for double */ + /* Convert 3-digit exponent to 2-digit: D+010 -> D+10 */ + if (e && strlen(e) >= 5 && e[2] == '0') { + memmove(e + 2, e + 3, strlen(e + 3) + 1); + } + if (d < 0) + snprintf(buf, bufsize, "-%s", tmp); + else + snprintf(buf, bufsize, " %s", tmp); + } + break; + } + default: + snprintf(buf, bufsize, "?"); + } +} + +/* MBF conversion routines */ +float gw_mbf_to_ieee_single(mbf_single_t mbf) +{ + if (mbf.exponent == 0) return 0.0f; + + uint32_t ieee; + uint8_t sign = mbf.mantissa[2] & 0x80; + uint32_t mantissa = ((uint32_t)(mbf.mantissa[2] | 0x80) << 16) + | ((uint32_t)mbf.mantissa[1] << 8) + | mbf.mantissa[0]; + + int exp = (int)mbf.exponent - 128 - 1 + 127; /* MBF bias 128, IEEE bias 127 */ + if (exp < 0 || exp > 254) return 0.0f; + + ieee = ((uint32_t)sign << 24) | ((uint32_t)exp << 23) | ((mantissa >> 1) & 0x7FFFFF); + float result; + memcpy(&result, &ieee, 4); + return result; +} + +double gw_mbf_to_ieee_double(mbf_double_t mbf) +{ + if (mbf.exponent == 0) return 0.0; + + uint64_t ieee; + uint8_t sign = mbf.mantissa[6] & 0x80; + uint64_t mantissa = 0; + for (int i = 6; i >= 0; i--) + mantissa = (mantissa << 8) | (i == 6 ? (mbf.mantissa[i] | 0x80) : mbf.mantissa[i]); + + int exp = (int)mbf.exponent - 128 - 1 + 1023; + if (exp < 0 || exp > 2046) return 0.0; + + ieee = ((uint64_t)sign << 56) | ((uint64_t)exp << 52) | ((mantissa >> 4) & 0x000FFFFFFFFFFFFF); + double result; + memcpy(&result, &ieee, 8); + return result; +} + +mbf_single_t gw_ieee_to_mbf_single(float f) +{ + mbf_single_t mbf = {{0}, 0}; + if (f == 0.0f) return mbf; + + uint32_t ieee; + memcpy(&ieee, &f, 4); + + uint8_t sign = (ieee >> 31) & 1; + int exp = ((ieee >> 23) & 0xFF) - 127 + 128 + 1; + uint32_t mantissa = (ieee & 0x7FFFFF) | 0x800000; + + if (exp < 0) { mbf.exponent = 0; return mbf; } + if (exp > 255) exp = 255; + + mbf.exponent = exp; + mantissa <<= 1; + mbf.mantissa[0] = mantissa & 0xFF; + mbf.mantissa[1] = (mantissa >> 8) & 0xFF; + mbf.mantissa[2] = ((mantissa >> 16) & 0x7F) | (sign << 7); + + return mbf; +} + +mbf_double_t gw_ieee_to_mbf_double(double d) +{ + mbf_double_t mbf = {{0}, 0}; + if (d == 0.0) return mbf; + + uint64_t ieee; + memcpy(&ieee, &d, 8); + + uint8_t sign = (ieee >> 63) & 1; + int exp = ((ieee >> 52) & 0x7FF) - 1023 + 128 + 1; + uint64_t mantissa = (ieee & 0x000FFFFFFFFFFFFF) | 0x0010000000000000; + + if (exp < 0) { mbf.exponent = 0; return mbf; } + if (exp > 255) exp = 255; + + mbf.exponent = exp; + mantissa <<= 4; + for (int i = 0; i < 7; i++) { + if (i == 6) + mbf.mantissa[i] = ((mantissa >> (i * 8)) & 0x7F) | (sign << 7); + else + mbf.mantissa[i] = (mantissa >> (i * 8)) & 0xFF; + } + + return mbf; +} diff --git a/src/math_int.c b/src/math_int.c new file mode 100644 index 0000000..81fc21b --- /dev/null +++ b/src/math_int.c @@ -0,0 +1,50 @@ +#include "gwbasic.h" +#include + +int16_t gw_int_add(int16_t a, int16_t b) +{ + int32_t r = (int32_t)a + (int32_t)b; + if (r < -32768 || r > 32767) + gw_error(ERR_OV); + return (int16_t)r; +} + +int16_t gw_int_sub(int16_t a, int16_t b) +{ + int32_t r = (int32_t)a - (int32_t)b; + if (r < -32768 || r > 32767) + gw_error(ERR_OV); + return (int16_t)r; +} + +int16_t gw_int_mul(int16_t a, int16_t b) +{ + int32_t r = (int32_t)a * (int32_t)b; + if (r < -32768 || r > 32767) + gw_error(ERR_OV); + return (int16_t)r; +} + +int16_t gw_int_div(int16_t a, int16_t b) +{ + if (b == 0) gw_error(ERR_DZ); + if (a == -32768 && b == -1) gw_error(ERR_OV); + return a / b; +} + +int16_t gw_int_mod(int16_t a, int16_t b) +{ + if (b == 0) gw_error(ERR_DZ); + return a % b; +} + +int16_t gw_int_idiv(int16_t a, int16_t b) +{ + return gw_int_div(a, b); +} + +int16_t gw_int_neg(int16_t a) +{ + if (a == -32768) gw_error(ERR_OV); + return -a; +} diff --git a/src/math_transcend.c b/src/math_transcend.c new file mode 100644 index 0000000..3188e56 --- /dev/null +++ b/src/math_transcend.c @@ -0,0 +1,87 @@ +#include "gwbasic.h" +#include +#include + +/* Random number state (compatible with GW-BASIC's LCG) */ +static uint32_t rnd_seed = 0x50000; /* default seed */ +static int rnd_seeded = 0; + +double gw_sin(double x) { return sin(x); } +double gw_cos(double x) { return cos(x); } + +double gw_tan(double x) +{ + double c = cos(x); + if (c == 0.0) + gw_error(ERR_OV); + return sin(x) / c; +} + +double gw_atn(double x) { return atan(x); } + +double gw_sqr(double x) +{ + if (x < 0.0) + gw_error(ERR_FC); + return sqrt(x); +} + +double gw_log(double x) +{ + if (x <= 0.0) + gw_error(ERR_FC); + return log(x); +} + +double gw_exp(double x) +{ + double r = exp(x); + if (isinf(r) || isnan(r)) + gw_error(ERR_OV); + return r; +} + +double gw_abs(double x) { return fabs(x); } + +double gw_sgn(double x) +{ + if (x > 0.0) return 1.0; + if (x < 0.0) return -1.0; + return 0.0; +} + +double gw_int_fn(double x) +{ + return floor(x); +} + +double gw_fix(double x) +{ + return (x >= 0.0) ? floor(x) : ceil(x); +} + +/* + * RND function - compatible with GW-BASIC: + * RND(0) = repeat last + * RND(>0) = next random + * RND(<0) = seed with argument, return first + */ +double gw_rnd(double x) +{ + if (x < 0.0) { + /* Seed from argument */ + union { float f; uint32_t u; } conv; + conv.f = (float)x; + 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; + rnd_seeded = 1; + } + + return (double)rnd_seed / 16777216.0; +} diff --git a/src/print.c b/src/print.c new file mode 100644 index 0000000..5670529 --- /dev/null +++ b/src/print.c @@ -0,0 +1,125 @@ +#include "gwbasic.h" +#include +#include + +static int print_col = 0; + +static void print_char(int ch) +{ + if (gw_hal) { + gw_hal->putch(ch); + } else { + putchar(ch); + } + if (ch == '\n') + print_col = 0; + else + print_col++; +} + +static void print_str(const char *s) +{ + while (*s) + print_char(*s++); +} + +void gw_print_newline(void) +{ + print_char('\n'); +} + +void gw_print_value(gw_value_t *v) +{ + if (v->type == VT_STR) { + for (int i = 0; i < v->sval.len; i++) + print_char(v->sval.data[i]); + gw_str_free(&v->sval); + } else { + char buf[64]; + gw_format_number(v, buf, sizeof(buf)); + print_str(buf); + /* Trailing space after number */ + print_char(' '); + } +} + +/* + * PRINT statement - reimplements the PRINT code from GW-BASIC. + * Handles: PRINT expr, expr; expr, TAB(n), SPC(n) + */ +void gw_stmt_print(void) +{ + int need_newline = 1; + int screen_width = 80; + if (gw_hal) + screen_width = gw_hal->screen_width; + + for (;;) { + gw_skip_spaces(); + uint8_t tok = gw_chrgot(); + + /* End of statement */ + if (tok == 0 || tok == ':' || tok == TOK_ELSE) + break; + + /* Semicolon - no spacing */ + if (tok == ';') { + gw_chrget(); + need_newline = 0; + continue; + } + + /* Comma - advance to next print zone (14-char zones) */ + if (tok == ',') { + gw_chrget(); + int zone_width = 14; + int target = ((print_col / zone_width) + 1) * zone_width; + if (target >= screen_width) { + gw_print_newline(); + } else { + while (print_col < target) + print_char(' '); + } + need_newline = 0; + continue; + } + + /* TAB(n) */ + if (tok == TOK_TAB) { + gw_chrget(); + /* The '(' is part of the token name */ + int n = gw_eval_int(); + gw_skip_spaces(); + gw_expect_rparen(); + if (n < 1) n = 1; + n--; /* convert to 0-based */ + if (n > print_col) { + while (print_col < n) + print_char(' '); + } + need_newline = 0; + continue; + } + + /* SPC(n) */ + if (tok == TOK_SPC) { + gw_chrget(); + int n = gw_eval_int(); + gw_skip_spaces(); + gw_expect_rparen(); + if (n < 0) n = 0; + for (int i = 0; i < n; i++) + print_char(' '); + need_newline = 0; + continue; + } + + /* Expression */ + gw_value_t v = gw_eval(); + gw_print_value(&v); + need_newline = 1; + } + + if (need_newline) + gw_print_newline(); +} diff --git a/src/strings.c b/src/strings.c new file mode 100644 index 0000000..ba78f0e --- /dev/null +++ b/src/strings.c @@ -0,0 +1,250 @@ +#include "gwbasic.h" +#include +#include +#include +#include + +gw_string_t gw_str_alloc(int len) +{ + gw_string_t s; + if (len < 0 || len > 255) + gw_error(ERR_LS); + s.len = len; + s.data = len > 0 ? malloc(len) : NULL; + if (len > 0 && !s.data) + gw_error(ERR_OS); + return s; +} + +gw_string_t gw_str_from_cstr(const char *cs) +{ + int len = strlen(cs); + if (len > 255) len = 255; + gw_string_t s = gw_str_alloc(len); + memcpy(s.data, cs, len); + return s; +} + +gw_string_t gw_str_copy(gw_string_t *s) +{ + gw_string_t n = gw_str_alloc(s->len); + if (s->len > 0) + memcpy(n.data, s->data, s->len); + return n; +} + +void gw_str_free(gw_string_t *s) +{ + free(s->data); + s->data = NULL; + s->len = 0; +} + +char *gw_str_to_cstr(gw_string_t *s) +{ + char *cs = malloc(s->len + 1); + if (!cs) gw_error(ERR_OS); + memcpy(cs, s->data, s->len); + cs[s->len] = '\0'; + return cs; +} + +gw_value_t gw_fn_len(gw_value_t *s) +{ + if (s->type != VT_STR) gw_error(ERR_TM); + gw_value_t r; + r.type = VT_INT; + r.ival = s->sval.len; + gw_str_free(&s->sval); + return r; +} + +gw_value_t gw_fn_left(gw_value_t *s, int n) +{ + if (s->type != VT_STR) gw_error(ERR_TM); + if (n < 0) gw_error(ERR_FC); + if (n > s->sval.len) n = s->sval.len; + + gw_value_t r; + r.type = VT_STR; + r.sval = gw_str_alloc(n); + memcpy(r.sval.data, s->sval.data, n); + gw_str_free(&s->sval); + return r; +} + +gw_value_t gw_fn_right(gw_value_t *s, int n) +{ + if (s->type != VT_STR) gw_error(ERR_TM); + if (n < 0) gw_error(ERR_FC); + if (n > s->sval.len) n = s->sval.len; + + gw_value_t r; + r.type = VT_STR; + r.sval = gw_str_alloc(n); + memcpy(r.sval.data, s->sval.data + s->sval.len - n, n); + gw_str_free(&s->sval); + return r; +} + +gw_value_t gw_fn_mid(gw_value_t *s, int start, int len) +{ + if (s->type != VT_STR) gw_error(ERR_TM); + if (start < 1) gw_error(ERR_FC); + start--; /* convert to 0-based */ + + int avail = s->sval.len - start; + if (avail < 0) avail = 0; + if (len > avail) len = avail; + + gw_value_t r; + r.type = VT_STR; + r.sval = gw_str_alloc(len); + if (len > 0) + memcpy(r.sval.data, s->sval.data + start, len); + gw_str_free(&s->sval); + return r; +} + +gw_value_t gw_fn_chr(int code) +{ + if (code < 0 || code > 255) gw_error(ERR_FC); + gw_value_t r; + r.type = VT_STR; + r.sval = gw_str_alloc(1); + r.sval.data[0] = (char)code; + return r; +} + +gw_value_t gw_fn_asc(gw_value_t *s) +{ + if (s->type != VT_STR) gw_error(ERR_TM); + if (s->sval.len == 0) gw_error(ERR_FC); + gw_value_t r; + r.type = VT_INT; + r.ival = (uint8_t)s->sval.data[0]; + gw_str_free(&s->sval); + return r; +} + +gw_value_t gw_fn_val(gw_value_t *s) +{ + if (s->type != VT_STR) gw_error(ERR_TM); + char *cs = gw_str_to_cstr(&s->sval); + gw_str_free(&s->sval); + + /* Skip leading spaces */ + char *p = cs; + while (*p == ' ') p++; + + char *end; + double d = strtod(p, &end); + free(cs); + + /* Check for &H hex and &O octal prefix */ + gw_value_t r; + r.type = VT_DBL; + r.dval = d; + return r; +} + +gw_value_t gw_fn_str(gw_value_t *v) +{ + if (v->type == VT_STR) gw_error(ERR_TM); + char buf[32]; + gw_format_number(v, buf, sizeof(buf)); + gw_value_t r; + r.type = VT_STR; + r.sval = gw_str_from_cstr(buf); + return r; +} + +gw_value_t gw_fn_space(int n) +{ + if (n < 0 || n > 255) gw_error(ERR_FC); + gw_value_t r; + r.type = VT_STR; + r.sval = gw_str_alloc(n); + memset(r.sval.data, ' ', n); + return r; +} + +gw_value_t gw_fn_strings(int n, int code) +{ + if (n < 0 || n > 255) gw_error(ERR_FC); + if (code < 0 || code > 255) gw_error(ERR_FC); + gw_value_t r; + r.type = VT_STR; + r.sval = gw_str_alloc(n); + memset(r.sval.data, (char)code, n); + return r; +} + +gw_value_t gw_fn_instr(int start, gw_value_t *haystack, gw_value_t *needle) +{ + if (haystack->type != VT_STR || needle->type != VT_STR) + gw_error(ERR_TM); + if (start < 1) gw_error(ERR_FC); + + gw_value_t r; + r.type = VT_INT; + r.ival = 0; + + if (start > haystack->sval.len || needle->sval.len == 0) { + if (needle->sval.len == 0 && start <= haystack->sval.len + 1) + r.ival = start; + gw_str_free(&haystack->sval); + gw_str_free(&needle->sval); + return r; + } + + for (int i = start - 1; i <= haystack->sval.len - needle->sval.len; i++) { + if (memcmp(haystack->sval.data + i, needle->sval.data, needle->sval.len) == 0) { + r.ival = i + 1; + break; + } + } + + gw_str_free(&haystack->sval); + gw_str_free(&needle->sval); + return r; +} + +gw_value_t gw_fn_hex(int16_t n) +{ + char buf[8]; + snprintf(buf, sizeof(buf), "%X", (uint16_t)n); + gw_value_t r; + r.type = VT_STR; + r.sval = gw_str_from_cstr(buf); + return r; +} + +gw_value_t gw_fn_oct(int16_t n) +{ + char buf[8]; + snprintf(buf, sizeof(buf), "%o", (uint16_t)n); + gw_value_t r; + r.type = VT_STR; + r.sval = gw_str_from_cstr(buf); + return r; +} + +gw_value_t gw_str_concat(gw_value_t *a, gw_value_t *b) +{ + if (a->type != VT_STR || b->type != VT_STR) + gw_error(ERR_TM); + + int newlen = a->sval.len + b->sval.len; + if (newlen > 255) + gw_error(ERR_LS); + + gw_value_t r; + r.type = VT_STR; + r.sval = gw_str_alloc(newlen); + memcpy(r.sval.data, a->sval.data, a->sval.len); + memcpy(r.sval.data + a->sval.len, b->sval.data, b->sval.len); + gw_str_free(&a->sval); + gw_str_free(&b->sval); + return r; +} diff --git a/src/tokenizer.c b/src/tokenizer.c new file mode 100644 index 0000000..10d3bb0 --- /dev/null +++ b/src/tokenizer.c @@ -0,0 +1,362 @@ +#include "gwbasic.h" +#include +#include +#include +#include + +/* + * CRUNCH: convert text to tokenized form. + * Reimplements GWMAIN.ASM's CRUNCH routine. + * + * Text like: PRINT 2+SIN(3.14) + * Becomes: 0x90 [space] 0x0F 0x02 0x00 0xE7 0xFF 0x88 0x28 ... + */ + +static int try_keyword(const char *text, int pos, const keyword_entry_t **match) +{ + int best_len = 0; + const keyword_entry_t *best = NULL; + + for (int i = 0; gw_keywords[i].name; i++) { + const char *kw = gw_keywords[i].name; + int klen = strlen(kw); + + /* Skip single-char operators - handled separately */ + if (klen == 1 && !isalpha((unsigned char)kw[0])) + continue; + + /* Case-insensitive match */ + int j; + for (j = 0; j < klen; j++) { + if (toupper((unsigned char)text[pos + j]) != kw[j]) + break; + } + + if (j == klen && klen > best_len) { + /* For alphabetic keywords, next char must not be alphanumeric */ + if (isalpha((unsigned char)kw[0]) && kw[klen - 1] != '(' && kw[klen - 1] != '$') { + char next = text[pos + klen]; + if (isalnum((unsigned char)next) || next == '.') + continue; + } + best_len = klen; + best = &gw_keywords[i]; + } + } + + *match = best; + return best_len; +} + +int gw_crunch(const char *text, uint8_t *out, int outsize) +{ + int ip = 0; /* input position */ + int op = 0; /* output position */ + int in_rem = 0; + int in_data = 0; + int in_string = 0; + + while (text[ip] && op < outsize - 2) { + char ch = text[ip]; + + /* Inside a string literal */ + if (in_string) { + out[op++] = ch; + if (ch == '"') + in_string = 0; + ip++; + continue; + } + + /* Inside REM or DATA - copy literally (DATA stops at colon) */ + if (in_rem) { + out[op++] = ch; + ip++; + continue; + } + if (in_data) { + if (ch == ':') { + in_data = 0; + /* fall through to normal processing */ + } else { + out[op++] = ch; + ip++; + continue; + } + } + + /* Start of string */ + if (ch == '"') { + in_string = 1; + out[op++] = ch; + ip++; + continue; + } + + /* Single quote = REM shorthand */ + if (ch == '\'') { + out[op++] = TOK_SQUOTE; + ip++; + in_rem = 1; + continue; + } + + /* Colon - statement separator */ + if (ch == ':') { + out[op++] = ':'; + ip++; + continue; + } + + /* Special single-char operators */ + if (ch == '+') { out[op++] = TOK_PLUS; ip++; continue; } + if (ch == '-') { out[op++] = TOK_MINUS; ip++; continue; } + if (ch == '*') { out[op++] = TOK_MUL; ip++; continue; } + if (ch == '/') { out[op++] = TOK_DIV; ip++; continue; } + if (ch == '^') { out[op++] = TOK_POW; ip++; continue; } + if (ch == '\\') { out[op++] = TOK_IDIV; ip++; continue; } + if (ch == '>') { out[op++] = TOK_GT; ip++; continue; } + if (ch == '=') { out[op++] = TOK_EQ; ip++; continue; } + if (ch == '<') { out[op++] = TOK_LT; ip++; continue; } + + /* Try keyword match (alphabetic) */ + if (isalpha((unsigned char)ch)) { + const keyword_entry_t *kw; + int klen = try_keyword(text, ip, &kw); + if (kw) { + if (kw->prefix) { + out[op++] = kw->prefix; + } + out[op++] = kw->token; + ip += klen; + + /* Set REM/DATA modes */ + if (kw->token == TOK_REM && kw->prefix == 0) + in_rem = 1; + if (kw->token == TOK_DATA && kw->prefix == 0) + in_data = 1; + continue; + } + } + + /* &H hex, &O octal, & octal */ + if (ch == '&') { + char next = toupper((unsigned char)text[ip + 1]); + if (next == 'H') { + ip += 2; + int16_t val = (int16_t)strtol(&text[ip], NULL, 16); + while (isxdigit((unsigned char)text[ip])) ip++; + out[op++] = TOK_INT2; + out[op++] = val & 0xFF; + out[op++] = (val >> 8) & 0xFF; + continue; + } + if (next == 'O' || isdigit((unsigned char)next)) { + if (next == 'O') ip += 2; else ip += 1; + int16_t val = (int16_t)strtol(&text[ip], NULL, 8); + while (text[ip] >= '0' && text[ip] <= '7') ip++; + out[op++] = TOK_INT2; + out[op++] = val & 0xFF; + out[op++] = (val >> 8) & 0xFF; + continue; + } + /* Just & by itself */ + out[op++] = ch; + ip++; + continue; + } + + /* Numeric constant */ + if (isdigit((unsigned char)ch) || ch == '.') { + const char *start = &text[ip]; + char *end; + + /* GW-BASIC uses D for double exponent (1D10 = 1E10 as double). + C's strtod doesn't handle D, so scan ahead and substitute. */ + char nbuf[32]; + int is_double = 0; + int is_float = 0; + int scan = 0; + while (scan < (int)sizeof(nbuf) - 1) { + char c = start[scan]; + if (!isdigit((unsigned char)c) && c != '.' && c != '+' && c != '-' + && toupper((unsigned char)c) != 'E' && toupper((unsigned char)c) != 'D') + break; + if (toupper((unsigned char)c) == 'D') { + is_double = 1; + nbuf[scan] = 'E'; + } else { + nbuf[scan] = c; + } + /* Don't allow +/- except after E/D */ + if ((c == '+' || c == '-') && scan > 0 + && toupper((unsigned char)start[scan - 1]) != 'E' + && toupper((unsigned char)start[scan - 1]) != 'D') + break; + scan++; + } + nbuf[scan] = '\0'; + + double val = strtod(nbuf, &end); + int nlen = end - nbuf; + + for (int k = 0; k < nlen; k++) { + if (nbuf[k] == '.') + is_float = 1; + else if (toupper((unsigned char)nbuf[k]) == 'E' && !is_double) + is_float = 1; + } + + /* Check for explicit type suffix after number */ + char suffix = toupper((unsigned char)text[ip + nlen]); + int consumed = nlen; + if (suffix == '!' || suffix == '#' || suffix == '%') + consumed++; + + if (!is_float && !is_double && suffix != '!' && suffix != '#') { + /* Try integer */ + long lval = (long)val; + if (lval >= -32768 && lval <= 32767) { + int16_t ival = (int16_t)lval; + if (ival >= 0 && ival <= 9) { + out[op++] = 0x11 + ival; + } else if (ival >= 10 && ival <= 255) { + out[op++] = TOK_INT1; + out[op++] = (uint8_t)ival; + } else { + out[op++] = TOK_INT2; + out[op++] = ival & 0xFF; + out[op++] = (ival >> 8) & 0xFF; + } + ip += consumed; + continue; + } + } + + /* Double: D exponent or # suffix */ + if (is_double || suffix == '#') { + out[op++] = TOK_CONST_DBL; + memcpy(&out[op], &val, 8); + op += 8; + } else { + /* Single: default for decimal/E numbers */ + float fval = (float)val; + out[op++] = TOK_CONST_SNG; + memcpy(&out[op], &fval, 4); + op += 4; + } + ip += consumed; + continue; + } + + /* Spaces - preserve in token stream (GW-BASIC preserves spaces) */ + if (ch == ' ') { + out[op++] = ' '; + ip++; + continue; + } + + /* Anything else (parens, commas, semicolons, variable names) */ + out[op++] = ch; + ip++; + } + + out[op] = 0; /* null terminate */ + return op; +} + +/* + * LIST: convert tokens back to text. + * Reimplements the LIST subroutine from IBMRES.ASM. + */ +void gw_list_line(uint8_t *tokens, int len, char *out, int outsize) +{ + int ip = 0; + int op = 0; + + while (ip < len && op < outsize - 10) { + uint8_t ch = tokens[ip++]; + + /* Multi-byte token prefixes */ + if (ch == TOK_PREFIX_FF || ch == TOK_PREFIX_FE || ch == TOK_PREFIX_FD) { + if (ip < len) { + uint8_t tok = tokens[ip++]; + const char *name = gw_token_name(ch, tok); + if (name) { + int nlen = strlen(name); + if (op + nlen + 2 < outsize) { + /* Space before keyword if needed */ + if (op > 0 && isalnum((unsigned char)out[op - 1])) + out[op++] = ' '; + memcpy(&out[op], name, nlen); + op += nlen; + } + } + } + continue; + } + + /* Embedded integer constant */ + if (ch == TOK_INT2 && ip + 1 < len) { + int16_t val = (int16_t)(tokens[ip] | (tokens[ip + 1] << 8)); + ip += 2; + op += snprintf(&out[op], outsize - op, "%d", val); + continue; + } + + /* Single-byte integer 0-9 */ + if (ch >= 0x11 && ch <= 0x1A) { + out[op++] = '0' + (ch - 0x11); + continue; + } + + /* One-byte int constant */ + if (ch == TOK_INT1 && ip < len) { + op += snprintf(&out[op], outsize - op, "%d", tokens[ip++]); + continue; + } + + /* Embedded single constant */ + if (ch == TOK_CONST_SNG && ip + 3 < len) { + float fval; + memcpy(&fval, &tokens[ip], 4); + ip += 4; + op += snprintf(&out[op], outsize - op, "%g", (double)fval); + continue; + } + + /* Embedded double constant */ + if (ch == TOK_CONST_DBL && ip + 7 < len) { + double dval; + memcpy(&dval, &tokens[ip], 8); + ip += 8; + op += snprintf(&out[op], outsize - op, "%g", dval); + continue; + } + + /* Single-byte token */ + if (ch >= 0x80) { + const char *name = gw_token_name(0, ch); + if (name) { + int nlen = strlen(name); + if (op + nlen + 2 < outsize) { + if (op > 0 && isalnum((unsigned char)out[op - 1]) + && isalpha((unsigned char)name[0])) + out[op++] = ' '; + memcpy(&out[op], name, nlen); + op += nlen; + /* Space after alphabetic keyword */ + if (isalpha((unsigned char)name[nlen - 1]) + && name[nlen - 1] != '(') + out[op++] = ' '; + } + } + continue; + } + + /* Regular character */ + out[op++] = ch; + } + + out[op] = '\0'; +} diff --git a/src/tokens.c b/src/tokens.c new file mode 100644 index 0000000..a74c6d3 --- /dev/null +++ b/src/tokens.c @@ -0,0 +1,213 @@ +#include "tokens.h" +#include +#include + +/* + * Complete keyword table from IBMRES.ASM RESLST. + * Sorted by token value for reverse lookup; the tokenizer + * uses alphabetic lookup tables like the original. + */ +const keyword_entry_t gw_keywords[] = { + /* Single-byte statement tokens */ + { "END", TOK_END, 0 }, + { "FOR", TOK_FOR, 0 }, + { "NEXT", TOK_NEXT, 0 }, + { "DATA", TOK_DATA, 0 }, + { "INPUT", TOK_INPUT, 0 }, + { "DIM", TOK_DIM, 0 }, + { "READ", TOK_READ, 0 }, + { "LET", TOK_LET, 0 }, + { "GOTO", TOK_GOTO, 0 }, + { "RUN", TOK_RUN, 0 }, + { "IF", TOK_IF, 0 }, + { "RESTORE", TOK_RESTORE, 0 }, + { "GOSUB", TOK_GOSUB, 0 }, + { "RETURN", TOK_RETURN, 0 }, + { "REM", TOK_REM, 0 }, + { "STOP", TOK_STOP, 0 }, + { "PRINT", TOK_PRINT, 0 }, + { "CLEAR", TOK_CLEAR, 0 }, + { "LIST", TOK_LIST, 0 }, + { "NEW", TOK_NEW, 0 }, + { "ON", TOK_ON, 0 }, + { "WAIT", TOK_WAIT, 0 }, + { "DEF", TOK_DEF, 0 }, + { "POKE", TOK_POKE, 0 }, + { "CONT", TOK_CONT, 0 }, + { "OUT", TOK_OUT, 0 }, + { "LPRINT", TOK_LPRINT, 0 }, + { "LLIST", TOK_LLIST, 0 }, + { "WIDTH", TOK_WIDTH, 0 }, + { "ELSE", TOK_ELSE, 0 }, + { "TRON", TOK_TRON, 0 }, + { "TROFF", TOK_TROFF, 0 }, + { "SWAP", TOK_SWAP, 0 }, + { "ERASE", TOK_ERASE, 0 }, + { "EDIT", TOK_EDIT, 0 }, + { "ERROR", TOK_ERROR, 0 }, + { "RESUME", TOK_RESUME, 0 }, + { "DELETE", TOK_DELETE, 0 }, + { "AUTO", TOK_AUTO, 0 }, + { "RENUM", TOK_RENUM, 0 }, + { "DEFSTR", TOK_DEFSTR, 0 }, + { "DEFINT", TOK_DEFINT, 0 }, + { "DEFSNG", TOK_DEFSNG, 0 }, + { "DEFDBL", TOK_DEFDBL, 0 }, + { "LINE", TOK_LINE, 0 }, + { "WHILE", TOK_WHILE, 0 }, + { "WEND", TOK_WEND, 0 }, + { "CALL", TOK_CALL, 0 }, + { "WRITE", TOK_WRITE, 0 }, + { "OPTION", TOK_OPTION, 0 }, + { "RANDOMIZE", TOK_RANDOMIZE, 0 }, + { "OPEN", TOK_OPEN, 0 }, + { "CLOSE", TOK_CLOSE, 0 }, + { "LOAD", TOK_LOAD, 0 }, + { "MERGE", TOK_MERGE, 0 }, + { "SAVE", TOK_SAVE, 0 }, + { "COLOR", TOK_COLOR, 0 }, + { "CLS", TOK_CLS, 0 }, + { "MOTOR", TOK_MOTOR, 0 }, + { "BSAVE", TOK_BSAVE, 0 }, + { "BLOAD", TOK_BLOAD, 0 }, + { "SOUND", TOK_SOUND, 0 }, + { "BEEP", TOK_BEEP, 0 }, + { "PSET", TOK_PSET, 0 }, + { "PRESET", TOK_PRESET, 0 }, + { "SCREEN", TOK_SCREEN, 0 }, + { "KEY", TOK_KEY, 0 }, + { "LOCATE", TOK_LOCATE, 0 }, + + /* Non-statement keyword tokens */ + { "TO", TOK_TO, 0 }, + { "THEN", TOK_THEN, 0 }, + { "TAB(", TOK_TAB, 0 }, + { "STEP", TOK_STEP, 0 }, + { "USR", TOK_USR, 0 }, + { "FN", TOK_FN, 0 }, + { "SPC(", TOK_SPC, 0 }, + { "NOT", TOK_NOT, 0 }, + { "ERL", TOK_ERL, 0 }, + { "ERR", TOK_ERR, 0 }, + { "STRING$", TOK_STRINGS, 0 }, + { "USING", TOK_USING, 0 }, + { "INSTR", TOK_INSTR, 0 }, + { "'", TOK_SQUOTE, 0 }, + { "VARPTR", TOK_VARPTR, 0 }, + { "CSRLIN", TOK_CSRLIN, 0 }, + { "POINT", TOK_POINT, 0 }, + { "OFF", TOK_OFF, 0 }, + { "INKEY$", TOK_INKEYS, 0 }, + + /* Operator tokens */ + { ">", TOK_GT, 0 }, + { "=", TOK_EQ, 0 }, + { "<", TOK_LT, 0 }, + { "+", TOK_PLUS, 0 }, + { "-", TOK_MINUS, 0 }, + { "*", TOK_MUL, 0 }, + { "/", TOK_DIV, 0 }, + { "^", TOK_POW, 0 }, + { "AND", TOK_AND, 0 }, + { "OR", TOK_OR, 0 }, + { "XOR", TOK_XOR, 0 }, + { "EQV", TOK_EQV, 0 }, + { "IMP", TOK_IMP, 0 }, + { "MOD", TOK_MOD, 0 }, + { "\\", TOK_IDIV, 0 }, + + /* Built-in functions (prefix 0xFF) */ + { "LEFT$", FUNC_LEFT, TOK_PREFIX_FF }, + { "RIGHT$", FUNC_RIGHT, TOK_PREFIX_FF }, + { "MID$", FUNC_MID, TOK_PREFIX_FF }, + { "SGN", FUNC_SGN, TOK_PREFIX_FF }, + { "INT", FUNC_INT, TOK_PREFIX_FF }, + { "ABS", FUNC_ABS, TOK_PREFIX_FF }, + { "SQR", FUNC_SQR, TOK_PREFIX_FF }, + { "RND", FUNC_RND, TOK_PREFIX_FF }, + { "SIN", FUNC_SIN, TOK_PREFIX_FF }, + { "LOG", FUNC_LOG, TOK_PREFIX_FF }, + { "EXP", FUNC_EXP, TOK_PREFIX_FF }, + { "COS", FUNC_COS, TOK_PREFIX_FF }, + { "TAN", FUNC_TAN, TOK_PREFIX_FF }, + { "ATN", FUNC_ATN, TOK_PREFIX_FF }, + { "FRE", FUNC_FRE, TOK_PREFIX_FF }, + { "INP", FUNC_INP, TOK_PREFIX_FF }, + { "POS", FUNC_POS, TOK_PREFIX_FF }, + { "LEN", FUNC_LEN, TOK_PREFIX_FF }, + { "STR$", FUNC_STR, TOK_PREFIX_FF }, + { "VAL", FUNC_VAL, TOK_PREFIX_FF }, + { "ASC", FUNC_ASC, TOK_PREFIX_FF }, + { "CHR$", FUNC_CHR, TOK_PREFIX_FF }, + { "PEEK", FUNC_PEEK, TOK_PREFIX_FF }, + { "SPACE$", FUNC_SPACE, TOK_PREFIX_FF }, + { "OCT$", FUNC_OCT, TOK_PREFIX_FF }, + { "HEX$", FUNC_HEX, TOK_PREFIX_FF }, + { "LPOS", FUNC_LPOS, TOK_PREFIX_FF }, + { "CINT", FUNC_CINT, TOK_PREFIX_FF }, + { "CSNG", FUNC_CSNG, TOK_PREFIX_FF }, + { "CDBL", FUNC_CDBL, TOK_PREFIX_FF }, + { "FIX", FUNC_FIX, TOK_PREFIX_FF }, + { "PEN", FUNC_PEN, TOK_PREFIX_FF }, + { "STICK", FUNC_STICK, TOK_PREFIX_FF }, + { "STRIG", FUNC_STRIG, TOK_PREFIX_FF }, + { "EOF", FUNC_EOF, TOK_PREFIX_FF }, + { "LOC", FUNC_LOC, TOK_PREFIX_FF }, + { "LOF", FUNC_LOF, TOK_PREFIX_FF }, + + /* Extended statements (prefix 0xFE) */ + { "FILES", XSTMT_FILES, TOK_PREFIX_FE }, + { "FIELD", XSTMT_FIELD, TOK_PREFIX_FE }, + { "SYSTEM", XSTMT_SYSTEM, TOK_PREFIX_FE }, + { "NAME", XSTMT_NAME, TOK_PREFIX_FE }, + { "LSET", XSTMT_LSET, TOK_PREFIX_FE }, + { "RSET", XSTMT_RSET, TOK_PREFIX_FE }, + { "KILL", XSTMT_KILL, TOK_PREFIX_FE }, + { "PUT", XSTMT_PUT, TOK_PREFIX_FE }, + { "GET", XSTMT_GET, TOK_PREFIX_FE }, + { "RESET", XSTMT_RESET, TOK_PREFIX_FE }, + { "COMMON", XSTMT_COMMON, TOK_PREFIX_FE }, + { "CHAIN", XSTMT_CHAIN, TOK_PREFIX_FE }, + { "DATE$", XSTMT_DATE, TOK_PREFIX_FE }, + { "TIME$", XSTMT_TIME, TOK_PREFIX_FE }, + { "PAINT", XSTMT_PAINT, TOK_PREFIX_FE }, + { "COM", XSTMT_COM, TOK_PREFIX_FE }, + { "CIRCLE", XSTMT_CIRCLE, TOK_PREFIX_FE }, + { "DRAW", XSTMT_DRAW, TOK_PREFIX_FE }, + { "PLAY", XSTMT_PLAY, TOK_PREFIX_FE }, + { "TIMER", XSTMT_TIMER, TOK_PREFIX_FE }, + { "ERDEV", XSTMT_ERDEV, TOK_PREFIX_FE }, + { "IOCTL", XSTMT_IOCTL, TOK_PREFIX_FE }, + { "CHDIR", XSTMT_CHDIR, TOK_PREFIX_FE }, + { "MKDIR", XSTMT_MKDIR, TOK_PREFIX_FE }, + { "RMDIR", XSTMT_RMDIR, TOK_PREFIX_FE }, + { "SHELL", XSTMT_SHELL, TOK_PREFIX_FE }, + { "ENVIRON", XSTMT_ENVIRON, TOK_PREFIX_FE }, + { "VIEW", XSTMT_VIEW, TOK_PREFIX_FE }, + { "WINDOW", XSTMT_WINDOW, TOK_PREFIX_FE }, + { "PMAP", XSTMT_PMAP, TOK_PREFIX_FE }, + { "PALETTE", XSTMT_PALETTE, TOK_PREFIX_FE }, + { "LCOPY", XSTMT_LCOPY, TOK_PREFIX_FE }, + { "CALLS", XSTMT_CALLS, TOK_PREFIX_FE }, + + /* Extended functions (prefix 0xFD) */ + { "CVI", XFUNC_CVI, TOK_PREFIX_FD }, + { "CVS", XFUNC_CVS, TOK_PREFIX_FD }, + { "CVD", XFUNC_CVD, TOK_PREFIX_FD }, + { "MKI$", XFUNC_MKI, TOK_PREFIX_FD }, + { "MKS$", XFUNC_MKS, TOK_PREFIX_FD }, + { "MKD$", XFUNC_MKD, TOK_PREFIX_FD }, + + { NULL, 0, 0 } +}; + +int gw_keyword_count = sizeof(gw_keywords) / sizeof(gw_keywords[0]) - 1; + +const char *gw_token_name(uint8_t prefix, uint8_t token) +{ + for (int i = 0; gw_keywords[i].name; i++) { + if (gw_keywords[i].prefix == prefix && gw_keywords[i].token == token) + return gw_keywords[i].name; + } + return NULL; +} diff --git a/tests/programs/hello.bas b/tests/programs/hello.bas new file mode 100644 index 0000000..60aba5d --- /dev/null +++ b/tests/programs/hello.bas @@ -0,0 +1 @@ +PRINT "HELLO, WORLD!" diff --git a/tests/programs/math_ops.bas b/tests/programs/math_ops.bas new file mode 100644 index 0000000..e9822e1 --- /dev/null +++ b/tests/programs/math_ops.bas @@ -0,0 +1,19 @@ +PRINT "Arithmetic:" +PRINT 2+2 +PRINT 10-3 +PRINT 6*7 +PRINT 100/3 +PRINT 2^10 +PRINT 17 MOD 5 +PRINT 17\5 +PRINT "Functions:" +PRINT SIN(3.14159/2) +PRINT COS(0) +PRINT SQR(144) +PRINT ABS(-42) +PRINT INT(3.7) +PRINT FIX(-3.7) +PRINT LOG(1) +PRINT EXP(0) +PRINT SGN(-5) +PRINT ATN(1)*4 diff --git a/tests/programs/string_ops.bas b/tests/programs/string_ops.bas new file mode 100644 index 0000000..c41d74d --- /dev/null +++ b/tests/programs/string_ops.bas @@ -0,0 +1,14 @@ +PRINT LEFT$("HELLO WORLD", 5) +PRINT RIGHT$("HELLO WORLD", 5) +PRINT MID$("HELLO WORLD", 7, 5) +PRINT LEN("TEST") +PRINT CHR$(65) +PRINT ASC("A") +PRINT VAL("3.14") +PRINT STR$(42) +PRINT SPACE$(5)+"X" +PRINT STRING$(10, 42) +PRINT HEX$(255) +PRINT OCT$(255) +PRINT INSTR("HELLO WORLD", "WORLD") +PRINT "ABC" + "DEF"