qubasic 0.1.0__py3-none-any.whl

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