qubasic 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- qubasic-0.4.0/CHANGELOG.md +63 -0
- {qubasic-0.3.0/qubasic.egg-info → qubasic-0.4.0}/PKG-INFO +1 -1
- {qubasic-0.3.0 → qubasic-0.4.0}/pyproject.toml +5 -2
- {qubasic-0.3.0 → qubasic-0.4.0/qubasic.egg-info}/PKG-INFO +1 -1
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/__init__.py +1 -1
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/analysis.py +95 -13
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/classic.py +1 -0
- qubasic-0.4.0/qubasic_core/control_flow.py +309 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/debug.py +31 -6
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/demos.py +64 -1
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/display.py +4 -2
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/engine_state.py +2 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/executor.py +15 -280
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/expression.py +93 -15
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/file_io.py +18 -9
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/help_text.py +1 -3
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/locc_commands.py +49 -36
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/locc_engine.py +44 -3
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/locc_execution.py +51 -3
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/memory.py +46 -21
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/mock_backend.py +2 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/noise_mixin.py +24 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/profiler.py +49 -5
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/program_mgmt.py +15 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/state_display.py +16 -14
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/subs.py +69 -54
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/sweep.py +3 -2
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/terminal.py +598 -196
- qubasic-0.3.0/CHANGELOG.md +0 -21
- qubasic-0.3.0/qubasic_core/control_flow.py +0 -576
- {qubasic-0.3.0 → qubasic-0.4.0}/LICENSE +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/MANIFEST.in +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/README.md +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/examples/bell.qb +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/examples/grover3.qb +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/examples/locc_teleport.qb +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/examples/sweep_rx.qb +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/__main__.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/backend.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/engine.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/errors.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/gates.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/locc.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/parser.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/patterns.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/protocol.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/scope.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/screen.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/statements.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/strings.py +0 -0
- {qubasic-0.3.0 → qubasic-0.4.0}/setup.cfg +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0 (2026-03-29)
|
|
4
|
+
|
|
5
|
+
- **Noise correctness**: transpile with optimization_level=0 when noisy so gates survive for noise attachment
|
|
6
|
+
- **Noisy statevector**: STATE/BLOCH/DENSITY now reflect the noisy executed state, not the ideal state
|
|
7
|
+
- **LOCC noise**: Monte Carlo depolarizing noise in the numpy LOCC engine with per-shot execution
|
|
8
|
+
- **GPU**: _make_backend centralizes device flag to all execution paths; graceful probe and fallback
|
|
9
|
+
- **cmd_run decomposition**: extracted _run_no_measure, _run_with_fallback, _extract_statevector, _finalize_run, _select_method, _build_backend_opts, _run_kwargs
|
|
10
|
+
- **State consistency**: _active_sv/_active_nqubits unify LOCC and standard paths for all state commands
|
|
11
|
+
- **SPLIT mode**: EXPECT/DENSITY correctly report that per-register commands are needed
|
|
12
|
+
- **Non-depolarizing noise warning**: entering LOCC mode with unsupported noise types warns explicitly
|
|
13
|
+
- SEED command for deterministic reproducible results
|
|
14
|
+
- VERSION command with build ID, simulator versions, and feature flags
|
|
15
|
+
- PROBE command: one-shot exercise of CPU, noise, LOCC, conditional, and combined paths
|
|
16
|
+
- CONSISTENCY command: cross-check SV norm, purity, Bloch vectors, EXPECT, and histogram
|
|
17
|
+
- METHOD capability map: real probing of each method and GPU availability
|
|
18
|
+
- HELP STATUS: tags all 93 commands as native/experimental/partial
|
|
19
|
+
- CATALOG shows backend behind each SYS routine
|
|
20
|
+
- RUN prints method, device, noise params in summary line
|
|
21
|
+
- Demo self-verification: Bell, GHZ, Grover, Deutsch, BV, Superdense auto-check with pass/fail thresholds
|
|
22
|
+
- Teleportation fidelity output with X-basis verification
|
|
23
|
+
- LOCCINFO: entanglement creation, correction log, branch statistics, noise status
|
|
24
|
+
- Method-device pre-check blocks incompatible combinations before execution
|
|
25
|
+
- Runtime errors identify failing subsystem (GPU/noise/stabilizer/MPS)
|
|
26
|
+
- Run manifest captures all execution parameters for replay
|
|
27
|
+
- Correction log in LOCC engine tracks SEND outcomes
|
|
28
|
+
- NOISE INFO prints exact channels, operations, qubits
|
|
29
|
+
- 25 new tests covering noise, LOCC noise, SEED, VERSION, PROBE, CONSISTENCY, demos, state-after-LOCC, manifest, and method-device pre-check (196 total, up from 171)
|
|
30
|
+
- real_sim pytest marker for tests requiring actual Qiskit Aer simulation
|
|
31
|
+
|
|
32
|
+
## 0.3.1 (2026-03-29)
|
|
33
|
+
|
|
34
|
+
- Fix f-string backslash escapes that broke import on Python 3.10/3.11
|
|
35
|
+
|
|
36
|
+
## 0.3.0 (2026-03-28)
|
|
37
|
+
|
|
38
|
+
- FUNCTION return value fix, APPLY_CIRCUIT in programs, stabilizer fallback
|
|
39
|
+
- Bump to 0.3.0
|
|
40
|
+
|
|
41
|
+
## 0.2.0 (2026-03-28)
|
|
42
|
+
|
|
43
|
+
- Rename qbasic -> qubasic everywhere (PyPI name conflict)
|
|
44
|
+
|
|
45
|
+
## 0.1.0 (2026-03-28)
|
|
46
|
+
|
|
47
|
+
Initial PyPI release.
|
|
48
|
+
|
|
49
|
+
- BASIC REPL with line-numbered program editing
|
|
50
|
+
- 30+ quantum gates (H, X, Y, Z, CX, CCX, RX, RY, RZ, CP, SWAP, etc.)
|
|
51
|
+
- LOCC mode: 2-26 party distributed quantum simulation (SPLIT and JOINT)
|
|
52
|
+
- Full BASIC language: FOR/NEXT, WHILE/WEND, DO/LOOP, SELECT CASE, SUB/FUNCTION, IF/THEN/ELSE
|
|
53
|
+
- AST-based expression evaluator (no eval)
|
|
54
|
+
- Noise models: depolarizing, amplitude damping, phase flip, thermal, readout, combined, Pauli, reset
|
|
55
|
+
- Debugging: STEP, TRON/TROFF, breakpoints, watch, time-travel (REWIND/FORWARD), PROFILE
|
|
56
|
+
- Memory map: PEEK/POKE/SYS/DUMP/MAP/MONITOR
|
|
57
|
+
- Analysis: EXPECT, ENTROPY, DENSITY, SWEEP, BENCH, RAM
|
|
58
|
+
- File I/O: SAVE/LOAD/INCLUDE/IMPORT, OPEN/CLOSE/PRINT#/INPUT#, CSV, OpenQASM 3.0 export
|
|
59
|
+
- 12 built-in demos: Bell, GHZ, Grover, QFT, Deutsch-Jozsa, Bernstein-Vazirani, Superdense, Teleport, LOCC
|
|
60
|
+
- JSON output mode for agent/pipeline integration
|
|
61
|
+
- String variable resolution in PRINT (LEFT$, RIGHT$, CHR$, concatenation)
|
|
62
|
+
- DEF FN and parameterized DEF subroutine invocation
|
|
63
|
+
- Trusted publisher CI/CD to PyPI
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qubasic"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Quantum BASIC Interactive Terminal"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -53,6 +53,9 @@ packages = ["qubasic_core"]
|
|
|
53
53
|
[tool.setuptools.data-files]
|
|
54
54
|
"share/qubasic/examples" = ["examples/*.qb"]
|
|
55
55
|
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
markers = ["real_sim: test requires real Qiskit Aer simulation (no mock)"]
|
|
58
|
+
|
|
56
59
|
[tool.coverage.run]
|
|
57
60
|
source = ["qubasic_core"]
|
|
58
61
|
omit = ["*/test_*"]
|
|
@@ -65,7 +68,7 @@ skip_empty = true
|
|
|
65
68
|
python_version = "3.10"
|
|
66
69
|
warn_return_any = true
|
|
67
70
|
warn_unused_configs = true
|
|
68
|
-
disallow_untyped_defs =
|
|
71
|
+
disallow_untyped_defs = true
|
|
69
72
|
check_untyped_defs = true
|
|
70
73
|
warn_redundant_casts = true
|
|
71
74
|
warn_unused_ignores = true
|
|
@@ -25,8 +25,12 @@ class AnalysisMixin:
|
|
|
25
25
|
def cmd_expect(self, rest: str) -> None:
|
|
26
26
|
"""EXPECT <pauli> [qubits] — compute expectation value.
|
|
27
27
|
Examples: EXPECT Z 0, EXPECT ZZ 0 1, EXPECT X 0"""
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
sv = self._active_sv
|
|
29
|
+
if sv is None:
|
|
30
|
+
if self.locc_mode and self.locc and not self.locc.joint:
|
|
31
|
+
self.io.writeln("?SPLIT mode: use STATE A / STATE B for per-register inspection")
|
|
32
|
+
else:
|
|
33
|
+
self.io.writeln("?NO STATE — RUN first")
|
|
30
34
|
return
|
|
31
35
|
parts = rest.split()
|
|
32
36
|
if not parts:
|
|
@@ -35,16 +39,16 @@ class AnalysisMixin:
|
|
|
35
39
|
pauli_str = parts[0].upper()
|
|
36
40
|
qubits = [int(q) for q in parts[1:]] if len(parts) > 1 else list(range(len(pauli_str)))
|
|
37
41
|
|
|
42
|
+
n = self._active_nqubits
|
|
38
43
|
try:
|
|
39
44
|
from qiskit.quantum_info import Statevector, SparsePauliOp
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
full_pauli = ['I'] * self.num_qubits
|
|
45
|
+
sv_q = Statevector(np.ascontiguousarray(sv).ravel())
|
|
46
|
+
full_pauli = ['I'] * n
|
|
43
47
|
for i, p in enumerate(pauli_str):
|
|
44
48
|
if i < len(qubits):
|
|
45
|
-
full_pauli[
|
|
49
|
+
full_pauli[n - 1 - qubits[i]] = p
|
|
46
50
|
op = SparsePauliOp(''.join(full_pauli))
|
|
47
|
-
val =
|
|
51
|
+
val = sv_q.expectation_value(op)
|
|
48
52
|
self.io.writeln(f" <{pauli_str}> on qubits {qubits} = {val.real:.6f}")
|
|
49
53
|
except Exception as e:
|
|
50
54
|
self.io.writeln(f"?EXPECT ERROR: {e}")
|
|
@@ -52,17 +56,18 @@ class AnalysisMixin:
|
|
|
52
56
|
def cmd_entropy(self, rest: str = '') -> None:
|
|
53
57
|
"""ENTROPY [qubits] — entanglement entropy of specified qubits vs rest.
|
|
54
58
|
Examples: ENTROPY 0 | ENTROPY 0 1 | ENTROPY (defaults to qubit 0)"""
|
|
55
|
-
|
|
59
|
+
sv = self._active_sv
|
|
60
|
+
if sv is None:
|
|
56
61
|
self.io.writeln("?NO STATE — RUN first")
|
|
57
62
|
return
|
|
58
63
|
if rest.strip():
|
|
59
64
|
partition_a = [int(q) for q in rest.replace(',', ' ').split() if q.strip()]
|
|
60
65
|
else:
|
|
61
66
|
partition_a = [0]
|
|
62
|
-
n = self.
|
|
67
|
+
n = self._active_nqubits
|
|
63
68
|
try:
|
|
64
69
|
from qiskit.quantum_info import Statevector, entropy, partial_trace
|
|
65
|
-
sv_obj = Statevector(np.ascontiguousarray(
|
|
70
|
+
sv_obj = Statevector(np.ascontiguousarray(sv).ravel())
|
|
66
71
|
keep = partition_a
|
|
67
72
|
rho_a = partial_trace(sv_obj, [q for q in range(n) if q not in keep])
|
|
68
73
|
ent = entropy(rho_a, base=2)
|
|
@@ -78,12 +83,13 @@ class AnalysisMixin:
|
|
|
78
83
|
|
|
79
84
|
def cmd_density(self) -> None:
|
|
80
85
|
"""Show density matrix (or partial trace for small systems)."""
|
|
81
|
-
|
|
86
|
+
sv = self._active_sv
|
|
87
|
+
if sv is None:
|
|
82
88
|
self.io.writeln("?NO STATE — RUN first")
|
|
83
89
|
return
|
|
84
|
-
sv = np.ascontiguousarray(
|
|
90
|
+
sv = np.ascontiguousarray(sv).ravel()
|
|
85
91
|
rho = np.outer(sv, sv.conj())
|
|
86
|
-
n = self.
|
|
92
|
+
n = self._active_nqubits
|
|
87
93
|
dim = 2**n
|
|
88
94
|
if dim > 16:
|
|
89
95
|
self.io.writeln(f" Density matrix: {dim}x{dim} (too large to display)")
|
|
@@ -140,6 +146,82 @@ class AnalysisMixin:
|
|
|
140
146
|
self.io.writeln(f" {n:>8} {method:>20} FAILED: {e}")
|
|
141
147
|
self.io.writeln('')
|
|
142
148
|
|
|
149
|
+
def cmd_consistency(self, rest: str = '') -> None:
|
|
150
|
+
"""CONSISTENCY — cross-check histogram, SV, density, Bloch, and EXPECT."""
|
|
151
|
+
sv = self._active_sv
|
|
152
|
+
if sv is None:
|
|
153
|
+
self.io.writeln("?NO STATE — RUN first")
|
|
154
|
+
return
|
|
155
|
+
n = self._active_nqubits
|
|
156
|
+
sv = np.ascontiguousarray(sv).ravel()
|
|
157
|
+
checks = []
|
|
158
|
+
|
|
159
|
+
# 1. SV normalization
|
|
160
|
+
norm = float(np.sum(np.abs(sv)**2))
|
|
161
|
+
ok = abs(norm - 1.0) < 1e-6
|
|
162
|
+
checks.append(('SV norm == 1', ok, f"{norm:.8f}"))
|
|
163
|
+
|
|
164
|
+
# 2. Density matrix purity
|
|
165
|
+
rho = np.outer(sv, sv.conj())
|
|
166
|
+
purity = float(np.real(np.trace(rho @ rho)))
|
|
167
|
+
ok2 = purity <= 1.0 + 1e-6
|
|
168
|
+
checks.append(('Purity <= 1', ok2, f"{purity:.8f}"))
|
|
169
|
+
|
|
170
|
+
# 3. Bloch vector length <= 1 for each qubit
|
|
171
|
+
bloch_ok = True
|
|
172
|
+
for q in range(min(n, 8)):
|
|
173
|
+
x, y, z = self._bloch_vector(sv, q, n)
|
|
174
|
+
r = (x**2 + y**2 + z**2) ** 0.5
|
|
175
|
+
if r > 1.0 + 1e-4:
|
|
176
|
+
bloch_ok = False
|
|
177
|
+
break
|
|
178
|
+
checks.append(('Bloch |r| <= 1', bloch_ok, ''))
|
|
179
|
+
|
|
180
|
+
# 4. EXPECT Z on qubit 0 matches P(0) - P(1)
|
|
181
|
+
try:
|
|
182
|
+
from qiskit.quantum_info import Statevector, SparsePauliOp
|
|
183
|
+
sv_q = Statevector(sv)
|
|
184
|
+
pauli_z = ['I'] * n
|
|
185
|
+
pauli_z[n - 1] = 'Z'
|
|
186
|
+
op = SparsePauliOp(''.join(pauli_z))
|
|
187
|
+
ez = float(sv_q.expectation_value(op).real)
|
|
188
|
+
# Compare with direct calculation
|
|
189
|
+
sv_t = sv.reshape([2] * n)
|
|
190
|
+
ax = n - 1
|
|
191
|
+
t0 = np.moveaxis(sv_t, ax, 0)[0].flatten()
|
|
192
|
+
t1 = np.moveaxis(sv_t, ax, 0)[1].flatten()
|
|
193
|
+
p0 = float(np.sum(np.abs(t0)**2))
|
|
194
|
+
p1 = float(np.sum(np.abs(t1)**2))
|
|
195
|
+
ez_direct = p0 - p1
|
|
196
|
+
ok4 = abs(ez - ez_direct) < 1e-6
|
|
197
|
+
checks.append(('<Z> consistent', ok4, f"qiskit={ez:.6f} direct={ez_direct:.6f}"))
|
|
198
|
+
except Exception as _e:
|
|
199
|
+
checks.append(('<Z> consistent', None, f"skip: {_e}"))
|
|
200
|
+
|
|
201
|
+
# 5. Histogram vs SV (if counts available)
|
|
202
|
+
if self.last_counts:
|
|
203
|
+
total = sum(self.last_counts.values())
|
|
204
|
+
hist_p0 = self.last_counts.get('0' * n, 0) / total
|
|
205
|
+
sv_p0 = float(np.abs(sv[0])**2)
|
|
206
|
+
# Loose check: histogram is statistical, allow 10% deviation
|
|
207
|
+
ok5 = abs(hist_p0 - sv_p0) < 0.15 or total < 50
|
|
208
|
+
checks.append(('Hist~SV P(|0>)', ok5,
|
|
209
|
+
f"hist={hist_p0:.3f} sv={sv_p0:.3f}"))
|
|
210
|
+
|
|
211
|
+
self.io.writeln(f"\n Consistency checks ({n} qubits):")
|
|
212
|
+
all_pass = True
|
|
213
|
+
for name, ok, detail in checks:
|
|
214
|
+
if ok is None:
|
|
215
|
+
status = 'SKIP'
|
|
216
|
+
elif ok:
|
|
217
|
+
status = 'PASS'
|
|
218
|
+
else:
|
|
219
|
+
status = 'FAIL'
|
|
220
|
+
all_pass = False
|
|
221
|
+
extra = f" {detail}" if detail else ""
|
|
222
|
+
self.io.writeln(f" {name:25s} {status}{extra}")
|
|
223
|
+
self.io.writeln(f"\n {'ALL CONSISTENT' if all_pass else 'INCONSISTENCY DETECTED'}")
|
|
224
|
+
|
|
143
225
|
def cmd_ram(self) -> None:
|
|
144
226
|
"""RAM — show memory budget, per-instance cost, and parallelism estimates."""
|
|
145
227
|
ram = _get_ram_gb()
|
|
@@ -409,6 +409,7 @@ class ClassicMixin:
|
|
|
409
409
|
# ── OPTION BASE ───────────────────────────────────────────────────
|
|
410
410
|
|
|
411
411
|
def _cf_option_base(self, stmt: str, *, parsed=None) -> tuple[bool, ExecOutcome] | None:
|
|
412
|
+
# Parsed and stored; array indexing always starts at 0 in this implementation.
|
|
412
413
|
if parsed is not None:
|
|
413
414
|
self._option_base = parsed.base
|
|
414
415
|
else:
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Control flow helpers extracted from QBasicTerminal.
|
|
2
|
+
|
|
3
|
+
Requires: TerminalProtocol (see qubasic_core.protocol).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
from qubasic_core.engine import ExecResult, ExecOutcome
|
|
12
|
+
from qubasic_core.parser import parse_stmt
|
|
13
|
+
from qubasic_core.statements import (
|
|
14
|
+
RawStmt, RemStmt, MeasureStmt, EndStmt, ReturnStmt, WendStmt,
|
|
15
|
+
LetArrayStmt, LetStmt, PrintStmt, GotoStmt, GosubStmt,
|
|
16
|
+
ForStmt, NextStmt, WhileStmt, IfThenStmt,
|
|
17
|
+
DataStmt, ReadStmt, OnGotoStmt, OnGosubStmt,
|
|
18
|
+
SelectCaseStmt, CaseStmt, EndSelectStmt,
|
|
19
|
+
DoStmt, LoopStmt, ExitStmt,
|
|
20
|
+
SwapStmt, DefFnStmt, OptionBaseStmt,
|
|
21
|
+
SubStmt, EndSubStmt, FunctionStmt, EndFunctionStmt, CallStmt,
|
|
22
|
+
LocalStmt, StaticStmt, SharedStmt,
|
|
23
|
+
OnErrorStmt, ResumeStmt, ErrorStmt, AssertStmt, StopStmt,
|
|
24
|
+
OnMeasureStmt, OnTimerStmt,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ControlFlowMixin:
|
|
29
|
+
"""Mixin providing control flow helpers for QBasicTerminal.
|
|
30
|
+
|
|
31
|
+
Requires: TerminalProtocol — uses self.program, self.variables,
|
|
32
|
+
self.arrays, self.locc_mode, self.locc, self._gosub_stack,
|
|
33
|
+
self._eval_with_vars(), self._eval_condition(), self._substitute_vars().
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# ── Control flow helpers (decomposed from _exec_control_flow) ────
|
|
37
|
+
#
|
|
38
|
+
# Each _cf_* method accepts (self, stmt, parsed, ...) where parsed is
|
|
39
|
+
# a required typed Stmt object. The stmt parameter is retained in the
|
|
40
|
+
# signature for compatibility with _exec_control_flow's argument
|
|
41
|
+
# passing but is not used by the methods themselves.
|
|
42
|
+
|
|
43
|
+
def _cf_let_array(self, stmt: str, run_vars: dict[str, Any],
|
|
44
|
+
parsed: LetArrayStmt) -> tuple[bool, ExecOutcome]:
|
|
45
|
+
name, idx_expr, val_expr = parsed.name, parsed.index_expr, parsed.value_expr
|
|
46
|
+
idx = int(self._eval_with_vars(idx_expr, run_vars))
|
|
47
|
+
val = self._eval_with_vars(val_expr, run_vars)
|
|
48
|
+
if name not in self.arrays:
|
|
49
|
+
self.arrays[name] = [0.0] * (idx + 1)
|
|
50
|
+
while idx >= len(self.arrays[name]):
|
|
51
|
+
self.arrays[name].append(0.0)
|
|
52
|
+
self.arrays[name][idx] = val
|
|
53
|
+
return True, ExecResult.ADVANCE
|
|
54
|
+
|
|
55
|
+
def _cf_let_var(self, stmt: str, run_vars: dict[str, Any],
|
|
56
|
+
parsed: LetStmt) -> tuple[bool, ExecOutcome]:
|
|
57
|
+
name, expr = parsed.name, parsed.expr
|
|
58
|
+
val = self._eval_with_vars(expr, run_vars)
|
|
59
|
+
run_vars[name] = val
|
|
60
|
+
self.variables[name] = val
|
|
61
|
+
return True, ExecResult.ADVANCE
|
|
62
|
+
|
|
63
|
+
def _cf_print(self, stmt: str, run_vars: dict[str, Any],
|
|
64
|
+
parsed: PrintStmt) -> tuple[bool, ExecOutcome]:
|
|
65
|
+
raw_expr = parsed.expr
|
|
66
|
+
text = self._substitute_vars(raw_expr.strip(), run_vars)
|
|
67
|
+
# Determine trailing separator: ; suppresses newline, , advances to tab
|
|
68
|
+
suppress_newline = raw_expr.rstrip().endswith(';')
|
|
69
|
+
tab_advance = raw_expr.rstrip().endswith(',')
|
|
70
|
+
if suppress_newline:
|
|
71
|
+
text = text.rstrip().removesuffix(';').rstrip()
|
|
72
|
+
elif tab_advance:
|
|
73
|
+
text = text.rstrip().removesuffix(',').rstrip()
|
|
74
|
+
# Evaluate SPC(n) and TAB(n) inline
|
|
75
|
+
def _replace_spc(m_spc):
|
|
76
|
+
n = int(self._eval_with_vars(m_spc.group(1), run_vars))
|
|
77
|
+
return ' ' * max(0, n)
|
|
78
|
+
def _replace_tab(m_tab):
|
|
79
|
+
n = int(self._eval_with_vars(m_tab.group(1), run_vars))
|
|
80
|
+
return ' ' * max(0, n)
|
|
81
|
+
text = re.sub(r'\bSPC\s*\(([^)]+)\)', _replace_spc, text, flags=re.IGNORECASE)
|
|
82
|
+
text = re.sub(r'\bTAB\s*\(([^)]+)\)', _replace_tab, text, flags=re.IGNORECASE)
|
|
83
|
+
# Evaluate the expression
|
|
84
|
+
if (text.startswith('"') and text.endswith('"')) or \
|
|
85
|
+
(text.startswith("'") and text.endswith("'")):
|
|
86
|
+
output = text[1:-1]
|
|
87
|
+
else:
|
|
88
|
+
try:
|
|
89
|
+
ns = run_vars.as_dict() if hasattr(run_vars, 'as_dict') else dict(run_vars) if not isinstance(run_vars, dict) else run_vars
|
|
90
|
+
result = self._safe_eval(text, extra_ns=ns)
|
|
91
|
+
output = str(result)
|
|
92
|
+
except Exception:
|
|
93
|
+
output = text
|
|
94
|
+
# Output with separator behavior
|
|
95
|
+
if suppress_newline:
|
|
96
|
+
self.io.write(output)
|
|
97
|
+
elif tab_advance:
|
|
98
|
+
col = len(output) % 14
|
|
99
|
+
padding = 14 - col if col > 0 else 14
|
|
100
|
+
self.io.write(output + ' ' * padding)
|
|
101
|
+
else:
|
|
102
|
+
self.io.writeln(output)
|
|
103
|
+
return True, ExecResult.ADVANCE
|
|
104
|
+
|
|
105
|
+
def _cf_goto(self, stmt: str, sorted_lines: list[int],
|
|
106
|
+
parsed: GotoStmt) -> tuple[bool, int]:
|
|
107
|
+
target = parsed.target
|
|
108
|
+
for idx, ln in enumerate(sorted_lines):
|
|
109
|
+
if ln == target:
|
|
110
|
+
return True, idx
|
|
111
|
+
raise RuntimeError(f"GOTO {target}: LINE NOT FOUND")
|
|
112
|
+
|
|
113
|
+
def _cf_gosub(self, stmt: str, sorted_lines: list[int], ip: int,
|
|
114
|
+
parsed: GosubStmt) -> tuple[bool, int]:
|
|
115
|
+
target = parsed.target
|
|
116
|
+
self._gosub_stack.append(ip + 1)
|
|
117
|
+
for idx, ln in enumerate(sorted_lines):
|
|
118
|
+
if ln == target:
|
|
119
|
+
return True, idx
|
|
120
|
+
raise RuntimeError(f"GOSUB {target}: LINE NOT FOUND")
|
|
121
|
+
|
|
122
|
+
def _cf_for(self, stmt: str, run_vars: dict[str, Any], loop_stack: list[dict[str, Any]], ip: int,
|
|
123
|
+
parsed: ForStmt) -> tuple[bool, ExecOutcome]:
|
|
124
|
+
var = parsed.var
|
|
125
|
+
start_expr, end_expr, step_expr = parsed.start_expr, parsed.end_expr, parsed.step_expr
|
|
126
|
+
start = self._eval_with_vars(start_expr, run_vars)
|
|
127
|
+
end = self._eval_with_vars(end_expr, run_vars)
|
|
128
|
+
step = self._eval_with_vars(step_expr, run_vars) if step_expr else 1
|
|
129
|
+
try:
|
|
130
|
+
if start == int(start): start = int(start)
|
|
131
|
+
except (OverflowError, ValueError):
|
|
132
|
+
pass
|
|
133
|
+
try:
|
|
134
|
+
if end == int(end): end = int(end)
|
|
135
|
+
except (OverflowError, ValueError):
|
|
136
|
+
pass
|
|
137
|
+
try:
|
|
138
|
+
if isinstance(step, float) and step == int(step): step = int(step)
|
|
139
|
+
except (OverflowError, ValueError):
|
|
140
|
+
pass
|
|
141
|
+
run_vars[var] = start
|
|
142
|
+
self.variables[var] = start
|
|
143
|
+
loop_stack.append({'var': var, 'current': start, 'end': end,
|
|
144
|
+
'step': step, 'return_ip': ip})
|
|
145
|
+
return True, ExecResult.ADVANCE
|
|
146
|
+
|
|
147
|
+
def _cf_next(self, stmt: str, run_vars: dict[str, Any], loop_stack: list[dict[str, Any]],
|
|
148
|
+
parsed: NextStmt) -> tuple[bool, ExecOutcome]:
|
|
149
|
+
var = parsed.var
|
|
150
|
+
if not loop_stack or loop_stack[-1].get('var') != var:
|
|
151
|
+
if loop_stack:
|
|
152
|
+
expected = loop_stack[-1].get('var', '?')
|
|
153
|
+
raise RuntimeError(f"NEXT {var} does not match current FOR {expected}")
|
|
154
|
+
raise RuntimeError(f"NEXT {var} without matching FOR")
|
|
155
|
+
loop = loop_stack[-1]
|
|
156
|
+
loop['current'] += loop['step']
|
|
157
|
+
if (loop['step'] > 0 and loop['current'] <= loop['end']) or \
|
|
158
|
+
(loop['step'] < 0 and loop['current'] >= loop['end']):
|
|
159
|
+
run_vars[var] = loop['current']
|
|
160
|
+
self.variables[var] = loop['current']
|
|
161
|
+
return True, loop['return_ip'] + 1
|
|
162
|
+
else:
|
|
163
|
+
loop_stack.pop()
|
|
164
|
+
return True, ExecResult.ADVANCE
|
|
165
|
+
|
|
166
|
+
def _find_matching_wend(self, sorted_lines: list[int], ip: int) -> int:
|
|
167
|
+
"""Find the ip after the WEND matching the WHILE at ip.
|
|
168
|
+
|
|
169
|
+
Scans forward with proper nesting depth tracking. Returns the ip
|
|
170
|
+
index past the matching WEND. Raises with the WHILE line number
|
|
171
|
+
for clear diagnostics.
|
|
172
|
+
"""
|
|
173
|
+
depth = 1
|
|
174
|
+
scan = ip + 1
|
|
175
|
+
while scan < len(sorted_lines):
|
|
176
|
+
s = self.program[sorted_lines[scan]].strip().upper()
|
|
177
|
+
if s.startswith('WHILE '):
|
|
178
|
+
depth += 1
|
|
179
|
+
elif s == 'WEND':
|
|
180
|
+
depth -= 1
|
|
181
|
+
if depth == 0:
|
|
182
|
+
return scan + 1
|
|
183
|
+
scan += 1
|
|
184
|
+
line_num = sorted_lines[ip]
|
|
185
|
+
raise RuntimeError(f"WHILE at line {line_num} has no matching WEND")
|
|
186
|
+
|
|
187
|
+
def _cf_while(self, stmt: str, run_vars: dict[str, Any], loop_stack: list[dict[str, Any]],
|
|
188
|
+
sorted_lines: list[int], ip: int,
|
|
189
|
+
parsed: WhileStmt) -> tuple[bool, ExecOutcome]:
|
|
190
|
+
cond = parsed.condition
|
|
191
|
+
if self._eval_condition(cond, run_vars):
|
|
192
|
+
loop_stack.append({'type': 'while', 'cond': cond, 'return_ip': ip})
|
|
193
|
+
return True, ExecResult.ADVANCE
|
|
194
|
+
else:
|
|
195
|
+
return True, self._find_matching_wend(sorted_lines, ip)
|
|
196
|
+
|
|
197
|
+
def _cf_wend(self, run_vars: dict[str, Any], loop_stack: list[dict[str, Any]],
|
|
198
|
+
sorted_lines: list[int] | None = None, ip: int | None = None) -> tuple[bool, ExecOutcome] | None:
|
|
199
|
+
if not loop_stack or loop_stack[-1].get('type') != 'while':
|
|
200
|
+
ctx = f" at line {sorted_lines[ip]}" if sorted_lines and ip is not None else ""
|
|
201
|
+
raise RuntimeError(f"WEND{ctx} without matching WHILE")
|
|
202
|
+
loop = loop_stack[-1]
|
|
203
|
+
if self._eval_condition(loop['cond'], run_vars):
|
|
204
|
+
return True, loop['return_ip']
|
|
205
|
+
else:
|
|
206
|
+
loop_stack.pop()
|
|
207
|
+
return True, ExecResult.ADVANCE
|
|
208
|
+
|
|
209
|
+
def _cf_if_then(self, stmt: str, run_vars: dict[str, Any], loop_stack: list[dict[str, Any]],
|
|
210
|
+
sorted_lines: list[int], ip: int,
|
|
211
|
+
exec_fn: Callable[..., Any],
|
|
212
|
+
parsed: IfThenStmt) -> tuple[bool, ExecOutcome]:
|
|
213
|
+
cond_str = parsed.condition
|
|
214
|
+
then_clause = parsed.then_clause
|
|
215
|
+
else_clause = parsed.else_clause
|
|
216
|
+
cond_vars = run_vars
|
|
217
|
+
if self.locc_mode and self.locc:
|
|
218
|
+
cond_vars = {**run_vars, **self.locc.classical}
|
|
219
|
+
result = ExecResult.ADVANCE
|
|
220
|
+
if self._eval_condition(cond_str, cond_vars):
|
|
221
|
+
if then_clause:
|
|
222
|
+
r = exec_fn(then_clause, loop_stack, sorted_lines, ip, run_vars)
|
|
223
|
+
if r is not None and r is not ExecResult.ADVANCE:
|
|
224
|
+
result = r
|
|
225
|
+
elif else_clause:
|
|
226
|
+
r = exec_fn(else_clause, loop_stack, sorted_lines, ip, run_vars)
|
|
227
|
+
if r is not None and r is not ExecResult.ADVANCE:
|
|
228
|
+
result = r
|
|
229
|
+
return True, result
|
|
230
|
+
|
|
231
|
+
# ── Type-based dispatch table ────────────────────────────────────
|
|
232
|
+
# Maps parsed Stmt types to handler lambdas. Each lambda receives
|
|
233
|
+
# (self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars,
|
|
234
|
+
# exec_fn) and returns (handled: bool, result).
|
|
235
|
+
|
|
236
|
+
_CF_DISPATCH: dict[type, Callable] = {
|
|
237
|
+
# Trivial handlers (no _cf_* method needed)
|
|
238
|
+
RemStmt: lambda s, st, p, ls, sl, ip, rv, ef: (True, ExecResult.ADVANCE),
|
|
239
|
+
MeasureStmt: lambda s, st, p, ls, sl, ip, rv, ef: (True, ExecResult.ADVANCE),
|
|
240
|
+
EndStmt: lambda s, st, p, ls, sl, ip, rv, ef: (True, ExecResult.END),
|
|
241
|
+
ReturnStmt: lambda s, st, p, ls, sl, ip, rv, ef: (_ for _ in ()).throw(RuntimeError("RETURN WITHOUT GOSUB")) if not s._gosub_stack else (True, s._gosub_stack.pop()),
|
|
242
|
+
# Handlers defined in control_flow.py (parsed is positional)
|
|
243
|
+
WendStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_wend(rv, ls, sl, ip),
|
|
244
|
+
LetArrayStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_let_array(st, rv, p),
|
|
245
|
+
LetStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_let_var(st, rv, p),
|
|
246
|
+
PrintStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_print(st, rv, p),
|
|
247
|
+
GotoStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_goto(st, sl, p),
|
|
248
|
+
GosubStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_gosub(st, sl, ip, p),
|
|
249
|
+
ForStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_for(st, rv, ls, ip, p),
|
|
250
|
+
NextStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_next(st, rv, ls, p),
|
|
251
|
+
WhileStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_while(st, rv, ls, sl, ip, p),
|
|
252
|
+
IfThenStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_if_then(st, rv, ls, sl, ip, ef, p),
|
|
253
|
+
# Handlers defined in classic.py (parsed is keyword-only)
|
|
254
|
+
DataStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_data(st, parsed=p),
|
|
255
|
+
ReadStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_read(st, rv, parsed=p),
|
|
256
|
+
OnGotoStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_on_goto(st, rv, sl, parsed=p),
|
|
257
|
+
OnGosubStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_on_gosub(st, rv, sl, ip, parsed=p),
|
|
258
|
+
SelectCaseStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_select_case(st, rv, sl, ip, parsed=p),
|
|
259
|
+
CaseStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_case(st, sl, ip, parsed=p),
|
|
260
|
+
EndSelectStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_end_select(st, parsed=p),
|
|
261
|
+
DoStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_do(st, rv, ls, sl, ip, parsed=p),
|
|
262
|
+
LoopStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_loop(st, rv, ls, sl, ip, parsed=p),
|
|
263
|
+
ExitStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_exit(st, ls, sl, ip, parsed=p),
|
|
264
|
+
SwapStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_swap(st, rv, parsed=p),
|
|
265
|
+
DefFnStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_def_fn(st, rv, parsed=p),
|
|
266
|
+
OptionBaseStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_option_base(st, parsed=p),
|
|
267
|
+
# Handlers defined in subs.py (parsed is keyword-only)
|
|
268
|
+
SubStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_sub(st, sl, ip, parsed=p),
|
|
269
|
+
EndSubStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_end_sub(st, parsed=p),
|
|
270
|
+
FunctionStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_function(st, sl, ip, parsed=p),
|
|
271
|
+
EndFunctionStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_end_function(st, parsed=p),
|
|
272
|
+
CallStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_call(st, rv, sl, ip, parsed=p),
|
|
273
|
+
LocalStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_local(st, rv, parsed=p),
|
|
274
|
+
StaticStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_static(st, rv, parsed=p),
|
|
275
|
+
SharedStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_shared(st, rv, parsed=p),
|
|
276
|
+
# Handlers defined in debug.py (parsed is keyword-only)
|
|
277
|
+
OnErrorStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_on_error(st, parsed=p),
|
|
278
|
+
ResumeStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_resume(st, sl, parsed=p),
|
|
279
|
+
ErrorStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_error(st, parsed=p),
|
|
280
|
+
AssertStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_assert(st, rv, parsed=p),
|
|
281
|
+
StopStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_stop(st, sl, ip, parsed=p),
|
|
282
|
+
OnMeasureStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_on_measure(st, parsed=p),
|
|
283
|
+
OnTimerStmt: lambda s, st, p, ls, sl, ip, rv, ef: s._cf_on_timer(st, parsed=p),
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
def _exec_control_flow(
|
|
287
|
+
self, stmt: str, loop_stack: list[dict[str, Any]],
|
|
288
|
+
sorted_lines: list[int], ip: int, run_vars: dict[str, Any],
|
|
289
|
+
exec_fn: Callable[..., Any],
|
|
290
|
+
*, parsed=None,
|
|
291
|
+
) -> tuple[bool, ExecOutcome | None]:
|
|
292
|
+
"""Shared control flow for both Qiskit and LOCC execution paths.
|
|
293
|
+
Returns (handled, result) -- if handled is True, result is the
|
|
294
|
+
return value. exec_fn is the recursive line executor for
|
|
295
|
+
IF/multi-statement dispatch.
|
|
296
|
+
|
|
297
|
+
Dispatches via dict lookup on the parsed Stmt type (O(1)).
|
|
298
|
+
|
|
299
|
+
If *parsed* is provided, the parse_stmt call is skipped (avoids
|
|
300
|
+
redundant parsing when the caller has already parsed the statement).
|
|
301
|
+
"""
|
|
302
|
+
if parsed is None:
|
|
303
|
+
parsed = parse_stmt(stmt)
|
|
304
|
+
handler = self._CF_DISPATCH.get(type(parsed))
|
|
305
|
+
if handler is not None:
|
|
306
|
+
return handler(self, stmt, parsed, loop_stack, sorted_lines, ip, run_vars, exec_fn)
|
|
307
|
+
|
|
308
|
+
# RawStmt or unmapped type -- not handled by control flow
|
|
309
|
+
return False, None
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
6
|
|
|
7
|
-
MAX_SV_CHECKPOINTS =
|
|
7
|
+
MAX_SV_CHECKPOINTS = 200
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
10
|
from qubasic_core.engine import (
|
|
@@ -131,8 +131,14 @@ class DebugMixin:
|
|
|
131
131
|
# ── Time-travel debugging ─────────────────────────────────────────
|
|
132
132
|
|
|
133
133
|
def _checkpoint_sv(self, line_num: int) -> None:
|
|
134
|
-
"""Save a statevector checkpoint (for small qubit counts).
|
|
135
|
-
|
|
134
|
+
"""Save a statevector checkpoint (for small qubit counts).
|
|
135
|
+
|
|
136
|
+
Only checkpoints systems with <= 12 qubits to keep memory reasonable.
|
|
137
|
+
12 qubits = 2^12 complex128 = ~64KB per checkpoint.
|
|
138
|
+
At MAX_SV_CHECKPOINTS=200 that is ~12MB total, versus ~1GB for 16-qubit
|
|
139
|
+
systems at the old limit.
|
|
140
|
+
"""
|
|
141
|
+
if self.last_sv is not None and self.num_qubits <= 12:
|
|
136
142
|
import numpy as np
|
|
137
143
|
self._sv_checkpoints.append((line_num, np.array(self.last_sv).copy()))
|
|
138
144
|
if len(self._sv_checkpoints) > MAX_SV_CHECKPOINTS:
|
|
@@ -225,7 +231,7 @@ class DebugMixin:
|
|
|
225
231
|
# ── Breakpoints ────────────────────────────────────────────────────
|
|
226
232
|
|
|
227
233
|
def cmd_breakpoint(self, rest: str) -> None:
|
|
228
|
-
"""BREAK <line> —
|
|
234
|
+
"""BREAK <line> | BREAK CLEAR | BREAK LIST | BREAK <start>-<end> — manage breakpoints."""
|
|
229
235
|
if not rest.strip():
|
|
230
236
|
if self._breakpoints:
|
|
231
237
|
self.io.writeln(f" Breakpoints: {sorted(self._breakpoints)}")
|
|
@@ -233,10 +239,29 @@ class DebugMixin:
|
|
|
233
239
|
self.io.writeln(" No breakpoints set")
|
|
234
240
|
return
|
|
235
241
|
rest = rest.strip().upper()
|
|
236
|
-
if rest
|
|
242
|
+
if rest in ('CLEAR', 'ALL'):
|
|
237
243
|
self._breakpoints.clear()
|
|
238
244
|
self.io.writeln("BREAKPOINTS CLEARED")
|
|
239
245
|
return
|
|
246
|
+
if rest == 'LIST':
|
|
247
|
+
if self._breakpoints:
|
|
248
|
+
for bp in sorted(self._breakpoints):
|
|
249
|
+
src = self.program.get(bp, '(no source)')
|
|
250
|
+
self.io.writeln(f" {bp}: {src}")
|
|
251
|
+
else:
|
|
252
|
+
self.io.writeln(" No breakpoints set")
|
|
253
|
+
return
|
|
254
|
+
# Range: BREAK 10-50
|
|
255
|
+
if '-' in rest:
|
|
256
|
+
try:
|
|
257
|
+
a, b = rest.split('-')
|
|
258
|
+
for bp in list(self._breakpoints):
|
|
259
|
+
if int(a) <= bp <= int(b):
|
|
260
|
+
self._breakpoints.discard(bp)
|
|
261
|
+
self.io.writeln(f"BREAKPOINTS CLEARED IN {a}-{b}")
|
|
262
|
+
return
|
|
263
|
+
except ValueError:
|
|
264
|
+
pass
|
|
240
265
|
try:
|
|
241
266
|
line = int(rest)
|
|
242
267
|
if line in self._breakpoints:
|
|
@@ -246,7 +271,7 @@ class DebugMixin:
|
|
|
246
271
|
self._breakpoints.add(line)
|
|
247
272
|
self.io.writeln(f"BREAKPOINT SET: {line}")
|
|
248
273
|
except ValueError:
|
|
249
|
-
self.io.writeln("?USAGE: BREAK <line> | BREAK CLEAR")
|
|
274
|
+
self.io.writeln("?USAGE: BREAK <line> | BREAK <start>-<end> | BREAK CLEAR | BREAK LIST")
|
|
250
275
|
|
|
251
276
|
def _check_breakpoint(self, line_num: int, sorted_lines: list[int], ip: int) -> bool:
|
|
252
277
|
"""Check if we should break at this line. Returns True to stop."""
|