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
qbasic_core/display.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""QBASIC display — histograms, statevector, Bloch sphere rendering."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import math
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from qbasic_core.engine import (
|
|
8
|
+
MAX_HISTOGRAM_STATES, MAX_DISPLAY_AMPLITUDES,
|
|
9
|
+
HISTOGRAM_BAR_WIDTH, AMPLITUDE_THRESHOLD,
|
|
10
|
+
_RICH, _RichTable,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_console():
|
|
15
|
+
"""Get a Rich console bound to current sys.stdout (test-safe)."""
|
|
16
|
+
if _RICH:
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
return Console(file=sys.stdout, highlight=False)
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DisplayMixin:
|
|
23
|
+
"""Display and visualization methods for the QBASIC terminal.
|
|
24
|
+
|
|
25
|
+
Requires: TerminalProtocol — uses self.num_qubits, self.arrays.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def print_histogram(self, counts: dict[str, int]) -> None:
|
|
29
|
+
"""Measurement histogram with optional rich-table formatting."""
|
|
30
|
+
total = sum(counts.values())
|
|
31
|
+
sorted_counts = sorted(counts.items(), key=lambda x: -x[1])
|
|
32
|
+
display = sorted_counts[:MAX_HISTOGRAM_STATES]
|
|
33
|
+
|
|
34
|
+
if _RICH:
|
|
35
|
+
self._print_histogram_rich(display, sorted_counts, total)
|
|
36
|
+
else:
|
|
37
|
+
self._print_histogram_plain(display, sorted_counts, total)
|
|
38
|
+
|
|
39
|
+
def _print_histogram_rich(self, display: list, sorted_counts: list,
|
|
40
|
+
total: int) -> None:
|
|
41
|
+
"""Rich-formatted histogram with colored bars."""
|
|
42
|
+
table = _RichTable(show_header=True, header_style="bold cyan",
|
|
43
|
+
box=None, padding=(0, 1))
|
|
44
|
+
table.add_column("State", justify="right", style="bold")
|
|
45
|
+
table.add_column("Count", justify="right")
|
|
46
|
+
table.add_column("%", justify="right")
|
|
47
|
+
table.add_column("Distribution", min_width=HISTOGRAM_BAR_WIDTH)
|
|
48
|
+
|
|
49
|
+
max_count = max(c for _, c in display) if display else 1
|
|
50
|
+
if len(sorted_counts) > MAX_HISTOGRAM_STATES:
|
|
51
|
+
_get_console().print(
|
|
52
|
+
f"\n [dim]Showing top {MAX_HISTOGRAM_STATES} of "
|
|
53
|
+
f"{len(sorted_counts)} outcomes:[/dim]\n")
|
|
54
|
+
else:
|
|
55
|
+
_get_console().print()
|
|
56
|
+
|
|
57
|
+
for state, count in display:
|
|
58
|
+
pct = 100 * count / total
|
|
59
|
+
bar_len = int(HISTOGRAM_BAR_WIDTH * count / max_count)
|
|
60
|
+
bar = '\u2588' * bar_len
|
|
61
|
+
color = "green" if pct > 40 else "yellow" if pct > 10 else "dim"
|
|
62
|
+
table.add_row(
|
|
63
|
+
f"|{state}\u27E9",
|
|
64
|
+
str(count),
|
|
65
|
+
f"{pct:5.1f}%",
|
|
66
|
+
f"[{color}]{bar}[/{color}]")
|
|
67
|
+
|
|
68
|
+
if len(sorted_counts) > MAX_HISTOGRAM_STATES:
|
|
69
|
+
rest_count = sum(c for _, c in sorted_counts[MAX_HISTOGRAM_STATES:])
|
|
70
|
+
table.add_row(
|
|
71
|
+
"...", str(rest_count),
|
|
72
|
+
f"{100*rest_count/total:5.1f}%", "[dim](remaining)[/dim]")
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
_get_console().print(table)
|
|
76
|
+
_get_console().print()
|
|
77
|
+
except UnicodeEncodeError:
|
|
78
|
+
self._print_histogram_plain(display, sorted_counts, total)
|
|
79
|
+
|
|
80
|
+
def _print_histogram_plain(self, display: list, sorted_counts: list,
|
|
81
|
+
total: int) -> None:
|
|
82
|
+
"""Plain-text histogram (fallback when rich is not available)."""
|
|
83
|
+
if len(sorted_counts) > MAX_HISTOGRAM_STATES:
|
|
84
|
+
self.io.writeln(f"\n Showing top {MAX_HISTOGRAM_STATES} of {len(sorted_counts)} outcomes:\n")
|
|
85
|
+
else:
|
|
86
|
+
self.io.writeln('')
|
|
87
|
+
|
|
88
|
+
max_count = max(c for _, c in display) if display else 1
|
|
89
|
+
max_label = max(len(k) for k, _ in display) if display else 1
|
|
90
|
+
|
|
91
|
+
for state, count in display:
|
|
92
|
+
pct = 100 * count / total
|
|
93
|
+
bar_len = int(HISTOGRAM_BAR_WIDTH * count / max_count)
|
|
94
|
+
bar = '\u2588' * bar_len
|
|
95
|
+
ket = f"|{state}\u27E9"
|
|
96
|
+
self.io.writeln(f" {ket:>{max_label+3}} {count:>6} ({pct:5.1f}%) {bar}")
|
|
97
|
+
|
|
98
|
+
if len(sorted_counts) > MAX_HISTOGRAM_STATES:
|
|
99
|
+
rest_count = sum(c for _, c in sorted_counts[MAX_HISTOGRAM_STATES:])
|
|
100
|
+
self.io.writeln(f" {'...':>{max_label+3}} {rest_count:>6} "
|
|
101
|
+
f"({100*rest_count/total:5.1f}%) (remaining)")
|
|
102
|
+
self.io.writeln('')
|
|
103
|
+
|
|
104
|
+
def _print_statevector(self, sv, n_qubits=None):
|
|
105
|
+
"""Print non-zero amplitudes of the statevector."""
|
|
106
|
+
sv = np.ascontiguousarray(sv).ravel()
|
|
107
|
+
n = n_qubits if n_qubits is not None else self.num_qubits
|
|
108
|
+
|
|
109
|
+
if _RICH:
|
|
110
|
+
table = _RichTable(show_header=True, header_style="bold cyan",
|
|
111
|
+
box=None, padding=(0, 1))
|
|
112
|
+
table.add_column("State", justify="right", style="bold")
|
|
113
|
+
table.add_column("Amplitude", justify="right")
|
|
114
|
+
table.add_column("P", justify="right")
|
|
115
|
+
_get_console().print(f"\n [bold]Statevector ({n} qubits):[/bold]")
|
|
116
|
+
count = 0
|
|
117
|
+
for i, amp in enumerate(sv):
|
|
118
|
+
if abs(amp) > AMPLITUDE_THRESHOLD:
|
|
119
|
+
state = format(i, f'0{n}b')
|
|
120
|
+
prob = abs(amp)**2
|
|
121
|
+
table.add_row(
|
|
122
|
+
f"|{state}\u27E9",
|
|
123
|
+
f"{amp.real:+.4f}{amp.imag:+.4f}j",
|
|
124
|
+
f"{prob:.4f}")
|
|
125
|
+
count += 1
|
|
126
|
+
if count >= MAX_DISPLAY_AMPLITUDES:
|
|
127
|
+
remaining = sum(1 for a in sv[i+1:]
|
|
128
|
+
if abs(a) > AMPLITUDE_THRESHOLD)
|
|
129
|
+
if remaining:
|
|
130
|
+
table.add_row("...", "", f"+{remaining} more")
|
|
131
|
+
break
|
|
132
|
+
try:
|
|
133
|
+
_get_console().print(table)
|
|
134
|
+
_get_console().print()
|
|
135
|
+
return
|
|
136
|
+
except UnicodeEncodeError:
|
|
137
|
+
pass # fall through to plain-text path below
|
|
138
|
+
|
|
139
|
+
self.io.writeln(f"\n Statevector ({n} qubits):")
|
|
140
|
+
count = 0
|
|
141
|
+
for i, amp in enumerate(sv):
|
|
142
|
+
if abs(amp) > AMPLITUDE_THRESHOLD:
|
|
143
|
+
state = format(i, f'0{n}b')
|
|
144
|
+
prob = abs(amp)**2
|
|
145
|
+
self.io.writeln(f" |{state}\u27E9 {amp.real:+.4f}{amp.imag:+.4f}j "
|
|
146
|
+
f"(P={prob:.4f})")
|
|
147
|
+
count += 1
|
|
148
|
+
if count >= MAX_DISPLAY_AMPLITUDES:
|
|
149
|
+
remaining = sum(1 for a in sv[i+1:] if abs(a) > AMPLITUDE_THRESHOLD)
|
|
150
|
+
if remaining:
|
|
151
|
+
self.io.writeln(f" ... and {remaining} more non-zero amplitudes")
|
|
152
|
+
break
|
|
153
|
+
self.io.writeln('')
|
|
154
|
+
|
|
155
|
+
def _print_sv_compact(self, sv):
|
|
156
|
+
"""Compact statevector display for step mode."""
|
|
157
|
+
n = self.num_qubits
|
|
158
|
+
parts = []
|
|
159
|
+
for i, amp in enumerate(sv):
|
|
160
|
+
if abs(amp) > AMPLITUDE_THRESHOLD:
|
|
161
|
+
state = format(i, f'0{n}b')
|
|
162
|
+
if abs(amp.imag) < AMPLITUDE_THRESHOLD:
|
|
163
|
+
parts.append(f"{amp.real:+.3f}|{state}\u27E9")
|
|
164
|
+
else:
|
|
165
|
+
parts.append(f"({amp.real:.2f}{amp.imag:+.2f}j)|{state}\u27E9")
|
|
166
|
+
if len(parts) >= 8:
|
|
167
|
+
parts.append("...")
|
|
168
|
+
break
|
|
169
|
+
self.io.writeln(f" |\u03C8\u27E9 = {' '.join(parts)}")
|
|
170
|
+
|
|
171
|
+
def _print_probs(self, sv):
|
|
172
|
+
"""Print probability distribution with histogram."""
|
|
173
|
+
n = self.num_qubits
|
|
174
|
+
probs = []
|
|
175
|
+
for i, amp in enumerate(sv):
|
|
176
|
+
p = abs(amp)**2
|
|
177
|
+
if p > AMPLITUDE_THRESHOLD:
|
|
178
|
+
state = format(i, f'0{n}b')
|
|
179
|
+
probs.append((state, p))
|
|
180
|
+
|
|
181
|
+
probs.sort(key=lambda x: -x[1])
|
|
182
|
+
display = probs[:MAX_HISTOGRAM_STATES]
|
|
183
|
+
|
|
184
|
+
self.io.writeln(f"\n Probability distribution ({len(probs)} non-zero):\n")
|
|
185
|
+
max_p = max(p for _, p in display) if display else 1
|
|
186
|
+
|
|
187
|
+
for state, p in display:
|
|
188
|
+
bar_len = int(HISTOGRAM_BAR_WIDTH * p / max_p)
|
|
189
|
+
bar = '\u2588' * bar_len
|
|
190
|
+
self.io.writeln(f" |{state}\u27E9 {p*100:6.2f}% {bar}")
|
|
191
|
+
|
|
192
|
+
if len(probs) > MAX_HISTOGRAM_STATES:
|
|
193
|
+
self.io.writeln(f" ... and {len(probs)-MAX_HISTOGRAM_STATES} more states")
|
|
194
|
+
self.io.writeln('')
|
|
195
|
+
|
|
196
|
+
def _print_bloch_single(self, sv, qubit, n_qubits=None):
|
|
197
|
+
"""ASCII Bloch sphere for a single qubit."""
|
|
198
|
+
x, y, z = self._bloch_vector(sv, qubit, n_qubits)
|
|
199
|
+
|
|
200
|
+
# Determine state label
|
|
201
|
+
if math.sqrt(x**2 + y**2 + z**2) < 0.01:
|
|
202
|
+
label = "maximally mixed"
|
|
203
|
+
elif z > 0.99:
|
|
204
|
+
label = "|0\u27E9 (north pole)"
|
|
205
|
+
elif z < -0.99:
|
|
206
|
+
label = "|1\u27E9 (south pole)"
|
|
207
|
+
elif abs(x - 1) < 0.01:
|
|
208
|
+
label = "|+\u27E9"
|
|
209
|
+
elif abs(x + 1) < 0.01:
|
|
210
|
+
label = "|-\u27E9"
|
|
211
|
+
elif abs(y - 1) < 0.01:
|
|
212
|
+
label = "|+i\u27E9"
|
|
213
|
+
elif abs(y + 1) < 0.01:
|
|
214
|
+
label = "|-i\u27E9"
|
|
215
|
+
else:
|
|
216
|
+
theta = math.acos(max(-1, min(1, z)))
|
|
217
|
+
phi = math.atan2(y, x)
|
|
218
|
+
label = f"\u03B8={theta:.2f} \u03C6={phi:.2f}"
|
|
219
|
+
|
|
220
|
+
# Draw ASCII Bloch sphere (XZ plane projection, 15x15)
|
|
221
|
+
R = 6
|
|
222
|
+
W = 2 * R + 1
|
|
223
|
+
grid = [[' '] * W for _ in range(W)]
|
|
224
|
+
cx, cy = R, R
|
|
225
|
+
|
|
226
|
+
# Circle
|
|
227
|
+
for angle in range(360):
|
|
228
|
+
rad = math.radians(angle)
|
|
229
|
+
gx = round(cx + R * math.cos(rad))
|
|
230
|
+
gy = round(cy + R * math.sin(rad))
|
|
231
|
+
if 0 <= gx < W and 0 <= gy < W:
|
|
232
|
+
if grid[gy][gx] == ' ':
|
|
233
|
+
grid[gy][gx] = '\u00B7'
|
|
234
|
+
|
|
235
|
+
# Axes
|
|
236
|
+
for i in range(W):
|
|
237
|
+
if grid[cy][i] == ' ':
|
|
238
|
+
grid[cy][i] = '-'
|
|
239
|
+
if grid[i][cx] == ' ':
|
|
240
|
+
grid[i][cx] = '|'
|
|
241
|
+
grid[cy][cx] = '+'
|
|
242
|
+
|
|
243
|
+
# State point (XZ projection)
|
|
244
|
+
px = round(cx + x * (R - 1))
|
|
245
|
+
pz = round(cy - z * (R - 1))
|
|
246
|
+
if 0 <= px < W and 0 <= pz < W:
|
|
247
|
+
grid[pz][px] = '\u25CF'
|
|
248
|
+
|
|
249
|
+
# Labels on the sphere
|
|
250
|
+
self.io.writeln(f" Qubit {qubit} ({x:.3f}, {y:.3f}, {z:.3f}) {label}")
|
|
251
|
+
self.io.writeln(f"{'|0\u27E9':^{W+4}}")
|
|
252
|
+
for row in grid:
|
|
253
|
+
self.io.writeln(f" {''.join(row)}")
|
|
254
|
+
self.io.writeln(f"{'|1\u27E9':^{W+4}}")
|
|
255
|
+
|
|
256
|
+
def _bloch_vector(self, sv, qubit, n_qubits=None):
|
|
257
|
+
"""Compute the Bloch vector for a single qubit from the statevector."""
|
|
258
|
+
n = n_qubits if n_qubits is not None else self.num_qubits
|
|
259
|
+
sv_arr = np.array(sv).reshape([2] * n)
|
|
260
|
+
|
|
261
|
+
target_axis = n - 1 - qubit
|
|
262
|
+
tensor = np.moveaxis(sv_arr, target_axis, 0)
|
|
263
|
+
t0 = tensor[0].flatten()
|
|
264
|
+
t1 = tensor[1].flatten()
|
|
265
|
+
|
|
266
|
+
rho_00 = np.sum(np.abs(t0)**2)
|
|
267
|
+
rho_11 = np.sum(np.abs(t1)**2)
|
|
268
|
+
rho_01 = np.sum(np.conj(t0) * t1)
|
|
269
|
+
|
|
270
|
+
x = float(2 * rho_01.real)
|
|
271
|
+
y = float(-2 * rho_01.imag)
|
|
272
|
+
z = float(rho_00 - rho_11)
|
|
273
|
+
|
|
274
|
+
return x, y, z
|
qbasic_core/engine.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""QBASIC engine — constants, gate tables, numpy simulation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
import numpy as np
|
|
7
|
+
from qbasic_core.patterns import (
|
|
8
|
+
RE_LINE_NUM, RE_DEF_SINGLE, RE_DEF_BEGIN, RE_REG_INDEX,
|
|
9
|
+
RE_AT_REG, RE_AT_REG_LINE, RE_SEND, RE_SHARE, RE_MEAS, RE_RESET,
|
|
10
|
+
RE_UNITARY, RE_DIM, RE_REDIM, RE_ERASE, RE_GET, RE_INPUT,
|
|
11
|
+
RE_CTRL, RE_INV, RE_LET_ARRAY, RE_LET_VAR, RE_PRINT,
|
|
12
|
+
RE_GOTO, RE_GOSUB, RE_FOR, RE_NEXT, RE_WHILE, RE_IF_THEN,
|
|
13
|
+
RE_GOTO_GOSUB_TARGET, RE_MEASURE_BASIS, RE_SYNDROME,
|
|
14
|
+
RE_DATA, RE_READ, RE_ON_GOTO, RE_ON_GOSUB,
|
|
15
|
+
RE_SELECT_CASE, RE_CASE, RE_DO, RE_LOOP_STMT, RE_EXIT,
|
|
16
|
+
RE_SUB, RE_END_SUB, RE_FUNCTION, RE_END_FUNCTION, RE_CALL,
|
|
17
|
+
RE_LOCAL, RE_STATIC_DECL, RE_SHARED,
|
|
18
|
+
RE_ON_ERROR, RE_RESUME, RE_ERROR_STMT, RE_ASSERT,
|
|
19
|
+
RE_SWAP, RE_POKE, RE_SYS, RE_OPEN, RE_CLOSE,
|
|
20
|
+
RE_PRINT_FILE, RE_INPUT_FILE, RE_LINE_INPUT, RE_OPTION_BASE,
|
|
21
|
+
RE_IMPORT, RE_SAVE_EXPECT, RE_SAVE_PROBS, RE_SAVE_AMPS,
|
|
22
|
+
RE_SET_STATE, RE_TYPE_BEGIN, RE_TYPE_FIELD, RE_END_TYPE, RE_DIM_TYPE,
|
|
23
|
+
RE_CHAIN, RE_MERGE, RE_DEF_FN, RE_PRINT_USING,
|
|
24
|
+
RE_COLOR, RE_LOCATE, RE_SCREEN, RE_LPRINT,
|
|
25
|
+
RE_ON_MEASURE, RE_ON_TIMER, RE_DIM_MULTI, RE_LET_STR,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
29
|
+
# Constants
|
|
30
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
31
|
+
|
|
32
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
33
|
+
# Limits and defaults
|
|
34
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
MAX_QUBITS = 32
|
|
37
|
+
DEFAULT_QUBITS = 4
|
|
38
|
+
DEFAULT_SHOTS = 1024
|
|
39
|
+
MAX_UNDO_STACK = 50
|
|
40
|
+
MAX_LOOP_ITERATIONS = 100_000
|
|
41
|
+
MAX_HISTOGRAM_STATES = 32
|
|
42
|
+
MAX_DISPLAY_AMPLITUDES = 64
|
|
43
|
+
MAX_BLOCH_DISPLAY = 8
|
|
44
|
+
HISTOGRAM_BAR_WIDTH = 35
|
|
45
|
+
AMPLITUDE_THRESHOLD = 1e-8
|
|
46
|
+
MAX_INCLUDE_DEPTH = 8
|
|
47
|
+
# Realistic multiplier for Qiskit Aer statevector simulation:
|
|
48
|
+
# 1x statevector itself (2^n complex128 = 2^n * 16 bytes)
|
|
49
|
+
# ~1x transpilation intermediates and circuit representation
|
|
50
|
+
# ~1x Aer internal copy during simulation and measurement collapse
|
|
51
|
+
# Conservative estimate; actual overhead varies by method and circuit depth.
|
|
52
|
+
OVERHEAD_FACTOR = 3.0
|
|
53
|
+
RAM_BUDGET_FRACTION = 0.8
|
|
54
|
+
|
|
55
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
56
|
+
# Exec-result sentinels
|
|
57
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
58
|
+
# _exec_line and helpers return one of:
|
|
59
|
+
# ExecResult.ADVANCE — advance instruction pointer by one
|
|
60
|
+
# ExecResult.END — stop execution
|
|
61
|
+
# int — jump to that instruction-pointer index
|
|
62
|
+
|
|
63
|
+
class ExecResult(Enum):
|
|
64
|
+
"""Instruction execution result sentinels."""
|
|
65
|
+
ADVANCE = auto()
|
|
66
|
+
END = auto()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# The return type of _exec_line and helpers: ADVANCE, END, or a jump target ip.
|
|
70
|
+
ExecOutcome = ExecResult | int
|
|
71
|
+
|
|
72
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
73
|
+
# Optional rich-terminal packages
|
|
74
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
from rich.console import Console as _RichConsole
|
|
78
|
+
from rich.table import Table as _RichTable
|
|
79
|
+
from rich.panel import Panel as _RichPanel
|
|
80
|
+
from rich.text import Text as _RichText
|
|
81
|
+
_RICH = True
|
|
82
|
+
_console = _RichConsole(highlight=False)
|
|
83
|
+
except ImportError:
|
|
84
|
+
_RICH = False
|
|
85
|
+
_console = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _estimate_gb(n_qubits: int) -> float:
|
|
90
|
+
"""Estimate realistic memory for one n-qubit statevector including overhead."""
|
|
91
|
+
return (2 ** n_qubits) * 16 * OVERHEAD_FACTOR / 1e9
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_ram_gb() -> tuple[float, float] | None:
|
|
95
|
+
"""Return (total_gb, available_gb) or None if psutil is unavailable."""
|
|
96
|
+
try:
|
|
97
|
+
import psutil
|
|
98
|
+
mem = psutil.virtual_memory()
|
|
99
|
+
return mem.total / 1e9, mem.available / 1e9
|
|
100
|
+
except ImportError:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
105
|
+
# Gate matrices, builders, and simulation primitives (re-exported from gates.py)
|
|
106
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
107
|
+
|
|
108
|
+
from qbasic_core.gates import (
|
|
109
|
+
GATE_TABLE, GATE_ALIASES, _np_gate_matrix, _apply_gate_np,
|
|
110
|
+
_measure_np, _sample_np, _sample_one_np, _GATE_BUILDERS,
|
|
111
|
+
_MAT_CX, _MAT_CY, _MAT_CH, _MAT_CCX, _MAT_CSWAP,
|
|
112
|
+
_MAT_H, _MAT_X, _MAT_Y, _MAT_Z, _MAT_S, _MAT_T,
|
|
113
|
+
_MAT_SDG, _MAT_TDG, _MAT_SX, _MAT_ID, _MAT_CZ,
|
|
114
|
+
_MAT_SWAP, _MAT_DCX, _MAT_ISWAP,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
118
|
+
# LOCC engine and constants (re-exported from locc_engine.py)
|
|
119
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
120
|
+
|
|
121
|
+
from qbasic_core.locc_engine import (
|
|
122
|
+
LOCCEngine,
|
|
123
|
+
LOCC_MAX_JOINT_QUBITS, LOCC_MAX_SPLIT_QUBITS, LOCC_MAX_REGISTERS,
|
|
124
|
+
LOCC_SEND_SHOT_CAP, LOCC_SEND_QUBIT_THRESHOLD,
|
|
125
|
+
)
|
|
126
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""QBASIC engine state — standalone execution state container.
|
|
2
|
+
|
|
3
|
+
Extracted from QBasicTerminal to break the god-object. Engine holds all
|
|
4
|
+
program state, variables, arrays, execution configuration, and caches.
|
|
5
|
+
QBasicTerminal composes Engine and adds the REPL shell and command dispatch.
|
|
6
|
+
|
|
7
|
+
Tests and headless callers can use Engine directly without the REPL.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from collections import OrderedDict
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from qbasic_core.engine import (
|
|
17
|
+
DEFAULT_QUBITS, DEFAULT_SHOTS, MAX_LOOP_ITERATIONS, MAX_UNDO_STACK,
|
|
18
|
+
)
|
|
19
|
+
from qbasic_core.io_protocol import StdIOPort, IOPort
|
|
20
|
+
from qbasic_core.parser import parse_stmt
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Engine:
|
|
24
|
+
"""Standalone execution state container.
|
|
25
|
+
|
|
26
|
+
Holds all program state that was previously scattered across
|
|
27
|
+
QBasicTerminal's __init__ and 16 mixin _init_* methods.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, *, io: IOPort | None = None) -> None:
|
|
31
|
+
# Program
|
|
32
|
+
self.program: dict[int, str] = {}
|
|
33
|
+
self._parsed: dict[int, Any] = {}
|
|
34
|
+
self._undo_stack: list[dict[int, str]] = []
|
|
35
|
+
|
|
36
|
+
# Configuration
|
|
37
|
+
self.num_qubits: int = DEFAULT_QUBITS
|
|
38
|
+
self.shots: int = DEFAULT_SHOTS
|
|
39
|
+
self.sim_method: str = 'automatic'
|
|
40
|
+
self.sim_device: str = 'CPU'
|
|
41
|
+
self._noise_model: Any = None
|
|
42
|
+
self._max_iterations: int = MAX_LOOP_ITERATIONS
|
|
43
|
+
self._include_depth: int = 0
|
|
44
|
+
|
|
45
|
+
# Variables and arrays
|
|
46
|
+
self.variables: dict[str, Any] = {}
|
|
47
|
+
self.arrays: dict[str, Any] = {}
|
|
48
|
+
self._array_dims: dict[str, list[int]] = {}
|
|
49
|
+
|
|
50
|
+
# Subroutines and registers
|
|
51
|
+
self.subroutines: dict[str, Any] = {}
|
|
52
|
+
self.registers: OrderedDict[str, tuple[int, int]] = OrderedDict()
|
|
53
|
+
|
|
54
|
+
# Custom gates
|
|
55
|
+
self._custom_gates: dict[str, Any] = {}
|
|
56
|
+
|
|
57
|
+
# Execution state
|
|
58
|
+
self._gosub_stack: list[int] = []
|
|
59
|
+
self.step_mode: bool = False
|
|
60
|
+
self.last_counts: dict[str, int] | None = None
|
|
61
|
+
self.last_sv: Any = None
|
|
62
|
+
self.last_circuit: Any = None
|
|
63
|
+
self._circuit_cache_key: Any = None
|
|
64
|
+
self._circuit_cache: Any = None
|
|
65
|
+
|
|
66
|
+
# LOCC
|
|
67
|
+
self.locc: Any = None
|
|
68
|
+
self.locc_mode: bool = False
|
|
69
|
+
|
|
70
|
+
# I/O
|
|
71
|
+
self.io: IOPort = io or StdIOPort()
|
|
72
|
+
|
|
73
|
+
# Security
|
|
74
|
+
self.agent_mode: bool = False
|
|
75
|
+
self._include_stack: list[str] = []
|
|
76
|
+
|
|
77
|
+
# User-defined types
|
|
78
|
+
self._user_types: dict[str, list[tuple[str, str]]] = {}
|
|
79
|
+
self._pending_type: dict | None = None
|
|
80
|
+
|
|
81
|
+
# Timing
|
|
82
|
+
self._start_time: float = time.time()
|
|
83
|
+
|
|
84
|
+
def _get_parsed(self, line_num: int) -> Any:
|
|
85
|
+
"""Get parsed Stmt for a line, lazily parsing if needed.
|
|
86
|
+
|
|
87
|
+
Re-parses if the program text has changed since the last parse.
|
|
88
|
+
"""
|
|
89
|
+
raw = self.program.get(line_num, '')
|
|
90
|
+
p = self._parsed.get(line_num)
|
|
91
|
+
if p is None or p.raw != raw:
|
|
92
|
+
p = parse_stmt(raw)
|
|
93
|
+
self._parsed[line_num] = p
|
|
94
|
+
return p
|
|
95
|
+
|
|
96
|
+
def clear(self) -> None:
|
|
97
|
+
"""Reset all state (equivalent to cmd_new)."""
|
|
98
|
+
self.program.clear()
|
|
99
|
+
self._parsed.clear()
|
|
100
|
+
self.subroutines.clear()
|
|
101
|
+
self.registers.clear()
|
|
102
|
+
self.variables.clear()
|
|
103
|
+
self.arrays.clear()
|
|
104
|
+
self._array_dims.clear()
|
|
105
|
+
self.last_counts = None
|
|
106
|
+
self.last_sv = None
|
|
107
|
+
self.last_circuit = None
|
|
108
|
+
self._circuit_cache_key = None
|
|
109
|
+
self._circuit_cache = None
|
qbasic_core/errors.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""QBASIC structured error hierarchy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class QBasicError(Exception):
|
|
7
|
+
"""Base for all QBASIC errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, *, code: int = 0, line: int | None = None):
|
|
10
|
+
self.message = message
|
|
11
|
+
self.code = code
|
|
12
|
+
self.line = line
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class QBasicSyntaxError(QBasicError):
|
|
17
|
+
"""Parse-time or syntax error."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class QBasicRuntimeError(QBasicError):
|
|
21
|
+
"""Execution-time error."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class QBasicBuildError(QBasicError):
|
|
25
|
+
"""Circuit/program build error."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class QBasicRangeError(QBasicError):
|
|
29
|
+
"""Value out of range."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class QBasicIOError(QBasicError):
|
|
33
|
+
"""File or I/O error."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class QBasicUndefinedError(QBasicError):
|
|
37
|
+
"""Reference to undefined name."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""QBASIC execution context — unified mutable state for program execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ExecContext:
|
|
11
|
+
"""Mutable execution state passed through the entire call tree."""
|
|
12
|
+
|
|
13
|
+
sorted_lines: list[int]
|
|
14
|
+
ip: int
|
|
15
|
+
run_vars: dict[str, Any]
|
|
16
|
+
loop_stack: list[dict[str, Any]] = field(default_factory=list)
|
|
17
|
+
iteration_count: int = 0
|
|
18
|
+
max_iterations: int = 100_000
|
|
19
|
+
|
|
20
|
+
# Qiskit circuit-build path (None in LOCC path)
|
|
21
|
+
qc: Any = None
|
|
22
|
+
|
|
23
|
+
# Backend abstraction (QiskitBackend or LOCCRegBackend)
|
|
24
|
+
backend: Any = None
|