diff --git a/CMakeLists.txt b/CMakeLists.txt index 2445680..9a5dc9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ set(SOURCES src/graphics.c src/virmem.c src/portio.c + src/strpool.c src/sound.c src/tui.c platform/hal_posix.c diff --git a/include/strpool.h b/include/strpool.h new file mode 100644 index 0000000..4b862c6 --- /dev/null +++ b/include/strpool.h @@ -0,0 +1,18 @@ +#ifndef STRPOOL_H +#define STRPOOL_H + +#include +#include + +#define STRPOOL_DEFAULT_SIZE (32 * 1024) /* 32KB — generous vs original's ~3KB */ +#define STRPOOL_GC_THRESHOLD 4096 /* GC when less than 4KB free */ + +void strpool_init(size_t size); +void strpool_shutdown(void); +void strpool_reset(size_t size); /* resize and clear (0 = keep current size) */ +char *strpool_alloc(int len); /* bump-allocate from pool */ +void strpool_gc(void); /* compact: walk vars+arrays, squeeze out dead space */ +size_t strpool_free(void); /* bytes available */ +bool strpool_owns(const char *p); /* true if p points into the pool */ + +#endif diff --git a/include/types.h b/include/types.h index 82a7ada..d98954f 100644 --- a/include/types.h +++ b/include/types.h @@ -16,7 +16,7 @@ typedef enum { /* String descriptor - matches original's 3-byte layout concept */ typedef struct { uint8_t len; - char *data; /* malloc'd, NOT null-terminated */ + char *data; /* points into string pool, NOT null-terminated */ } gw_string_t; /* Unified value - the FAC (floating accumulator) equivalent */ diff --git a/src/eval.c b/src/eval.c index 4859218..59aa3b3 100644 --- a/src/eval.c +++ b/src/eval.c @@ -3,6 +3,7 @@ #include "graphics.h" #include "virmem.h" #include "portio.h" +#include "strpool.h" #include #include #include @@ -640,10 +641,12 @@ static gw_value_t eval_function(uint8_t prefix, uint8_t func_tok) 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 */ + if (arg.type == VT_STR) { + gw_str_free(&arg.sval); + strpool_gc(); + } v.type = VT_SNG; - v.fval = 60000.0f; + v.fval = (float)strpool_free(); return v; case FUNC_POS: diff --git a/src/interp.c b/src/interp.c index 1442589..96b1e11 100644 --- a/src/interp.c +++ b/src/interp.c @@ -3,6 +3,7 @@ #include "graphics.h" #include "virmem.h" #include "portio.h" +#include "strpool.h" #include "sound.h" #include #include @@ -1462,6 +1463,7 @@ void gw_exec_stmt(void) gw_chrget(); gfx_shutdown(); portio_reset(); + strpool_reset(0); gw_free_program(); gw_vars_clear(); gw_arrays_clear(); @@ -1483,9 +1485,20 @@ void gw_exec_stmt(void) return; } - /* CLEAR */ + /* CLEAR [stringspace] */ if (tok == TOK_CLEAR) { gw_chrget(); + gw_skip_spaces(); + size_t new_pool = 0; + /* Parse optional string space size */ + if (gw_chrgot() == ',') { + gw_chrget(); /* skip comma */ + gw_skip_spaces(); + if (gw_chrgot() && gw_chrgot() != ',' && gw_chrgot() != ':' && gw_chrgot() != 0) + new_pool = (size_t)gw_eval_int(); + } else if (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != 0) { + new_pool = (size_t)gw_eval_int(); + } portio_reset(); gw_vars_clear(); gw_arrays_clear(); @@ -1498,7 +1511,8 @@ void gw_exec_stmt(void) gw.data_line_ptr = NULL; gw.on_error_line = 0; gw.in_error_handler = false; - /* Skip optional args (memory size, stack size) */ + strpool_reset(new_pool); + /* Skip remaining optional args (stack size) */ while (gw_chrgot() && gw_chrgot() != ':') gw.text_ptr++; return; @@ -1541,6 +1555,7 @@ void gw_exec_stmt(void) if (!start) return; portio_reset(); + strpool_reset(0); gw_vars_clear(); gw_arrays_clear(); memset(gw.fn_defs, 0, sizeof(gw.fn_defs)); @@ -3151,6 +3166,10 @@ void gw_run_loop(void) } while (gw.running) { + /* Compact string pool if running low */ + if (strpool_free() < STRPOOL_GC_THRESHOLD) + strpool_gc(); + /* Check for Ctrl+Break */ if (tui.active) tui_check_break(); diff --git a/src/main.c b/src/main.c index 407c9f4..f50979b 100644 --- a/src/main.c +++ b/src/main.c @@ -2,6 +2,7 @@ #include "tui.h" #include "sound.h" #include "portio.h" +#include "strpool.h" #include #include #include @@ -169,6 +170,7 @@ int main(int argc, char **argv) gw_hal = hal_posix_create(); gw_hal->init(); gw_init(); + strpool_init(STRPOOL_DEFAULT_SIZE); snd_init(); portio_reset(); diff --git a/src/strings.c b/src/strings.c index ba78f0e..3cdf713 100644 --- a/src/strings.c +++ b/src/strings.c @@ -1,4 +1,5 @@ #include "gwbasic.h" +#include "strpool.h" #include #include #include @@ -10,9 +11,7 @@ gw_string_t gw_str_alloc(int len) 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); + s.data = len > 0 ? strpool_alloc(len) : NULL; return s; } @@ -35,7 +34,7 @@ gw_string_t gw_str_copy(gw_string_t *s) void gw_str_free(gw_string_t *s) { - free(s->data); + /* Pool strings are reclaimed by GC, not individually freed */ s->data = NULL; s->len = 0; } diff --git a/src/strpool.c b/src/strpool.c new file mode 100644 index 0000000..3685d83 --- /dev/null +++ b/src/strpool.c @@ -0,0 +1,111 @@ +/* + * String space pool with compacting garbage collector. + * + * Original GW-BASIC stored all string data in a contiguous "string space" + * (GETSPA/GARBAG in GWEVAL.ASM). Allocation was a bump pointer; when the + * space filled, GARBAG compacted it by scanning every live string descriptor + * (variables, arrays, temporaries) and sliding live data toward the bottom. + * + * This reimplementation follows the same model. The pool is a flat char + * buffer. gw_str_alloc() bumps a pointer; gw_str_free() is a no-op (just + * nulls the descriptor). Compaction runs at statement boundaries — at that + * point no temporaries exist on the C stack, so the only roots are the + * variable table and array storage. + */ + +#include "strpool.h" +#include "gwbasic.h" +#include +#include + +static char *pool; +static size_t pool_size; +static size_t pool_used; + +void strpool_init(size_t size) +{ + pool = malloc(size); + if (!pool) { pool_size = 0; pool_used = 0; return; } + pool_size = size; + pool_used = 0; +} + +void strpool_shutdown(void) +{ + free(pool); + pool = NULL; + pool_size = pool_used = 0; +} + +void strpool_reset(size_t size) +{ + if (size == 0) size = pool_size; + if (size != pool_size) { + free(pool); + pool = malloc(size); + pool_size = pool ? size : 0; + } + pool_used = 0; +} + +bool strpool_owns(const char *p) +{ + return p >= pool && p < pool + pool_size; +} + +char *strpool_alloc(int len) +{ + if ((size_t)len > pool_size - pool_used) + gw_error(ERR_OS); + char *p = pool + pool_used; + pool_used += len; + return p; +} + +size_t strpool_free(void) +{ + return pool_size - pool_used; +} + +/* + * Compacting garbage collector. + * + * Walk every live string descriptor (variables + arrays). Copy live data + * into a temporary buffer in order, then memcpy back and update pool_used. + * String descriptors are updated in place to point at their new locations. + */ +void strpool_gc(void) +{ + char *tmp = malloc(pool_size); + if (!tmp) return; + + size_t offset = 0; + + for (int i = 0; i < gw.var_count; i++) { + if (gw.vars[i].type == VT_STR) { + gw_string_t *s = &gw.vars[i].val.sval; + if (s->len > 0 && s->data && strpool_owns(s->data)) { + memcpy(tmp + offset, s->data, s->len); + s->data = pool + offset; + offset += s->len; + } + } + } + + for (int i = 0; i < gw.array_count; i++) { + if (gw.arrays[i].type == VT_STR) { + for (int j = 0; j < gw.arrays[i].total_elements; j++) { + gw_string_t *s = &gw.arrays[i].data[j].sval; + if (s->len > 0 && s->data && strpool_owns(s->data)) { + memcpy(tmp + offset, s->data, s->len); + s->data = pool + offset; + offset += s->len; + } + } + } + } + + memcpy(pool, tmp, offset); + pool_used = offset; + free(tmp); +} diff --git a/tests/expected/string_gc.expected b/tests/expected/string_gc.expected new file mode 100644 index 0000000..196c218 --- /dev/null +++ b/tests/expected/string_gc.expected @@ -0,0 +1,7 @@ +INITIAL OK +ALLOC OK +GC RECLAIMED +CONCAT LEN= 50 +POOL OK +CLEAR SIZE OK +ALL DONE diff --git a/tests/programs/string_gc.bas b/tests/programs/string_gc.bas new file mode 100644 index 0000000..79e6d0f --- /dev/null +++ b/tests/programs/string_gc.bas @@ -0,0 +1,31 @@ +10 REM Test string garbage collection +20 REM FRE("") triggers GC and returns free string space +30 F0 = FRE("") +40 IF F0 > 0 THEN PRINT "INITIAL OK" ELSE PRINT "INITIAL FAIL" +50 REM --- Allocate some string variables +60 A$ = STRING$(200, "A") +70 B$ = STRING$(200, "B") +80 C$ = STRING$(200, "C") +90 F1 = FRE("") +100 IF F1 < F0 THEN PRINT "ALLOC OK" ELSE PRINT "ALLOC FAIL" +110 REM --- Overwrite with shorter strings (old data becomes garbage) +120 A$ = "short" +130 B$ = "tiny" +140 C$ = "x" +150 F2 = FRE("") +160 IF F2 > F1 THEN PRINT "GC RECLAIMED" ELSE PRINT "GC FAIL" +170 REM --- Stress test: loop with string concatenation +180 CLEAR 4096 +190 D$ = "" +200 FOR I = 1 TO 50 +210 D$ = D$ + "X" +220 NEXT I +230 PRINT "CONCAT LEN="; LEN(D$) +240 F3 = FRE("") +250 IF F3 > 0 THEN PRINT "POOL OK" ELSE PRINT "POOL FAIL" +260 REM --- CLEAR with string space size +270 CLEAR 8192 +280 F4 = FRE("") +290 IF F4 > 7000 AND F4 < 9000 THEN PRINT "CLEAR SIZE OK" ELSE PRINT "CLEAR SIZE FAIL" +300 PRINT "ALL DONE" +310 SYSTEM