qubasic 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qbasic.py +113 -0
- qbasic_core/__init__.py +80 -0
- qbasic_core/__main__.py +6 -0
- qbasic_core/analysis.py +179 -0
- qbasic_core/backend.py +76 -0
- qbasic_core/classic.py +419 -0
- qbasic_core/control_flow.py +576 -0
- qbasic_core/debug.py +327 -0
- qbasic_core/demos.py +356 -0
- qbasic_core/display.py +274 -0
- qbasic_core/engine.py +126 -0
- qbasic_core/engine_state.py +109 -0
- qbasic_core/errors.py +37 -0
- qbasic_core/exec_context.py +24 -0
- qbasic_core/executor.py +861 -0
- qbasic_core/expression.py +228 -0
- qbasic_core/file_io.py +457 -0
- qbasic_core/gates.py +284 -0
- qbasic_core/help_text.py +167 -0
- qbasic_core/io_protocol.py +33 -0
- qbasic_core/locc.py +10 -0
- qbasic_core/locc_commands.py +221 -0
- qbasic_core/locc_display.py +61 -0
- qbasic_core/locc_engine.py +195 -0
- qbasic_core/locc_execution.py +389 -0
- qbasic_core/memory.py +369 -0
- qbasic_core/mock_backend.py +66 -0
- qbasic_core/noise_mixin.py +96 -0
- qbasic_core/parser.py +564 -0
- qbasic_core/patterns.py +186 -0
- qbasic_core/profiler.py +156 -0
- qbasic_core/program_mgmt.py +369 -0
- qbasic_core/protocol.py +77 -0
- qbasic_core/py.typed +0 -0
- qbasic_core/scope.py +74 -0
- qbasic_core/screen.py +115 -0
- qbasic_core/state_display.py +60 -0
- qbasic_core/statements.py +387 -0
- qbasic_core/strings.py +107 -0
- qbasic_core/subs.py +261 -0
- qbasic_core/sweep.py +82 -0
- qbasic_core/terminal.py +1697 -0
- qubasic-0.1.0.dist-info/METADATA +736 -0
- qubasic-0.1.0.dist-info/RECORD +48 -0
- qubasic-0.1.0.dist-info/WHEEL +5 -0
- qubasic-0.1.0.dist-info/entry_points.txt +2 -0
- qubasic-0.1.0.dist-info/licenses/LICENSE +21 -0
- qubasic-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""QBASIC state display mixin — STATE, BLOCH, HIST, PROBS commands."""
|
|
2
|
+
|
|
3
|
+
from qbasic_core.engine import MAX_BLOCH_DISPLAY
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StateDisplayMixin:
|
|
7
|
+
"""State inspection commands for QBasicTerminal.
|
|
8
|
+
|
|
9
|
+
Requires: TerminalProtocol — uses self.locc_mode, self.last_sv,
|
|
10
|
+
self.last_counts, self.last_circuit, self.num_qubits,
|
|
11
|
+
self._locc_state(), self._locc_bloch(),
|
|
12
|
+
self._print_statevector(), self._print_probs(), self._print_bloch_single(),
|
|
13
|
+
self.print_histogram(), self.io.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def cmd_state(self, rest: str = '') -> None:
|
|
17
|
+
if self.locc_mode:
|
|
18
|
+
return self._locc_state(rest)
|
|
19
|
+
if self.last_sv is None:
|
|
20
|
+
self.io.writeln("?NO STATE — RUN first")
|
|
21
|
+
return
|
|
22
|
+
self._print_statevector(self.last_sv)
|
|
23
|
+
|
|
24
|
+
def cmd_hist(self) -> None:
|
|
25
|
+
if self.last_counts is None:
|
|
26
|
+
self.io.writeln("?NO RESULTS — RUN first")
|
|
27
|
+
return
|
|
28
|
+
self.print_histogram(self.last_counts)
|
|
29
|
+
|
|
30
|
+
def cmd_probs(self) -> None:
|
|
31
|
+
if self.last_sv is None:
|
|
32
|
+
self.io.writeln("?NO STATE — RUN first")
|
|
33
|
+
return
|
|
34
|
+
self._print_probs(self.last_sv)
|
|
35
|
+
|
|
36
|
+
def cmd_bloch(self, rest: str) -> None:
|
|
37
|
+
if self.locc_mode:
|
|
38
|
+
return self._locc_bloch(rest)
|
|
39
|
+
if self.last_sv is None:
|
|
40
|
+
self.io.writeln("?NO STATE — RUN first")
|
|
41
|
+
return
|
|
42
|
+
if rest:
|
|
43
|
+
q = int(rest)
|
|
44
|
+
self._print_bloch_single(self.last_sv, q)
|
|
45
|
+
else:
|
|
46
|
+
for q in range(min(self.num_qubits, MAX_BLOCH_DISPLAY)):
|
|
47
|
+
self._print_bloch_single(self.last_sv, q)
|
|
48
|
+
if q < min(self.num_qubits, MAX_BLOCH_DISPLAY) - 1:
|
|
49
|
+
self.io.writeln('')
|
|
50
|
+
|
|
51
|
+
def cmd_circuit(self) -> None:
|
|
52
|
+
if self.last_circuit is None:
|
|
53
|
+
self.io.writeln("?NO CIRCUIT — RUN first")
|
|
54
|
+
return
|
|
55
|
+
try:
|
|
56
|
+
self.io.writeln(self.last_circuit.draw(output='text'))
|
|
57
|
+
except Exception:
|
|
58
|
+
self.io.writeln(f"Circuit: {self.last_circuit.num_qubits} qubits, "
|
|
59
|
+
f"depth {self.last_circuit.depth()}, "
|
|
60
|
+
f"{self.last_circuit.size()} gates")
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
"""QBASIC typed statement AST — produced by the parser, consumed by the executor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class Stmt:
|
|
10
|
+
"""Base for all parsed statements."""
|
|
11
|
+
raw: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ── Terminals ──────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class RemStmt(Stmt):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class MeasureStmt(Stmt):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class EndStmt(Stmt):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class ReturnStmt(Stmt):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class BarrierStmt(Stmt):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class WendStmt(Stmt):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class RestoreStmt(Stmt):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class EndSelectStmt(Stmt):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class EndSubStmt(Stmt):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class EndFunctionStmt(Stmt):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class StopStmt(Stmt):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── Control flow ───────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class GotoStmt(Stmt):
|
|
65
|
+
target: int
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class GosubStmt(Stmt):
|
|
69
|
+
target: int
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class ForStmt(Stmt):
|
|
73
|
+
var: str
|
|
74
|
+
start_expr: str
|
|
75
|
+
end_expr: str
|
|
76
|
+
step_expr: str | None
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True, slots=True)
|
|
79
|
+
class NextStmt(Stmt):
|
|
80
|
+
var: str
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True, slots=True)
|
|
83
|
+
class WhileStmt(Stmt):
|
|
84
|
+
condition: str
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True, slots=True)
|
|
87
|
+
class IfThenStmt(Stmt):
|
|
88
|
+
condition: str
|
|
89
|
+
then_clause: str
|
|
90
|
+
else_clause: str | None
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True, slots=True)
|
|
93
|
+
class DoStmt(Stmt):
|
|
94
|
+
kind: str | None # WHILE or UNTIL or None
|
|
95
|
+
condition: str | None
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class LoopStmt(Stmt):
|
|
99
|
+
kind: str | None
|
|
100
|
+
condition: str | None
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True, slots=True)
|
|
103
|
+
class ExitStmt(Stmt):
|
|
104
|
+
target: str # FOR, WHILE, DO, SUB, FUNCTION
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True, slots=True)
|
|
107
|
+
class OnGotoStmt(Stmt):
|
|
108
|
+
expr: str
|
|
109
|
+
targets: tuple[int, ...]
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class OnGosubStmt(Stmt):
|
|
113
|
+
expr: str
|
|
114
|
+
targets: tuple[int, ...]
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class SelectCaseStmt(Stmt):
|
|
118
|
+
expr: str
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True, slots=True)
|
|
121
|
+
class CaseStmt(Stmt):
|
|
122
|
+
value: str
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True, slots=True)
|
|
125
|
+
class CallStmt(Stmt):
|
|
126
|
+
name: str
|
|
127
|
+
args: str
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True, slots=True)
|
|
130
|
+
class SubStmt(Stmt):
|
|
131
|
+
name: str
|
|
132
|
+
params: str
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True, slots=True)
|
|
135
|
+
class FunctionStmt(Stmt):
|
|
136
|
+
name: str
|
|
137
|
+
params: str
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True, slots=True)
|
|
140
|
+
class OnErrorStmt(Stmt):
|
|
141
|
+
target: int
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True, slots=True)
|
|
144
|
+
class ResumeStmt(Stmt):
|
|
145
|
+
arg: str | None
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True, slots=True)
|
|
148
|
+
class ErrorStmt(Stmt):
|
|
149
|
+
code: int
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True)
|
|
152
|
+
class AssertStmt(Stmt):
|
|
153
|
+
condition: str
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True, slots=True)
|
|
156
|
+
class SwapStmt(Stmt):
|
|
157
|
+
a: str
|
|
158
|
+
b: str
|
|
159
|
+
|
|
160
|
+
@dataclass(frozen=True, slots=True)
|
|
161
|
+
class DefFnStmt(Stmt):
|
|
162
|
+
name: str
|
|
163
|
+
params: str
|
|
164
|
+
body: str
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True, slots=True)
|
|
167
|
+
class OptionBaseStmt(Stmt):
|
|
168
|
+
base: int
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True, slots=True)
|
|
171
|
+
class OnMeasureStmt(Stmt):
|
|
172
|
+
target: int
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True, slots=True)
|
|
175
|
+
class OnTimerStmt(Stmt):
|
|
176
|
+
interval: str
|
|
177
|
+
target: int
|
|
178
|
+
|
|
179
|
+
@dataclass(frozen=True, slots=True)
|
|
180
|
+
class DataStmt(Stmt):
|
|
181
|
+
values: str
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True, slots=True)
|
|
184
|
+
class ReadStmt(Stmt):
|
|
185
|
+
var_list: str
|
|
186
|
+
|
|
187
|
+
@dataclass(frozen=True, slots=True)
|
|
188
|
+
class LocalStmt(Stmt):
|
|
189
|
+
var_list: str
|
|
190
|
+
|
|
191
|
+
@dataclass(frozen=True, slots=True)
|
|
192
|
+
class StaticStmt(Stmt):
|
|
193
|
+
var_list: str
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True, slots=True)
|
|
196
|
+
class SharedStmt(Stmt):
|
|
197
|
+
var_list: str
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ── Assignment ─────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True, slots=True)
|
|
203
|
+
class LetStmt(Stmt):
|
|
204
|
+
name: str
|
|
205
|
+
expr: str
|
|
206
|
+
|
|
207
|
+
@dataclass(frozen=True, slots=True)
|
|
208
|
+
class LetArrayStmt(Stmt):
|
|
209
|
+
name: str
|
|
210
|
+
index_expr: str
|
|
211
|
+
value_expr: str
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True, slots=True)
|
|
214
|
+
class LetStrStmt(Stmt):
|
|
215
|
+
name: str
|
|
216
|
+
expr: str
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── I/O ────────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
@dataclass(frozen=True, slots=True)
|
|
222
|
+
class PrintStmt(Stmt):
|
|
223
|
+
expr: str
|
|
224
|
+
|
|
225
|
+
@dataclass(frozen=True, slots=True)
|
|
226
|
+
class PrintUsingStmt(Stmt):
|
|
227
|
+
fmt: str
|
|
228
|
+
values: str
|
|
229
|
+
|
|
230
|
+
@dataclass(frozen=True, slots=True)
|
|
231
|
+
class InputStmt(Stmt):
|
|
232
|
+
prompt: str | None
|
|
233
|
+
var: str
|
|
234
|
+
|
|
235
|
+
@dataclass(frozen=True, slots=True)
|
|
236
|
+
class LineInputStmt(Stmt):
|
|
237
|
+
prompt: str | None
|
|
238
|
+
var: str
|
|
239
|
+
|
|
240
|
+
@dataclass(frozen=True, slots=True)
|
|
241
|
+
class GetStmt(Stmt):
|
|
242
|
+
var: str
|
|
243
|
+
|
|
244
|
+
@dataclass(frozen=True, slots=True)
|
|
245
|
+
class DimStmt(Stmt):
|
|
246
|
+
name: str
|
|
247
|
+
size: str
|
|
248
|
+
|
|
249
|
+
@dataclass(frozen=True, slots=True)
|
|
250
|
+
class RedimStmt(Stmt):
|
|
251
|
+
name: str
|
|
252
|
+
size: str
|
|
253
|
+
|
|
254
|
+
@dataclass(frozen=True, slots=True)
|
|
255
|
+
class EraseStmt(Stmt):
|
|
256
|
+
name: str
|
|
257
|
+
|
|
258
|
+
@dataclass(frozen=True, slots=True)
|
|
259
|
+
class PokeStmt(Stmt):
|
|
260
|
+
addr_expr: str
|
|
261
|
+
value_expr: str
|
|
262
|
+
|
|
263
|
+
@dataclass(frozen=True, slots=True)
|
|
264
|
+
class SysStmt(Stmt):
|
|
265
|
+
arg: str
|
|
266
|
+
|
|
267
|
+
@dataclass(frozen=True, slots=True)
|
|
268
|
+
class UnitaryStmt(Stmt):
|
|
269
|
+
name: str
|
|
270
|
+
matrix: str
|
|
271
|
+
|
|
272
|
+
@dataclass(frozen=True, slots=True)
|
|
273
|
+
class OpenStmt(Stmt):
|
|
274
|
+
path: str
|
|
275
|
+
mode: str
|
|
276
|
+
handle: int
|
|
277
|
+
encoding: str | None = None
|
|
278
|
+
|
|
279
|
+
@dataclass(frozen=True, slots=True)
|
|
280
|
+
class CloseStmt(Stmt):
|
|
281
|
+
handle: int
|
|
282
|
+
|
|
283
|
+
@dataclass(frozen=True, slots=True)
|
|
284
|
+
class PrintFileStmt(Stmt):
|
|
285
|
+
handle: int
|
|
286
|
+
data: str
|
|
287
|
+
|
|
288
|
+
@dataclass(frozen=True, slots=True)
|
|
289
|
+
class InputFileStmt(Stmt):
|
|
290
|
+
handle: int
|
|
291
|
+
var: str
|
|
292
|
+
|
|
293
|
+
@dataclass(frozen=True, slots=True)
|
|
294
|
+
class LprintStmt(Stmt):
|
|
295
|
+
expr: str
|
|
296
|
+
|
|
297
|
+
@dataclass(frozen=True, slots=True)
|
|
298
|
+
class ScreenStmt(Stmt):
|
|
299
|
+
mode: str
|
|
300
|
+
|
|
301
|
+
@dataclass(frozen=True, slots=True)
|
|
302
|
+
class ColorStmt(Stmt):
|
|
303
|
+
fg: str
|
|
304
|
+
bg: str | None
|
|
305
|
+
|
|
306
|
+
@dataclass(frozen=True, slots=True)
|
|
307
|
+
class LocateStmt(Stmt):
|
|
308
|
+
row: str
|
|
309
|
+
col: str
|
|
310
|
+
|
|
311
|
+
@dataclass(frozen=True, slots=True)
|
|
312
|
+
class ImportStmt(Stmt):
|
|
313
|
+
path: str
|
|
314
|
+
|
|
315
|
+
@dataclass(frozen=True, slots=True)
|
|
316
|
+
class ChainStmt(Stmt):
|
|
317
|
+
path: str
|
|
318
|
+
|
|
319
|
+
@dataclass(frozen=True, slots=True)
|
|
320
|
+
class MergeStmt(Stmt):
|
|
321
|
+
path: str
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# ── Quantum ────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
@dataclass(frozen=True, slots=True)
|
|
327
|
+
class MeasStmt(Stmt):
|
|
328
|
+
qubit_expr: str
|
|
329
|
+
var: str
|
|
330
|
+
|
|
331
|
+
@dataclass(frozen=True, slots=True)
|
|
332
|
+
class ResetStmt(Stmt):
|
|
333
|
+
qubit_expr: str
|
|
334
|
+
|
|
335
|
+
@dataclass(frozen=True, slots=True)
|
|
336
|
+
class MeasureBasisStmt(Stmt):
|
|
337
|
+
basis: str
|
|
338
|
+
qubit_expr: str
|
|
339
|
+
|
|
340
|
+
@dataclass(frozen=True, slots=True)
|
|
341
|
+
class SyndromeStmt(Stmt):
|
|
342
|
+
rest: str
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ── LOCC ───────────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
@dataclass(frozen=True, slots=True)
|
|
348
|
+
class SendStmt(Stmt):
|
|
349
|
+
reg: str
|
|
350
|
+
qubit_expr: str
|
|
351
|
+
var: str
|
|
352
|
+
|
|
353
|
+
@dataclass(frozen=True, slots=True)
|
|
354
|
+
class ShareStmt(Stmt):
|
|
355
|
+
reg1: str
|
|
356
|
+
q1: int
|
|
357
|
+
reg2: str
|
|
358
|
+
q2: int
|
|
359
|
+
|
|
360
|
+
@dataclass(frozen=True, slots=True)
|
|
361
|
+
class AtRegStmt(Stmt):
|
|
362
|
+
reg: str
|
|
363
|
+
inner: str
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ── Compound ───────────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
@dataclass(frozen=True, slots=True)
|
|
369
|
+
class CompoundStmt(Stmt):
|
|
370
|
+
parts: tuple[str, ...]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ── Gate application ───────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
@dataclass(frozen=True, slots=True)
|
|
376
|
+
class GateStmt(Stmt):
|
|
377
|
+
"""Direct gate application: H 0, CX 0,1, RX PI/4, 0"""
|
|
378
|
+
name: str # canonical gate name (after alias resolution)
|
|
379
|
+
args: tuple[str, ...] # raw argument strings (params + qubits, unparsed)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ── Fallback ───────────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
@dataclass(frozen=True, slots=True)
|
|
385
|
+
class RawStmt(Stmt):
|
|
386
|
+
"""Unrecognized statement — falls through to legacy regex path."""
|
|
387
|
+
pass
|
qbasic_core/strings.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""QBASIC string support — string variables, functions, and operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _left(s: str, n: float) -> str:
|
|
10
|
+
return s[:int(n)]
|
|
11
|
+
|
|
12
|
+
def _right(s: str, n: float) -> str:
|
|
13
|
+
return s[-int(n):] if int(n) > 0 else ''
|
|
14
|
+
|
|
15
|
+
def _mid(s: str, start: float, length: float = -1) -> str:
|
|
16
|
+
i = int(start) - 1 # 1-based in BASIC
|
|
17
|
+
if length < 0:
|
|
18
|
+
return s[i:]
|
|
19
|
+
return s[i:i + int(length)]
|
|
20
|
+
|
|
21
|
+
def _instr(haystack: str, needle: str) -> float:
|
|
22
|
+
pos = haystack.find(needle)
|
|
23
|
+
return float(pos + 1) if pos >= 0 else 0.0 # 1-based, 0 = not found
|
|
24
|
+
|
|
25
|
+
def _asc(s: str) -> float:
|
|
26
|
+
return float(ord(s[0])) if s else 0.0
|
|
27
|
+
|
|
28
|
+
def _chr_fn(n: float) -> str:
|
|
29
|
+
return chr(int(n))
|
|
30
|
+
|
|
31
|
+
def _str_fn(n: float) -> str:
|
|
32
|
+
v = float(n)
|
|
33
|
+
return str(int(v)) if v == int(v) else str(v)
|
|
34
|
+
|
|
35
|
+
def _val_fn(s: str) -> float:
|
|
36
|
+
try:
|
|
37
|
+
return float(s)
|
|
38
|
+
except (ValueError, TypeError):
|
|
39
|
+
return 0.0
|
|
40
|
+
|
|
41
|
+
def _hex_fn(n: float) -> str:
|
|
42
|
+
return format(int(n), 'X')
|
|
43
|
+
|
|
44
|
+
def _bin_fn(n: float) -> str:
|
|
45
|
+
return format(int(n), 'b')
|
|
46
|
+
|
|
47
|
+
def _len_fn(x: Any) -> float:
|
|
48
|
+
if isinstance(x, str):
|
|
49
|
+
return float(len(x))
|
|
50
|
+
if isinstance(x, (list, tuple)):
|
|
51
|
+
return float(len(x))
|
|
52
|
+
return float(len(str(x)))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Functions that return strings (names ending in $)
|
|
56
|
+
STRING_FUNCS: dict[str, Any] = {
|
|
57
|
+
'LEFT$': _left, 'RIGHT$': _right, 'MID$': _mid,
|
|
58
|
+
'CHR$': _chr_fn, 'STR$': _str_fn, 'HEX$': _hex_fn, 'BIN$': _bin_fn,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Functions that take/return mixed types
|
|
62
|
+
MIXED_FUNCS: dict[str, Any] = {
|
|
63
|
+
'ASC': _asc, 'VAL': _val_fn, 'INSTR': _instr, 'LEN': _len_fn,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class StringMixin:
|
|
68
|
+
"""String variable and function support for QBasicTerminal.
|
|
69
|
+
|
|
70
|
+
Requires: TerminalProtocol — uses self.variables, self._safe_eval().
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def _is_string_var(self, name: str) -> bool:
|
|
74
|
+
return name.endswith('$')
|
|
75
|
+
|
|
76
|
+
def _get_string_ns(self) -> dict[str, Any]:
|
|
77
|
+
"""Return namespace entries for string functions."""
|
|
78
|
+
ns: dict[str, Any] = {}
|
|
79
|
+
ns.update(STRING_FUNCS)
|
|
80
|
+
ns.update(MIXED_FUNCS)
|
|
81
|
+
# String variables
|
|
82
|
+
for k, v in self.variables.items():
|
|
83
|
+
if isinstance(v, str):
|
|
84
|
+
ns[k] = v
|
|
85
|
+
return ns
|
|
86
|
+
|
|
87
|
+
def _eval_string_expr(self, expr: str) -> str | float:
|
|
88
|
+
"""Evaluate an expression that might return a string."""
|
|
89
|
+
expr = expr.strip()
|
|
90
|
+
# Quoted string literal
|
|
91
|
+
if (expr.startswith('"') and expr.endswith('"')) or \
|
|
92
|
+
(expr.startswith("'") and expr.endswith("'")):
|
|
93
|
+
return expr[1:-1]
|
|
94
|
+
# String variable
|
|
95
|
+
if expr in self.variables and isinstance(self.variables[expr], str):
|
|
96
|
+
return self.variables[expr]
|
|
97
|
+
# Try numeric
|
|
98
|
+
try:
|
|
99
|
+
return self.eval_expr(expr)
|
|
100
|
+
except Exception:
|
|
101
|
+
return expr
|
|
102
|
+
|
|
103
|
+
def cmd_let_str(self, name: str, expr: str) -> None:
|
|
104
|
+
"""Assign a string value to a string variable."""
|
|
105
|
+
val = self._eval_string_expr(expr)
|
|
106
|
+
self.variables[name] = val
|
|
107
|
+
self.io.writeln(f"{name} = {val!r}" if isinstance(val, str) else f"{name} = {val}")
|