Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions src/bassy.nim
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ type
program: Program
strings: TextStorage
stringLiterals: seq[int32]
stringRoots: seq[Value]
limits: Limits
globals: seq[Value]
memory: seq[Value]
Expand Down Expand Up @@ -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 [
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions src/bassy/texts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 45 additions & 1 deletion tests/test_strings.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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"
Loading