qubasic 0.3.0__tar.gz → 0.3.1__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 (59) hide show
  1. {qubasic-0.3.0 → qubasic-0.3.1}/CHANGELOG.md +13 -0
  2. {qubasic-0.3.0/qubasic.egg-info → qubasic-0.3.1}/PKG-INFO +1 -1
  3. {qubasic-0.3.0 → qubasic-0.3.1}/pyproject.toml +2 -2
  4. {qubasic-0.3.0 → qubasic-0.3.1/qubasic.egg-info}/PKG-INFO +1 -1
  5. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/__init__.py +1 -1
  6. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/classic.py +1 -0
  7. qubasic-0.3.1/qubasic_core/control_flow.py +309 -0
  8. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/debug.py +31 -6
  9. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/display.py +4 -2
  10. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/executor.py +14 -278
  11. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/expression.py +93 -15
  12. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/file_io.py +18 -9
  13. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/help_text.py +1 -3
  14. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/locc_commands.py +0 -35
  15. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/locc_execution.py +6 -2
  16. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/memory.py +34 -19
  17. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/profiler.py +49 -5
  18. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/program_mgmt.py +15 -0
  19. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/subs.py +69 -54
  20. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/sweep.py +2 -1
  21. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/terminal.py +66 -9
  22. qubasic-0.3.0/qubasic_core/control_flow.py +0 -576
  23. {qubasic-0.3.0 → qubasic-0.3.1}/LICENSE +0 -0
  24. {qubasic-0.3.0 → qubasic-0.3.1}/MANIFEST.in +0 -0
  25. {qubasic-0.3.0 → qubasic-0.3.1}/README.md +0 -0
  26. {qubasic-0.3.0 → qubasic-0.3.1}/examples/bell.qb +0 -0
  27. {qubasic-0.3.0 → qubasic-0.3.1}/examples/grover3.qb +0 -0
  28. {qubasic-0.3.0 → qubasic-0.3.1}/examples/locc_teleport.qb +0 -0
  29. {qubasic-0.3.0 → qubasic-0.3.1}/examples/sweep_rx.qb +0 -0
  30. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic.egg-info/SOURCES.txt +0 -0
  31. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic.egg-info/dependency_links.txt +0 -0
  32. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic.egg-info/entry_points.txt +0 -0
  33. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic.egg-info/requires.txt +0 -0
  34. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic.egg-info/top_level.txt +0 -0
  35. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic.py +0 -0
  36. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/__main__.py +0 -0
  37. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/analysis.py +0 -0
  38. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/backend.py +0 -0
  39. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/demos.py +0 -0
  40. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/engine.py +0 -0
  41. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/engine_state.py +0 -0
  42. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/errors.py +0 -0
  43. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/exec_context.py +0 -0
  44. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/gates.py +0 -0
  45. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/io_protocol.py +0 -0
  46. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/locc.py +0 -0
  47. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/locc_display.py +0 -0
  48. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/locc_engine.py +0 -0
  49. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/mock_backend.py +0 -0
  50. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/noise_mixin.py +0 -0
  51. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/parser.py +0 -0
  52. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/patterns.py +0 -0
  53. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/protocol.py +0 -0
  54. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/scope.py +0 -0
  55. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/screen.py +0 -0
  56. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/state_display.py +0 -0
  57. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/statements.py +0 -0
  58. {qubasic-0.3.0 → qubasic-0.3.1}/qubasic_core/strings.py +0 -0
  59. {qubasic-0.3.0 → qubasic-0.3.1}/setup.cfg +0 -0
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 (2026-03-29)
4
+
5
+ - Fix f-string backslash escapes that broke import on Python 3.10/3.11
6
+
7
+ ## 0.3.0 (2026-03-28)
8
+
9
+ - FUNCTION return value fix, APPLY_CIRCUIT in programs, stabilizer fallback
10
+ - Bump to 0.3.0
11
+
12
+ ## 0.2.0 (2026-03-28)
13
+
14
+ - Rename qbasic -> qubasic everywhere (PyPI name conflict)
15
+
3
16
  ## 0.1.0 (2026-03-28)
4
17
 
5
18
  Initial PyPI release.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qubasic
3
- Version: 0.3.0
3
+ Version: 0.3.1
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.3.1"
8
8
  description = "Quantum BASIC Interactive Terminal"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -65,7 +65,7 @@ skip_empty = true
65
65
  python_version = "3.10"
66
66
  warn_return_any = true
67
67
  warn_unused_configs = true
68
- disallow_untyped_defs = false
68
+ disallow_untyped_defs = true
69
69
  check_untyped_defs = true
70
70
  warn_redundant_casts = true
71
71
  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.3.1
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.3.1'
32
32
 
33
33
  def __getattr__(name):
34
34
  """Lazy import heavy modules on first access."""
@@ -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."""
@@ -248,10 +248,12 @@ class DisplayMixin:
248
248
 
249
249
  # Labels on the sphere
250
250
  self.io.writeln(f" Qubit {qubit} ({x:.3f}, {y:.3f}, {z:.3f}) {label}")
251
- self.io.writeln(f"{'|0\u27E9':^{W+4}}")
251
+ _ket0 = '|0\u27E9'
252
+ _ket1 = '|1\u27E9'
253
+ self.io.writeln(f"{_ket0:^{W+4}}")
252
254
  for row in grid:
253
255
  self.io.writeln(f" {''.join(row)}")
254
- self.io.writeln(f"{'|1\u27E9':^{W+4}}")
256
+ self.io.writeln(f"{_ket1:^{W+4}}")
255
257
 
256
258
  def _bloch_vector(self, sv, qubit, n_qubits=None):
257
259
  """Compute the Bloch vector for a single qubit from the statevector."""