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.
This commit is contained in:
Eremey Valetov
2026-03-29 04:55:48 -04:00
parent 724f823536
commit eb32021c19
10 changed files with 201 additions and 10 deletions
+1
View File
@@ -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
+18
View File
@@ -0,0 +1,18 @@
#ifndef STRPOOL_H
#define STRPOOL_H
#include <stddef.h>
#include <stdbool.h>
#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
+1 -1
View File
@@ -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 */
+6 -3
View File
@@ -3,6 +3,7 @@
#include "graphics.h"
#include "virmem.h"
#include "portio.h"
#include "strpool.h"
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
@@ -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:
+21 -2
View File
@@ -3,6 +3,7 @@
#include "graphics.h"
#include "virmem.h"
#include "portio.h"
#include "strpool.h"
#include "sound.h"
#include <ctype.h>
#include <string.h>
@@ -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();
+2
View File
@@ -2,6 +2,7 @@
#include "tui.h"
#include "sound.h"
#include "portio.h"
#include "strpool.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -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();
+3 -4
View File
@@ -1,4 +1,5 @@
#include "gwbasic.h"
#include "strpool.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
@@ -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;
}
+111
View File
@@ -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 <stdlib.h>
#include <string.h>
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);
}
+7
View File
@@ -0,0 +1,7 @@
INITIAL OK
ALLOC OK
GC RECLAIMED
CONCAT LEN= 50
POOL OK
CLEAR SIZE OK
ALL DONE
+31
View File
@@ -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