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.
Files changed (60) hide show
  1. qubasic-0.4.0/CHANGELOG.md +63 -0
  2. {qubasic-0.3.0/qubasic.egg-info → qubasic-0.4.0}/PKG-INFO +1 -1
  3. {qubasic-0.3.0 → qubasic-0.4.0}/pyproject.toml +5 -2
  4. {qubasic-0.3.0 → qubasic-0.4.0/qubasic.egg-info}/PKG-INFO +1 -1
  5. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/__init__.py +1 -1
  6. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/analysis.py +95 -13
  7. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/classic.py +1 -0
  8. qubasic-0.4.0/qubasic_core/control_flow.py +309 -0
  9. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/debug.py +31 -6
  10. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/demos.py +64 -1
  11. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/display.py +4 -2
  12. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/engine_state.py +2 -0
  13. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/executor.py +15 -280
  14. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/expression.py +93 -15
  15. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/file_io.py +18 -9
  16. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/help_text.py +1 -3
  17. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/locc_commands.py +49 -36
  18. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/locc_engine.py +44 -3
  19. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/locc_execution.py +51 -3
  20. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/memory.py +46 -21
  21. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/mock_backend.py +2 -0
  22. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/noise_mixin.py +24 -0
  23. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/profiler.py +49 -5
  24. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/program_mgmt.py +15 -0
  25. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/state_display.py +16 -14
  26. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/subs.py +69 -54
  27. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/sweep.py +3 -2
  28. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/terminal.py +598 -196
  29. qubasic-0.3.0/CHANGELOG.md +0 -21
  30. qubasic-0.3.0/qubasic_core/control_flow.py +0 -576
  31. {qubasic-0.3.0 → qubasic-0.4.0}/LICENSE +0 -0
  32. {qubasic-0.3.0 → qubasic-0.4.0}/MANIFEST.in +0 -0
  33. {qubasic-0.3.0 → qubasic-0.4.0}/README.md +0 -0
  34. {qubasic-0.3.0 → qubasic-0.4.0}/examples/bell.qb +0 -0
  35. {qubasic-0.3.0 → qubasic-0.4.0}/examples/grover3.qb +0 -0
  36. {qubasic-0.3.0 → qubasic-0.4.0}/examples/locc_teleport.qb +0 -0
  37. {qubasic-0.3.0 → qubasic-0.4.0}/examples/sweep_rx.qb +0 -0
  38. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.egg-info/SOURCES.txt +0 -0
  39. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.egg-info/dependency_links.txt +0 -0
  40. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.egg-info/entry_points.txt +0 -0
  41. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.egg-info/requires.txt +0 -0
  42. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.egg-info/top_level.txt +0 -0
  43. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic.py +0 -0
  44. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/__main__.py +0 -0
  45. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/backend.py +0 -0
  46. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/engine.py +0 -0
  47. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/errors.py +0 -0
  48. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/exec_context.py +0 -0
  49. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/gates.py +0 -0
  50. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/io_protocol.py +0 -0
  51. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/locc.py +0 -0
  52. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/locc_display.py +0 -0
  53. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/parser.py +0 -0
  54. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/patterns.py +0 -0
  55. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/protocol.py +0 -0
  56. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/scope.py +0 -0
  57. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/screen.py +0 -0
  58. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/statements.py +0 -0
  59. {qubasic-0.3.0 → qubasic-0.4.0}/qubasic_core/strings.py +0 -0
  60. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qubasic"
7
- version = "0.3.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 = false
71
+ disallow_untyped_defs = true
69
72
  check_untyped_defs = true
70
73
  warn_redundant_casts = true
71
74
  warn_unused_ignores = true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Quantum BASIC Interactive Terminal
5
5
  Author-email: "Charles C. Norton" <machineelv@gmail.com>
6
6
  License-Expression: MIT
@@ -28,7 +28,7 @@ __all__ = [
28
28
  'GATE_TABLE', 'GATE_ALIASES',
29
29
  ]
30
30
 
31
- __version__ = '0.3.0'
31
+ __version__ = '0.4.0'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -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
- if self.last_sv is None:
29
- self.io.writeln("?NO STATE RUN first")
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
- sv = Statevector(np.ascontiguousarray(self.last_sv).ravel())
41
- # Build Pauli string for full system
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[self.num_qubits - 1 - qubits[i]] = p
49
+ full_pauli[n - 1 - qubits[i]] = p
46
50
  op = SparsePauliOp(''.join(full_pauli))
47
- val = sv.expectation_value(op)
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
- if self.last_sv is None:
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.num_qubits
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(self.last_sv).ravel())
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
- if self.last_sv is None:
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(self.last_sv).ravel()
90
+ sv = np.ascontiguousarray(sv).ravel()
85
91
  rho = np.outer(sv, sv.conj())
86
- n = self.num_qubits
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 = 1000
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
- if self.last_sv is not None and self.num_qubits <= 16:
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> — set/clear/list breakpoints."""
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 == 'CLEAR':
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."""