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,369 @@
|
|
|
1
|
+
"""QBASIC program management — AUTO, EDIT, COPY, MOVE, FIND, REPLACE, slots, CHECKSUM, CHAIN, MERGE, LIST+."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import hashlib
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from qbasic_core.engine import RE_CHAIN, RE_MERGE, RE_DEF_BEGIN
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProgramMgmtMixin:
|
|
14
|
+
"""Program management commands for QBasicTerminal.
|
|
15
|
+
|
|
16
|
+
Requires: TerminalProtocol — uses self.program, self.variables,
|
|
17
|
+
self.num_qubits, self.shots, self.process(), self.cmd_new(),
|
|
18
|
+
self._sanitize_path().
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def _init_program_mgmt(self) -> None:
|
|
22
|
+
self._program_slots: dict[int, dict[int, str]] = {}
|
|
23
|
+
self._current_slot: int = 0
|
|
24
|
+
self._auto_mode: bool = False
|
|
25
|
+
self._auto_line: int = 10
|
|
26
|
+
self._auto_step: int = 10
|
|
27
|
+
|
|
28
|
+
# ── AUTO ───────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
def cmd_auto(self, rest: str = '') -> None:
|
|
31
|
+
"""AUTO [start][, step] — auto-generate line numbers."""
|
|
32
|
+
parts = rest.replace(',', ' ').split()
|
|
33
|
+
if parts:
|
|
34
|
+
self._auto_line = int(parts[0])
|
|
35
|
+
if len(parts) > 1:
|
|
36
|
+
self._auto_step = int(parts[1])
|
|
37
|
+
self._auto_mode = True
|
|
38
|
+
self.io.writeln(f"AUTO {self._auto_line}, {self._auto_step} — type . to stop")
|
|
39
|
+
while self._auto_mode:
|
|
40
|
+
try:
|
|
41
|
+
line = self.io.read_line(f'{self._auto_line} ').rstrip()
|
|
42
|
+
if line == '.':
|
|
43
|
+
self._auto_mode = False
|
|
44
|
+
break
|
|
45
|
+
if line:
|
|
46
|
+
self.process(f'{self._auto_line} {line}')
|
|
47
|
+
self._auto_line += self._auto_step
|
|
48
|
+
except (KeyboardInterrupt, EOFError):
|
|
49
|
+
self._auto_mode = False
|
|
50
|
+
self.io.writeln('')
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
# ── EDIT ───────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
def cmd_edit(self, rest: str) -> None:
|
|
56
|
+
"""EDIT <line> — edit a specific line."""
|
|
57
|
+
if not rest.strip():
|
|
58
|
+
self.io.writeln("?USAGE: EDIT <line>")
|
|
59
|
+
return
|
|
60
|
+
num = int(rest.strip())
|
|
61
|
+
if num not in self.program:
|
|
62
|
+
self.io.writeln(f"?LINE {num} NOT FOUND")
|
|
63
|
+
return
|
|
64
|
+
current = self.program[num]
|
|
65
|
+
self.io.writeln(f" {num} {current}")
|
|
66
|
+
try:
|
|
67
|
+
new_content = self.io.read_line(f'{num} ').rstrip()
|
|
68
|
+
if new_content:
|
|
69
|
+
self.program[num] = new_content
|
|
70
|
+
self.io.writeln(f" UPDATED {num}")
|
|
71
|
+
else:
|
|
72
|
+
self.io.writeln(" (unchanged)")
|
|
73
|
+
except (KeyboardInterrupt, EOFError):
|
|
74
|
+
self.io.writeln("\n (cancelled)")
|
|
75
|
+
|
|
76
|
+
# ── LIST enhancements ──────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
def cmd_list_subs(self) -> None:
|
|
79
|
+
"""LIST SUBS — list all SUB/FUNCTION definitions."""
|
|
80
|
+
found = False
|
|
81
|
+
for ln in sorted(self.program.keys()):
|
|
82
|
+
stmt = self.program[ln].strip().upper()
|
|
83
|
+
if stmt.startswith('SUB ') or stmt.startswith('FUNCTION '):
|
|
84
|
+
self.io.writeln(f" {ln:5d} {self.program[ln]}")
|
|
85
|
+
found = True
|
|
86
|
+
if self.subroutines:
|
|
87
|
+
for name, sub in self.subroutines.items():
|
|
88
|
+
if isinstance(sub, dict):
|
|
89
|
+
params = f"({', '.join(sub['params'])})" if sub['params'] else ""
|
|
90
|
+
self.io.writeln(f" DEF {name}{params} ({len(sub['body'])} gates)")
|
|
91
|
+
found = True
|
|
92
|
+
if not found:
|
|
93
|
+
self.io.writeln(" No subroutines defined")
|
|
94
|
+
|
|
95
|
+
def cmd_list_vars(self) -> None:
|
|
96
|
+
"""LIST VARS — alias for VARS with types."""
|
|
97
|
+
if not self.variables:
|
|
98
|
+
self.io.writeln(" No variables")
|
|
99
|
+
return
|
|
100
|
+
for name, val in sorted(self.variables.items()):
|
|
101
|
+
typ = type(val).__name__
|
|
102
|
+
self.io.writeln(f" {name} = {val} ({typ})")
|
|
103
|
+
|
|
104
|
+
def cmd_list_arrays(self) -> None:
|
|
105
|
+
"""LIST ARRAYS — list all arrays with sizes."""
|
|
106
|
+
if not self.arrays:
|
|
107
|
+
self.io.writeln(" No arrays")
|
|
108
|
+
return
|
|
109
|
+
for name, data in sorted(self.arrays.items()):
|
|
110
|
+
if isinstance(data, list):
|
|
111
|
+
self.io.writeln(f" {name}({len(data)}) = {data[:5]}{'...' if len(data) > 5 else ''}")
|
|
112
|
+
elif isinstance(data, dict):
|
|
113
|
+
self.io.writeln(f" {name}({len(data)} dims)")
|
|
114
|
+
|
|
115
|
+
# ── Program slots (BANK) ──────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
def cmd_bank(self, rest: str) -> None:
|
|
118
|
+
"""BANK <n> — switch to program slot n."""
|
|
119
|
+
if not rest.strip():
|
|
120
|
+
self.io.writeln(f" Current slot: {self._current_slot}")
|
|
121
|
+
self.io.writeln(f" Slots in use: {sorted(self._program_slots.keys())}")
|
|
122
|
+
return
|
|
123
|
+
slot = int(rest.strip())
|
|
124
|
+
# Save current
|
|
125
|
+
self._program_slots[self._current_slot] = dict(self.program)
|
|
126
|
+
# Load target
|
|
127
|
+
self._current_slot = slot
|
|
128
|
+
if slot in self._program_slots:
|
|
129
|
+
self.program = dict(self._program_slots[slot])
|
|
130
|
+
self.io.writeln(f"BANK {slot} ({len(self.program)} lines)")
|
|
131
|
+
else:
|
|
132
|
+
self.program = {}
|
|
133
|
+
self.io.writeln(f"BANK {slot} (empty)")
|
|
134
|
+
|
|
135
|
+
# ── COPY / MOVE ────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
def cmd_copy(self, rest: str) -> None:
|
|
138
|
+
"""COPY <start>-<end> TO <dest> — copy line range."""
|
|
139
|
+
m = self._parse_range_to(rest)
|
|
140
|
+
if not m:
|
|
141
|
+
self.io.writeln("?USAGE: COPY <start>-<end> TO <dest>")
|
|
142
|
+
return
|
|
143
|
+
start, end, dest = m
|
|
144
|
+
offset = dest - start
|
|
145
|
+
for ln in sorted(self.program.keys()):
|
|
146
|
+
if start <= ln <= end:
|
|
147
|
+
self.program[ln + offset] = self.program[ln]
|
|
148
|
+
self.io.writeln(f"COPIED {start}-{end} TO {dest}-{end + offset}")
|
|
149
|
+
|
|
150
|
+
def cmd_move(self, rest: str) -> None:
|
|
151
|
+
"""MOVE <start>-<end> TO <dest> — move line range."""
|
|
152
|
+
m = self._parse_range_to(rest)
|
|
153
|
+
if not m:
|
|
154
|
+
self.io.writeln("?USAGE: MOVE <start>-<end> TO <dest>")
|
|
155
|
+
return
|
|
156
|
+
start, end, dest = m
|
|
157
|
+
offset = dest - start
|
|
158
|
+
lines = {ln: self.program[ln] for ln in sorted(self.program.keys())
|
|
159
|
+
if start <= ln <= end}
|
|
160
|
+
for ln in lines:
|
|
161
|
+
del self.program[ln]
|
|
162
|
+
for ln, stmt in lines.items():
|
|
163
|
+
self.program[ln + offset] = stmt
|
|
164
|
+
self.io.writeln(f"MOVED {start}-{end} TO {dest}-{end + offset}")
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def _parse_range_to(rest: str) -> tuple[int, int, int] | None:
|
|
168
|
+
m = re.match(r'(\d+)\s*-\s*(\d+)\s+TO\s+(\d+)', rest, re.IGNORECASE)
|
|
169
|
+
if not m:
|
|
170
|
+
return None
|
|
171
|
+
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
|
172
|
+
|
|
173
|
+
# ── FIND / REPLACE ─────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
def cmd_find(self, rest: str) -> None:
|
|
176
|
+
"""FIND "text" — search program lines."""
|
|
177
|
+
text = rest.strip().strip('"').strip("'")
|
|
178
|
+
if not text:
|
|
179
|
+
self.io.writeln("?USAGE: FIND \"text\"")
|
|
180
|
+
return
|
|
181
|
+
found = 0
|
|
182
|
+
for ln in sorted(self.program.keys()):
|
|
183
|
+
if text.upper() in self.program[ln].upper():
|
|
184
|
+
self.io.writeln(f" {ln:5d} {self.program[ln]}")
|
|
185
|
+
found += 1
|
|
186
|
+
self.io.writeln(f" {found} match(es)")
|
|
187
|
+
|
|
188
|
+
def cmd_replace(self, rest: str) -> None:
|
|
189
|
+
"""REPLACE "old" WITH "new" — find and replace in program."""
|
|
190
|
+
m = re.match(r'"([^"]+)"\s+WITH\s+"([^"]*)"', rest, re.IGNORECASE)
|
|
191
|
+
if not m:
|
|
192
|
+
self.io.writeln('?USAGE: REPLACE "old" WITH "new"')
|
|
193
|
+
return
|
|
194
|
+
old, new = m.group(1), m.group(2)
|
|
195
|
+
count = 0
|
|
196
|
+
for ln in sorted(self.program.keys()):
|
|
197
|
+
if old in self.program[ln]:
|
|
198
|
+
self.program[ln] = self.program[ln].replace(old, new)
|
|
199
|
+
self.io.writeln(f" {ln:5d} {self.program[ln]}")
|
|
200
|
+
count += 1
|
|
201
|
+
self.io.writeln(f" {count} replacement(s)")
|
|
202
|
+
|
|
203
|
+
# ── CHECKSUM ───────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
def cmd_checksum(self) -> None:
|
|
206
|
+
"""CHECKSUM — hash of program listing for verification."""
|
|
207
|
+
if not self.program:
|
|
208
|
+
self.io.writeln("?EMPTY PROGRAM")
|
|
209
|
+
return
|
|
210
|
+
content = '\n'.join(f'{ln} {self.program[ln]}'
|
|
211
|
+
for ln in sorted(self.program.keys()))
|
|
212
|
+
h = hashlib.md5(content.encode()).hexdigest()[:8].upper()
|
|
213
|
+
self.io.writeln(f"CHECKSUM: {h} ({len(self.program)} lines)")
|
|
214
|
+
|
|
215
|
+
# ── CHAIN / MERGE ──────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _load_lines_with_defs(lines: list[str], process_fn) -> int:
|
|
219
|
+
"""Iterate stripped lines, handling DEF BEGIN...DEF END blocks.
|
|
220
|
+
|
|
221
|
+
Skips blank lines and # comments. For each DEF BEGIN block, collects
|
|
222
|
+
the body lines and calls process_fn with a synthesized single-line DEF
|
|
223
|
+
command. For all other lines, calls process_fn directly.
|
|
224
|
+
|
|
225
|
+
Returns the count of logical lines/blocks processed (excluding skips).
|
|
226
|
+
"""
|
|
227
|
+
count = 0
|
|
228
|
+
i = 0
|
|
229
|
+
while i < len(lines):
|
|
230
|
+
line = lines[i].strip()
|
|
231
|
+
if not line or line.startswith('#'):
|
|
232
|
+
i += 1
|
|
233
|
+
continue
|
|
234
|
+
if re.match(r'DEF\s+BEGIN\s+', line, re.IGNORECASE):
|
|
235
|
+
dm = RE_DEF_BEGIN.match(line)
|
|
236
|
+
if dm:
|
|
237
|
+
name = dm.group(1).upper()
|
|
238
|
+
params = [p.strip() for p in dm.group(2).split(',')] if dm.group(2) else []
|
|
239
|
+
body = []
|
|
240
|
+
i += 1
|
|
241
|
+
while i < len(lines):
|
|
242
|
+
bl = lines[i].strip()
|
|
243
|
+
if bl.upper() in ('DEF END', 'END'):
|
|
244
|
+
break
|
|
245
|
+
if bl and not bl.startswith('#'):
|
|
246
|
+
body.append(bl)
|
|
247
|
+
i += 1
|
|
248
|
+
body_str = ' : '.join(body)
|
|
249
|
+
param_str = f"({', '.join(params)})" if params else ""
|
|
250
|
+
process_fn(f"DEF {name}{param_str} = {body_str}")
|
|
251
|
+
count += 1
|
|
252
|
+
else:
|
|
253
|
+
process_fn(line)
|
|
254
|
+
count += 1
|
|
255
|
+
i += 1
|
|
256
|
+
return count
|
|
257
|
+
|
|
258
|
+
def cmd_chain(self, rest: str) -> None:
|
|
259
|
+
"""CHAIN "file" — load and run a program, preserving variables."""
|
|
260
|
+
m = RE_CHAIN.match(f"CHAIN {rest}")
|
|
261
|
+
if not m:
|
|
262
|
+
self.io.writeln('?USAGE: CHAIN "filename"')
|
|
263
|
+
return
|
|
264
|
+
path = m.group(1).strip()
|
|
265
|
+
try:
|
|
266
|
+
path = self._sanitize_path(path)
|
|
267
|
+
except ValueError as e:
|
|
268
|
+
self.io.writeln(f"?CHAIN ERROR: {e}")
|
|
269
|
+
return
|
|
270
|
+
if not path.endswith('.qb'):
|
|
271
|
+
path += '.qb'
|
|
272
|
+
if not os.path.isfile(path):
|
|
273
|
+
self.io.writeln(f"?FILE NOT FOUND: {path}")
|
|
274
|
+
return
|
|
275
|
+
saved_vars = dict(self.variables)
|
|
276
|
+
self.program.clear()
|
|
277
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
278
|
+
lines = [l.rstrip('\n\r') for l in f.readlines()]
|
|
279
|
+
self._load_lines_with_defs(
|
|
280
|
+
lines, lambda line: self.process(line, track_undo=False))
|
|
281
|
+
self.variables.update(saved_vars)
|
|
282
|
+
self.io.writeln(f"CHAINED {path}")
|
|
283
|
+
if self.program:
|
|
284
|
+
self.cmd_run()
|
|
285
|
+
|
|
286
|
+
def cmd_merge(self, rest: str) -> None:
|
|
287
|
+
"""MERGE "file" — merge lines from another file without clearing."""
|
|
288
|
+
m = RE_MERGE.match(f"MERGE {rest}")
|
|
289
|
+
if not m:
|
|
290
|
+
self.io.writeln('?USAGE: MERGE "filename"')
|
|
291
|
+
return
|
|
292
|
+
path = m.group(1).strip()
|
|
293
|
+
try:
|
|
294
|
+
path = self._sanitize_path(path)
|
|
295
|
+
except ValueError as e:
|
|
296
|
+
self.io.writeln(f"?MERGE ERROR: {e}")
|
|
297
|
+
return
|
|
298
|
+
if not path.endswith('.qb'):
|
|
299
|
+
path += '.qb'
|
|
300
|
+
if not os.path.isfile(path):
|
|
301
|
+
self.io.writeln(f"?FILE NOT FOUND: {path}")
|
|
302
|
+
return
|
|
303
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
304
|
+
lines = [l.rstrip('\n\r') for l in f.readlines()]
|
|
305
|
+
count = self._load_lines_with_defs(
|
|
306
|
+
lines, lambda line: self.process(line, track_undo=False))
|
|
307
|
+
self.io.writeln(f"MERGED {path} ({count} lines)")
|
|
308
|
+
|
|
309
|
+
# ── Introspection ─────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
def cmd_defs(self) -> None:
|
|
312
|
+
"""List all defined subroutines."""
|
|
313
|
+
found = False
|
|
314
|
+
if self.subroutines:
|
|
315
|
+
for name, sub in self.subroutines.items():
|
|
316
|
+
if isinstance(sub, list):
|
|
317
|
+
self.io.writeln(f" {name} = {' : '.join(sub)}")
|
|
318
|
+
else:
|
|
319
|
+
params = f"({', '.join(sub['params'])})" if sub['params'] else ""
|
|
320
|
+
self.io.writeln(f" {name}{params} = {' : '.join(sub['body'])}")
|
|
321
|
+
found = True
|
|
322
|
+
# Also show SUB/FUNCTION blocks in the program
|
|
323
|
+
from qbasic_core.engine import RE_SUB, RE_FUNCTION
|
|
324
|
+
for ln in sorted(self.program.keys()):
|
|
325
|
+
stmt = self.program[ln].strip()
|
|
326
|
+
m = RE_SUB.match(stmt)
|
|
327
|
+
if m:
|
|
328
|
+
self.io.writeln(f" SUB {m.group(1).upper()}({m.group(2) or ''}) at line {ln}")
|
|
329
|
+
found = True
|
|
330
|
+
m = RE_FUNCTION.match(stmt)
|
|
331
|
+
if m:
|
|
332
|
+
self.io.writeln(f" FUNCTION {m.group(1).upper()}({m.group(2) or ''}) at line {ln}")
|
|
333
|
+
found = True
|
|
334
|
+
if not found:
|
|
335
|
+
self.io.writeln("NO SUBROUTINES DEFINED")
|
|
336
|
+
|
|
337
|
+
def cmd_regs(self) -> None:
|
|
338
|
+
"""List all named registers with their qubit ranges."""
|
|
339
|
+
if not self.registers:
|
|
340
|
+
self.io.writeln("NO REGISTERS DEFINED")
|
|
341
|
+
return
|
|
342
|
+
for name, (start, size) in self.registers.items():
|
|
343
|
+
self.io.writeln(f" {name}[0:{size}] -> qubits {start}-{start+size-1}")
|
|
344
|
+
|
|
345
|
+
def cmd_vars(self) -> None:
|
|
346
|
+
"""List all variables and their current values."""
|
|
347
|
+
if not self.variables:
|
|
348
|
+
self.io.writeln("NO VARIABLES SET")
|
|
349
|
+
return
|
|
350
|
+
for name, val in self.variables.items():
|
|
351
|
+
self.io.writeln(f" {name} = {val}")
|
|
352
|
+
|
|
353
|
+
def cmd_clear(self, rest: str) -> None:
|
|
354
|
+
"""CLEAR var — remove a variable. CLEAR ARRAYS — clear all arrays."""
|
|
355
|
+
name = rest.strip()
|
|
356
|
+
if not name:
|
|
357
|
+
self.io.writeln("?USAGE: CLEAR <var> or CLEAR ARRAYS")
|
|
358
|
+
return
|
|
359
|
+
if name.upper() == 'ARRAYS':
|
|
360
|
+
self.arrays.clear()
|
|
361
|
+
self.io.writeln("ARRAYS CLEARED")
|
|
362
|
+
elif name in self.variables:
|
|
363
|
+
del self.variables[name]
|
|
364
|
+
self.io.writeln(f"CLEARED {name}")
|
|
365
|
+
elif name in self.arrays:
|
|
366
|
+
del self.arrays[name]
|
|
367
|
+
self.io.writeln(f"CLEARED array {name}")
|
|
368
|
+
else:
|
|
369
|
+
self.io.writeln(f"?{name} NOT FOUND")
|
qbasic_core/protocol.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Mixin contract — documents what QBasicTerminal provides to its mixins.
|
|
2
|
+
|
|
3
|
+
Mixins (ExpressionMixin, DisplayMixin, LOCCMixin, ControlFlowMixin, DemoMixin)
|
|
4
|
+
rely on attributes and methods defined on QBasicTerminal. This Protocol
|
|
5
|
+
formalizes that contract so that type checkers and future refactors can verify
|
|
6
|
+
it is not broken.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from qbasic_core.protocol import TerminalProtocol
|
|
10
|
+
|
|
11
|
+
Mixins should document ``Requires: TerminalProtocol`` in their class docstring.
|
|
12
|
+
QBasicTerminal implicitly satisfies this protocol by construction.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections import OrderedDict
|
|
18
|
+
from typing import Any, Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@runtime_checkable
|
|
24
|
+
class TerminalProtocol(Protocol):
|
|
25
|
+
"""Attributes and methods that mixins expect on the host terminal."""
|
|
26
|
+
|
|
27
|
+
# ── State ──────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
program: dict[int, str]
|
|
30
|
+
num_qubits: int
|
|
31
|
+
shots: int
|
|
32
|
+
subroutines: dict[str, Any]
|
|
33
|
+
registers: OrderedDict[str, tuple[int, int]]
|
|
34
|
+
variables: dict[str, Any]
|
|
35
|
+
arrays: dict[str, list[float]]
|
|
36
|
+
last_counts: dict[str, int] | None
|
|
37
|
+
last_sv: np.ndarray | None
|
|
38
|
+
last_circuit: Any | None
|
|
39
|
+
step_mode: bool
|
|
40
|
+
sim_method: str
|
|
41
|
+
sim_device: str
|
|
42
|
+
locc: Any | None
|
|
43
|
+
locc_mode: bool
|
|
44
|
+
|
|
45
|
+
_undo_stack: list[dict[int, str]]
|
|
46
|
+
_gosub_stack: list[int]
|
|
47
|
+
_custom_gates: dict[str, np.ndarray]
|
|
48
|
+
_noise_model: Any | None
|
|
49
|
+
_max_iterations: int
|
|
50
|
+
_include_depth: int
|
|
51
|
+
_parsed: dict[int, Any]
|
|
52
|
+
_circuit_cache_key: Any | None
|
|
53
|
+
_circuit_cache: Any | None
|
|
54
|
+
io: Any # IOPort
|
|
55
|
+
|
|
56
|
+
# ── Methods mixins call on the host ────────────────────────────────
|
|
57
|
+
|
|
58
|
+
def _get_parsed(self, line_num: int) -> Any: ...
|
|
59
|
+
def eval_expr(self, expr: str) -> float: ...
|
|
60
|
+
def _safe_eval(self, expr: str, extra_ns: dict[str, Any] | None = None) -> Any: ...
|
|
61
|
+
def _eval_with_vars(self, expr: str, run_vars: dict[str, Any]) -> float: ...
|
|
62
|
+
def _eval_condition(self, cond: str, run_vars: dict[str, Any]) -> bool: ...
|
|
63
|
+
def _gate_info(self, name: str) -> tuple[int, int] | None: ...
|
|
64
|
+
def _resolve_qubit(self, arg: str) -> int: ...
|
|
65
|
+
def _substitute_vars(self, stmt: str, run_vars: dict[str, Any]) -> str: ...
|
|
66
|
+
def _expand_statement(self, stmt: str) -> list[str]: ...
|
|
67
|
+
def _tokenize_gate(self, stmt: str) -> list[str]: ...
|
|
68
|
+
def _parse_syndrome(self, stmt: str, run_vars: dict[str, Any]) -> tuple[str, list[int], str] | None: ...
|
|
69
|
+
def _split_colon_stmts(self, stmt: str) -> list[str]: ...
|
|
70
|
+
def _print_statevector(self, sv: np.ndarray, n_qubits: int | None = None) -> None: ...
|
|
71
|
+
def _print_bloch_single(self, sv: np.ndarray, qubit: int, n_qubits: int | None = None) -> None: ...
|
|
72
|
+
def print_histogram(self, counts: dict[str, int]) -> None: ...
|
|
73
|
+
def cmd_new(self, *, silent: bool = False) -> None: ...
|
|
74
|
+
def cmd_run(self) -> None: ...
|
|
75
|
+
def cmd_list(self, rest: str = '') -> None: ...
|
|
76
|
+
def cmd_locc(self, rest: str) -> None: ...
|
|
77
|
+
def process(self, line: str) -> None: ...
|
qbasic_core/py.typed
ADDED
|
File without changes
|
qbasic_core/scope.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""QBASIC variable scope — unified layered scope model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Scope:
|
|
9
|
+
"""Layered variable scope: persistent + runtime.
|
|
10
|
+
|
|
11
|
+
Reads check runtime first, then persistent.
|
|
12
|
+
Writes mirror to both for backward compatibility with code
|
|
13
|
+
that reads self.variables directly.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, persistent: dict[str, Any]):
|
|
17
|
+
self._persistent = persistent
|
|
18
|
+
self._runtime: dict[str, Any] = {}
|
|
19
|
+
|
|
20
|
+
def get(self, name: str, default: Any = None) -> Any:
|
|
21
|
+
if name in self._runtime:
|
|
22
|
+
return self._runtime[name]
|
|
23
|
+
return self._persistent.get(name, default)
|
|
24
|
+
|
|
25
|
+
def __contains__(self, name: str) -> bool:
|
|
26
|
+
return name in self._runtime or name in self._persistent
|
|
27
|
+
|
|
28
|
+
def __getitem__(self, name: str) -> Any:
|
|
29
|
+
if name in self._runtime:
|
|
30
|
+
return self._runtime[name]
|
|
31
|
+
return self._persistent[name]
|
|
32
|
+
|
|
33
|
+
def __setitem__(self, name: str, value: Any) -> None:
|
|
34
|
+
self._runtime[name] = value
|
|
35
|
+
self._persistent[name] = value
|
|
36
|
+
|
|
37
|
+
def keys(self):
|
|
38
|
+
return set(self._persistent.keys()) | set(self._runtime.keys())
|
|
39
|
+
|
|
40
|
+
def items(self):
|
|
41
|
+
merged = {**self._persistent, **self._runtime}
|
|
42
|
+
return merged.items()
|
|
43
|
+
|
|
44
|
+
def values(self):
|
|
45
|
+
merged = {**self._persistent, **self._runtime}
|
|
46
|
+
return merged.values()
|
|
47
|
+
|
|
48
|
+
def update(self, other):
|
|
49
|
+
if isinstance(other, dict):
|
|
50
|
+
for k, v in other.items():
|
|
51
|
+
self[k] = v
|
|
52
|
+
elif isinstance(other, Scope):
|
|
53
|
+
for k, v in other.items():
|
|
54
|
+
self[k] = v
|
|
55
|
+
|
|
56
|
+
def as_dict(self) -> dict[str, Any]:
|
|
57
|
+
"""Merged view for expression evaluation."""
|
|
58
|
+
return {**self._persistent, **self._runtime}
|
|
59
|
+
|
|
60
|
+
def __delitem__(self, name: str) -> None:
|
|
61
|
+
self._runtime.pop(name, None)
|
|
62
|
+
self._persistent.pop(name, None)
|
|
63
|
+
|
|
64
|
+
def pop(self, name: str, *default):
|
|
65
|
+
# Check runtime first, then persistent, before falling back to default.
|
|
66
|
+
if name in self._runtime:
|
|
67
|
+
val = self._runtime.pop(name)
|
|
68
|
+
self._persistent.pop(name, None)
|
|
69
|
+
return val
|
|
70
|
+
if name in self._persistent:
|
|
71
|
+
return self._persistent.pop(name)
|
|
72
|
+
if default:
|
|
73
|
+
return default[0]
|
|
74
|
+
raise KeyError(name)
|
qbasic_core/screen.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""QBASIC screen control — SCREEN, COLOR, CLS, LOCATE, PLAY, prompt."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ScreenMixin:
|
|
9
|
+
"""Screen control commands for QBasicTerminal.
|
|
10
|
+
|
|
11
|
+
Requires: TerminalProtocol — uses self._screen_mode, self.last_counts,
|
|
12
|
+
self.last_sv, self.last_circuit, self.print_histogram(),
|
|
13
|
+
self._print_statevector(), self._print_bloch_single(),
|
|
14
|
+
self.cmd_density(), self.cmd_circuit().
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def _init_screen(self) -> None:
|
|
18
|
+
self._color_fg: str = ''
|
|
19
|
+
self._color_bg: str = ''
|
|
20
|
+
self._prompt: str = '] '
|
|
21
|
+
|
|
22
|
+
def cmd_screen(self, rest: str) -> None:
|
|
23
|
+
"""SCREEN mode — set display mode.
|
|
24
|
+
0=text 1=histogram 2=statevector 3=Bloch 4=density 5=circuit"""
|
|
25
|
+
if not rest.strip():
|
|
26
|
+
names = {0: 'text', 1: 'histogram', 2: 'statevector',
|
|
27
|
+
3: 'Bloch', 4: 'density', 5: 'circuit'}
|
|
28
|
+
self.io.writeln(f"SCREEN = {self._screen_mode} ({names.get(self._screen_mode, '?')})")
|
|
29
|
+
return
|
|
30
|
+
mode = int(rest.strip())
|
|
31
|
+
if mode < 0 or mode > 5:
|
|
32
|
+
self.io.writeln("?SCREEN 0-5")
|
|
33
|
+
return
|
|
34
|
+
self._screen_mode = mode
|
|
35
|
+
names = {0: 'text', 1: 'histogram', 2: 'statevector',
|
|
36
|
+
3: 'Bloch', 4: 'density', 5: 'circuit'}
|
|
37
|
+
self.io.writeln(f"SCREEN {mode} ({names[mode]})")
|
|
38
|
+
|
|
39
|
+
def _auto_display(self) -> None:
|
|
40
|
+
"""Display results using current SCREEN mode after RUN."""
|
|
41
|
+
mode = self._screen_mode
|
|
42
|
+
if mode <= 1:
|
|
43
|
+
return # text/histogram mode: histogram already shown by cmd_run
|
|
44
|
+
if mode == 2:
|
|
45
|
+
if self.last_sv is not None:
|
|
46
|
+
self._print_statevector(self.last_sv)
|
|
47
|
+
elif mode == 3:
|
|
48
|
+
if self.last_sv is not None:
|
|
49
|
+
from qbasic_core.engine import MAX_BLOCH_DISPLAY
|
|
50
|
+
for q in range(min(self.num_qubits, MAX_BLOCH_DISPLAY)):
|
|
51
|
+
self._print_bloch_single(self.last_sv, q)
|
|
52
|
+
self.io.writeln('')
|
|
53
|
+
elif mode == 4:
|
|
54
|
+
self.cmd_density()
|
|
55
|
+
elif mode == 5:
|
|
56
|
+
self.cmd_circuit()
|
|
57
|
+
|
|
58
|
+
def cmd_color(self, rest: str) -> None:
|
|
59
|
+
"""COLOR foreground[, background] — set terminal colors."""
|
|
60
|
+
from qbasic_core.engine import RE_COLOR
|
|
61
|
+
m = RE_COLOR.match(f"COLOR {rest}")
|
|
62
|
+
if not m:
|
|
63
|
+
self.io.writeln("?USAGE: COLOR <fg>[, <bg>]")
|
|
64
|
+
return
|
|
65
|
+
self._color_fg = m.group(1).lower()
|
|
66
|
+
self._color_bg = m.group(2).lower() if m.group(2) else ''
|
|
67
|
+
# Apply via ANSI if Rich is not available
|
|
68
|
+
colors = {
|
|
69
|
+
'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
|
|
70
|
+
'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37,
|
|
71
|
+
}
|
|
72
|
+
fg_code = colors.get(self._color_fg, 37)
|
|
73
|
+
if self._color_bg:
|
|
74
|
+
bg_code = colors.get(self._color_bg, 40) + 10
|
|
75
|
+
self.io.write(f"\033[{fg_code};{bg_code}m")
|
|
76
|
+
else:
|
|
77
|
+
self.io.write(f"\033[{fg_code}m")
|
|
78
|
+
self.io.writeln(f"COLOR {self._color_fg}" + (f", {self._color_bg}" if self._color_bg else ""))
|
|
79
|
+
|
|
80
|
+
def cmd_cls(self) -> None:
|
|
81
|
+
"""CLS — clear screen."""
|
|
82
|
+
self.io.write('\033[2J\033[H')
|
|
83
|
+
|
|
84
|
+
def cmd_locate(self, rest: str) -> None:
|
|
85
|
+
"""LOCATE row, col — position cursor."""
|
|
86
|
+
from qbasic_core.engine import RE_LOCATE
|
|
87
|
+
m = RE_LOCATE.match(f"LOCATE {rest}")
|
|
88
|
+
if not m:
|
|
89
|
+
self.io.writeln("?USAGE: LOCATE <row>, <col>")
|
|
90
|
+
return
|
|
91
|
+
row, col = int(m.group(1)), int(m.group(2))
|
|
92
|
+
self.io.write(f"\033[{row};{col}H")
|
|
93
|
+
|
|
94
|
+
def cmd_play(self, rest: str = '') -> None:
|
|
95
|
+
"""PLAY — terminal bell/beep."""
|
|
96
|
+
count = 1
|
|
97
|
+
if rest.strip():
|
|
98
|
+
try:
|
|
99
|
+
count = int(rest.strip())
|
|
100
|
+
except ValueError:
|
|
101
|
+
pass
|
|
102
|
+
for _ in range(count):
|
|
103
|
+
self.io.write('\a')
|
|
104
|
+
|
|
105
|
+
def cmd_prompt(self, rest: str) -> None:
|
|
106
|
+
"""PROMPT <string> — set the REPL prompt."""
|
|
107
|
+
if not rest.strip():
|
|
108
|
+
self.io.writeln(f"PROMPT = {self._prompt!r}")
|
|
109
|
+
return
|
|
110
|
+
text = rest.strip()
|
|
111
|
+
if (text.startswith('"') and text.endswith('"')) or \
|
|
112
|
+
(text.startswith("'") and text.endswith("'")):
|
|
113
|
+
text = text[1:-1]
|
|
114
|
+
self._prompt = text
|
|
115
|
+
self.io.writeln(f"PROMPT = {self._prompt!r}")
|