Repository navigation
Expand file tree
/
Copy pathstrings.py
More file actions
313 lines (270 loc) · 11.8 KB
/
Copy pathstrings.py
File metadata and controls
313 lines (270 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
"""
PC-BASIC - strings.py
String values
(c) 2013--2023 Rob Hagemans
This file is released under the GNU GPL version 3 or later.
"""
import struct
import logging
from operator import itemgetter
from ...compat import iteritems
from ..base import error
from . import numbers
class String(numbers.Value):
"""String pointer."""
sigil = b'$'
size = 3
def __init__(self, buffer, values):
"""Initialise the pointer."""
numbers.Value.__init__(self, buffer, values)
self._stringspace = values.stringspace
def length(self):
"""String length."""
return bytearray(self._buffer)[0]
def address(self):
"""Pointer address."""
return struct.unpack_from('<H', self._buffer, 1)[0]
def dereference(self):
"""String value pointed to."""
length, address = struct.unpack('<BH', self._buffer)
return self._stringspace.view(length, address).tobytes()
def from_str(self, python_str):
"""Set to value of python str."""
assert isinstance(python_str, bytes), type(python_str)
self._buffer[:] = struct.pack('<BH', *self._stringspace.store(python_str))
return self
def from_pointer(self, length, address):
"""Set buffer to string pointer."""
self._buffer[:] = struct.pack('<BH', length, address)
return self
def to_pointer(self):
"""Get length and address."""
return struct.unpack('<BH', self._buffer)
from_value = from_str
to_value = dereference
to_str = dereference
def add(self, right):
"""Concatenate strings. In-place for the pointer."""
return self.new().from_str(self.dereference() + right.dereference())
def eq(self, right):
"""This string equals the right-hand side."""
return self.to_str() == right.to_str()
def gt(self, right):
"""This string orders after the right-hand side."""
left = self.to_str()
right = right.to_str()
shortest = min(len(left), len(right))
for i in range(shortest):
if left[i] > right[i]:
return True
elif left[i] < right[i]:
return False
# the same so far...
# the shorter string is said to be less than the longer,
# provided they are the same up till the length of the shorter.
if len(left) > len(right):
return True
# left is shorter, or equal strings
return False
def lset(self, in_str, justify_right):
"""Justify a str into an existing buffer and pad with spaces."""
# v is empty string if variable does not exist
# trim and pad to size of target buffer
length = self.length()
in_str = in_str.to_value()
if justify_right:
in_str = in_str[:length].rjust(length)
else:
in_str = in_str[:length].ljust(length)
# make a copy only if not in a writeable location
target = self._stringspace.check_modify(*self.to_pointer())
self.from_pointer(*target)
# copy the new string in
self._stringspace.view(*target)[:] = in_str
return self
def midset(self, start, num, val):
"""Modify a string in an existing buffer."""
# we need to decrement basic offset by 1 to get python offset
offset = start - 1
# don't overwrite more of the old string than the length of the new string
num = min(num, val.length())
# ensure the length of source string matches target
length = self.length()
if offset + num > length:
num = length - offset
if num <= 0:
return self
# make a copy only if not in a writeable location
target = self._stringspace.check_modify(*self.to_pointer())
self.from_pointer(*target)
source = val.to_pointer()
if source != target:
self._stringspace.view(*target)[offset:offset+num] = (
self._stringspace.view(*source)[:num]
)
else:
# copy byte by byte from left to right
# to conform to GW overwriting of source string on overlap
for i in range(num):
self._stringspace.view(*target)[i+offset:i+offset+1] = (
self._stringspace.view(*source)[i:i+1]
)
return self
######################################################################
# unary functions
def len(self):
"""LEN: length of string."""
return numbers.Integer(None, self._values).from_int(self.length())
def asc(self):
"""ASC: ordinal ASCII value of a character."""
s = bytearray(self.to_str())
error.throw_if(not s)
return numbers.Integer(None, self._values).from_int(s[0])
def space(self, num):
"""SPACE$: repeat spaces."""
num = num.to_integer().to_int()
error.range_check(0, 255, num)
return self.new().from_str(b' ' * num)
class StringSpace(object):
"""Table of strings accessible by their length and address."""
def __init__(self, memory):
"""Initialise empty string space."""
self._memory = memory
self._strings = {}
self._temp = None
self.clear()
def __repr__(self):
"""Debugging representation of string table."""
return '\n'.join(
'%x: %r' % (n, bytes(v))
for n, v in iteritems(self._strings)
)
def clear(self):
"""Empty string space."""
self._strings.clear()
# strings are placed at the top of string memory, just below the stack
self.current = self._memory.stack_start()
def rebuild(self, stringspace):
"""Rebuild from stored copy."""
self.clear()
self._strings.update(stringspace._strings)
self.current = stringspace.current
def copy_to(self, string_space, length, address):
"""Copy a string to another string space."""
return string_space.store(self.view(length, address).tobytes())
def _retrieve(self, length, address):
"""Retrieve a string by its pointer."""
# if string length == 0, return empty string
try:
return bytearray() if length == 0 else self._strings[address]
except KeyError: # pragma: no cover
raise KeyError(u'Dereferencing detached string at %x (%d)' % (address, address))
def view(self, length, address):
"""Return a writeable view of a string from its string pointer."""
# empty string pointers can point anywhere
if length == 0:
return memoryview(bytearray())
if address >= self._memory.var_start():
# string stored in string space
return memoryview(self._retrieve(length, address))
elif address >= self._memory.code_start:
# get string stored in code as bytearray
codestr = self._memory.program.get_memory_block(address, length)
# NOTE this is a writeable view of a *copy* of the code!
return memoryview(codestr)
else:
# string stored in field buffers
return self._memory.view_field_memory(address, length)
def check_modify(self, length, address):
"""Assign a new string into an existing buffer."""
# if it is a code literal, we now do need to allocate space for a copy
if address >= self._memory.code_start and address < self._memory.var_start():
length, address = self.store(self.view(length, address).tobytes())
return length, address
def store(self, in_str, address=None, check_free=True):
"""Store a new string and return the string pointer."""
length = len(in_str)
# don't store overlong strings
if length > 255:
raise error.BASICError(error.STRING_TOO_LONG)
# don't store if address is provided (code or FIELD strings)
if address is None:
# reserve string space; collect garbage if necessary
if check_free:
self._memory.check_free(length, error.OUT_OF_STRING_SPACE)
# find new string address
self.current -= length
address = self.current + 1
# don't store empty strings
if length > 0:
# copy and convert to bytearray
self._strings[address] = bytearray(in_str)
return length, address
def _delete_last(self):
"""Delete the string provided if it is at the top of string space."""
last_address = self.current + 1
try:
length = len(self._strings[last_address])
self.current += length
del self._strings[last_address]
except KeyError: # pragma: no cover
# maybe happens if we're called before an out-of-memory exception is handled
# and the string wasn't allocated
pass
def collect_garbage(self, string_ptrs):
"""Re-store the strings referenced in string_ptrs, delete the rest."""
# string_ptrs should be a list of memoryviews to the original pointers
# retrieve addresses and copy strings
string_list = []
# find last non-temporary string
last_permanent = self._memory.stack_start()
last_perm_view = None
for view in string_ptrs:
length, addr = struct.unpack('<BH', view.tobytes())
# exclude empty elements of string arrays (len==0 and addr==0)
# exclude strings is not located in memory (FIELD or code strings)
if addr >= self._memory.var_start():
string_list.append((view, addr, self._retrieve(length, addr)))
# set sentinel string (lowest-address permanent string)
# don't use zero-length strings as sentinel:
# they share an address with allocated strings and may get swapped on sorting
# in which case the allocated permanent string ends up below the sentinel
if self._temp is not None and length > 0:
if addr > self._temp and addr < last_permanent:
last_permanent, last_perm_view = addr, view
# sort by address, largest first (maintain order of storage)
string_list.sort(key=itemgetter(1), reverse=True)
# clear the string buffer and re-store all referenced strings
self.clear()
for view, _, string in string_list:
# re-allocate string space
# update the original pointers supplied (these are memoryviews)
view[:] = struct.pack('<BH', *self.store(string, check_free=False))
# readdress start of temporary strings
if last_perm_view is None:
self._temp = None
elif self._temp is not None and self._temp != self._memory.stack_start():
self._temp = -1 + struct.unpack_from('<H', last_perm_view.tobytes(), 1)[0]
def get_memory(self, address):
"""Retrieve data from data memory: string space """
# find the variable we're in
for try_address, value in iteritems(self._strings):
length = len(value)
if try_address <= address < try_address + length:
return value[address - try_address]
return -1
def fix_temporaries(self):
"""Make all temporary strings permanent."""
self._temp = self.current
def reset_temporaries(self):
"""Delete temporary string at top of string space."""
if self._temp is not None and self._temp != self.current:
self._delete_last()
self._temp = self.current
def is_permanent(self, string):
"""Return whether string is in permanent string space."""
addr = string.address()
return addr > self._temp
def is_field_string(self, string):
"""Return whether string is a FIELD string."""
return string.address() < self._memory.code_start