From eb32021c192e47868423cb90755c657fa586c54e Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Sun, 29 Mar 2026 04:55:48 -0400 Subject: [PATCH] Implement string space pool with compacting garbage collector Replaces individual malloc/free string management with a contiguous pool (default 32KB) and compacting GC, matching the original GW-BASIC's GETSPA/GARBAG architecture. Allocation is a bump pointer; gw_str_free() is a no-op (descriptors are nulled, data reclaimed by GC). Compaction runs at statement boundaries when the pool drops below 4KB free, walking the variable table and array storage to relocate live strings. FRE() now returns actual free pool space. FRE("") triggers a GC pass before reporting. CLEAR n sets the string space size. --- CMakeLists.txt | 1 + include/strpool.h | 18 +++++ include/types.h | 2 +- src/eval.c | 9 ++- src/interp.c | 23 ++++++- src/main.c | 2 + src/strings.c | 7 +- src/strpool.c | 111 ++++++++++++++++++++++++++++++ tests/expected/string_gc.expected | 7 ++ tests/programs/string_gc.bas | 31 +++++++++ 10 files changed, 201 insertions(+), 10 deletions(-) create mode 100644 include/strpool.h create mode 100644 src/strpool.c create mode 100644 tests/expected/string_gc.expected create mode 100644 tests/programs/string_gc.bas 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