From 669a7c4b94e3d5b0a38dc9557608a7e58c2a764d Mon Sep 17 00:00:00 2001 From: treeform Date: Thu, 24 Sep 2026 11:45:27 -0700 Subject: [PATCH] Reclaim BASIC strings when restarting scripts --- src/bassy.nim | 47 ++++++++++++++++++++++++++++++++++++++++-- src/bassy/texts.nim | 14 +++++++++++++ tests/test_strings.nim | 46 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/bassy.nim b/src/bassy.nim index a6c6f00..097e3cd 100644 --- a/src/bassy.nim +++ b/src/bassy.nim @@ -272,6 +272,7 @@ type program: Program strings: TextStorage stringLiterals: seq[int32] + stringRoots: seq[Value] limits: Limits globals: seq[Value] memory: seq[Value] @@ -3188,7 +3189,9 @@ proc initRuntimeState( allocatedBytes += cells * LogicalValueBytes if program.usesStrings: allocatedBytes += storageBytes(limits.maxStrings, limits.maxStringBytes) + - int64(program.literals.len) * 4 + int64(program.literals.len) * 4 + + (globalCells + arrayCells + hostDataCells + + int64(program.literals.len)) * LogicalValueBytes if allocatedBytes > limits.maxMemoryBytes: fail("BASIC runtime exceeds the configured memory limit") for cells in [ @@ -3233,6 +3236,11 @@ proc initRuntimeState( result.strings = initTextStorage( limits.maxStrings, limits.maxStringBytes, limits.maxStringLength ) + result.stringRoots = newSeq[Value](portableCells( + globalCells + arrayCells + hostDataCells + + int64(program.literals.len), + "too many BASIC string roots for this target" + )) result.stringLiterals = newSeq[int32](program.literals.len) for handle in result.stringLiterals.mitems: handle = -1 @@ -3301,7 +3309,35 @@ proc reset*(runtime: var Runtime) = runtime.finished = false proc restart*(runtime: var Runtime) = - ## Restarts execution and budgets while preserving globals and arrays. + ## Restarts execution and reclaims strings, preserving globals and arrays. + ## Host-held string Values must be fetched again after restarting. + if runtime.program.usesStrings: + let + globals = runtime.globals.len + cells = runtime.memory.len + literals = globals + cells + runtime.hostData.len + for i, value in runtime.globals: + runtime.stringRoots[i] = value + for i, value in runtime.memory: + runtime.stringRoots[globals + i] = value + for i, value in runtime.hostData: + runtime.stringRoots[globals + cells + i] = value + for i, handle in runtime.stringLiterals: + runtime.stringRoots[literals + i] = + if handle >= 0: + stringValue(runtime.strings.empty.stringOwner, handle) + else: + Value() + runtime.strings.reset(runtime.stringRoots) + for i in 0 ..< globals: + runtime.globals[i] = runtime.stringRoots[i] + for i in 0 ..< cells: + runtime.memory[i] = runtime.stringRoots[globals + i] + for i in 0 ..< runtime.hostData.len: + runtime.hostData[i] = runtime.stringRoots[globals + cells + i] + for i, handle in runtime.stringLiterals.mpairs: + let value = runtime.stringRoots[literals + i] + handle = if value.kind == StringValue: value.stringHandle else: -1 runtime.registers.clear runtime.arguments.clear runtime.pc = runtime.program.routines[0].entry @@ -3527,6 +3563,13 @@ proc getString*(runtime: Runtime, value: Value): string = ## Reads owned text without exposing raw arena handles. runtime.strings.get(value) +template withString*(runtime: Runtime, value: Value, text, body: untyped) = + ## Borrows string bytes for a block that must not modify or restart the VM. + block: + let source = runtime + source.strings.withText(value, text): + body + proc putString*(runtime: var Runtime, value: string): Value = ## Copies host text into bounded storage for a string-valued callback. runtime.strings.put(value) diff --git a/src/bassy/texts.nim b/src/bassy/texts.nim index c5c24fb..c6f7681 100644 --- a/src/bassy/texts.nim +++ b/src/bassy/texts.nim @@ -114,6 +114,20 @@ proc get*(storage: TextStorage, value: Value): string = if view.length > 0: result = storage.arena[int(view.start) ..< int(view.start + view.length)] +template withText*( + storage: var TextStorage, value: Value, text, body: untyped +) = + ## Borrows validated bytes until the storage is next modified. + block: + let + source = addr storage + view = source[].span(value) + template text: untyped = + source[].arena.toOpenArray( + int(view.start), int(view.start + view.length) - 1 + ) + body + proc count*(storage: TextStorage): int {.raises: [].} = ## Returns the number of occupied string slots including the empty string. storage.spans.len diff --git a/tests/test_strings.nim b/tests/test_strings.nim index fa61429..e8a9518 100644 --- a/tests/test_strings.nim +++ b/tests/test_strings.nim @@ -187,7 +187,8 @@ saved$(1) = answer$ doAssert runtime.getStringGlobal("message$") == "host global" runtime.setData("mail$", "new mail") runtime.restart - doAssert runtime.getString(value) == "ok" + doAssert runtime.getStringGlobal("answer$") == "ok" + doAssert errorContains(proc() = discard runtime.getString(value), "stale") var other = initRuntime(program, host) doAssert errorContains(proc() = other.setGlobal("answer$", value), "stale") doAssert errorContains(proc() = runtime.setGlobal("answer$", 1), "type") @@ -312,4 +313,47 @@ c$ = right$(a$, 4) doAssert runtime.stringBytes == 6 doAssert runtime.getStringData("second$") == "cdef" +echo "Testing repeated string restarts and borrowed host access" +block: + var host = initHost() + discard host.addData("incoming$", "host value") + var runtime: Runtime + let inspect: NumericHostProc = proc(args: openArray[Value]): Value = + ## Borrows a substring without constructing a Nim string. + runtime.withString(args[0], text): + doAssert text.len == 4 and text[0] == 'e' and text[3] == 'i' + toValue(1) + discard host.addFunction("inspect", 1, inspect) + let program = compile(""" +dim saved$(2) +if turns = 0 then + saved$(0) = "persistent text" + saved$(1) = mid$(saved$(0), 2, 4) +end if +turns = turns + 1 +message$ = "turn " + str$(turns) + incoming$ +saved$(2) = message$ +ok = inspect(saved$(1)) +""", host) + runtime = initRuntime(program, host) + for i in 1 .. 1000: + runtime.restart() + discard runtime.run() + doAssert runtime.getGlobal("turns") == i + doAssert runtime.getStringArray("saved$", 0) == "persistent text" + doAssert runtime.getStringArray("saved$", 1) == "ersi" + doAssert runtime.getStringArray("saved$", 2) == + "turn " & $i & "host value" + doAssert runtime.getStringData("incoming$") == "host value" + doAssert runtime.stringCount < 32 + doAssert runtime.stringBytes < 256 + let empty = runtime.putString("") + runtime.withString(empty, text): + doAssert text.len == 0 + runtime.reset() + doAssert errorContains(proc() = + runtime.withString(empty, text): + discard text.len + , "stale") + echo "Native string tests passed"