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
@@ -0,0 +1,861 @@
1
+ """QBASIC executor mixin — circuit building and line execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import TYPE_CHECKING
7
+
8
+ import numpy as np
9
+ from qiskit import QuantumCircuit, transpile
10
+ from qiskit_aer import AerSimulator
11
+
12
+ from qbasic_core.engine import (
13
+ GATE_TABLE, GATE_ALIASES,
14
+ ExecResult,
15
+ RE_REG_INDEX, RE_AT_REG_LINE,
16
+ RE_CTRL, RE_INV,
17
+ RE_SYNDROME,
18
+ )
19
+ from qbasic_core.expression import ExpressionMixin
20
+ from qbasic_core.errors import QBasicBuildError, QBasicRangeError
21
+
22
+ if TYPE_CHECKING:
23
+ from qbasic_core.exec_context import ExecContext
24
+
25
+
26
+ class ExecutorMixin:
27
+ """Circuit building and per-line execution for QBasicTerminal.
28
+
29
+ Provides the core execution pipeline: circuit compilation from stored
30
+ programs, single-line execution, gate tokenization, qubit resolution,
31
+ subroutine expansion, and immediate-mode gate application.
32
+
33
+ All methods access shared terminal state (variables, arrays, subroutines,
34
+ registers, etc.) through ``self``.
35
+ """
36
+
37
+ # Names reserved from variable substitution -- derived from source tables
38
+ # so they stay in sync automatically.
39
+ _RESERVED_KEYWORDS = frozenset({
40
+ 'REM', 'MEASURE', 'BARRIER', 'END', 'RETURN',
41
+ 'FOR', 'NEXT', 'WHILE', 'WEND', 'IF', 'THEN', 'ELSE',
42
+ 'GOTO', 'GOSUB', 'LET', 'PRINT', 'INPUT', 'DIM',
43
+ 'AND', 'OR', 'NOT', 'TO', 'STEP', 'SEND', 'SHARE',
44
+ 'MEAS', 'RESET', 'UNITARY', 'CTRL', 'INV',
45
+ })
46
+ _RESERVED_NAMES = (
47
+ set(GATE_TABLE.keys()) | set(GATE_ALIASES.keys()) |
48
+ set(ExpressionMixin._SAFE_CONSTS.keys()) |
49
+ set(ExpressionMixin._SAFE_FUNCS.keys()) |
50
+ _RESERVED_KEYWORDS
51
+ )
52
+
53
+ # Gate dispatch: name -> (method_name, arg_pattern)
54
+ # arg_pattern: 'q' = qubits only, 'pq' = params then qubits, 'ppq' = 3-param then qubit
55
+ _GATE_DISPATCH = {
56
+ 'H': 'h', 'X': 'x', 'Y': 'y', 'Z': 'z',
57
+ 'S': 's', 'T': 't', 'SDG': 'sdg', 'TDG': 'tdg',
58
+ 'SX': 'sx', 'ID': 'id',
59
+ 'CX': 'cx', 'CZ': 'cz', 'CY': 'cy', 'CH': 'ch',
60
+ 'SWAP': 'swap', 'DCX': 'dcx', 'ISWAP': 'iswap',
61
+ 'CCX': 'ccx', 'CSWAP': 'cswap',
62
+ 'RX': 'rx', 'RY': 'ry', 'RZ': 'rz', 'P': 'p',
63
+ 'CRX': 'crx', 'CRY': 'cry', 'CRZ': 'crz', 'CP': 'cp',
64
+ 'RXX': 'rxx', 'RYY': 'ryy', 'RZZ': 'rzz',
65
+ 'U': 'u',
66
+ }
67
+
68
+ # ── Circuit Building ──────────────────────────────────────────────
69
+
70
+ def build_circuit(self) -> tuple['QuantumCircuit', bool]:
71
+ """Compile program lines into a QuantumCircuit. Returns (circuit, has_measure)."""
72
+ from qbasic_core.exec_context import ExecContext
73
+ from qbasic_core.statements import MeasureStmt, CompoundStmt
74
+ from qbasic_core.scope import Scope
75
+ from qbasic_core.backend import QiskitBackend
76
+
77
+ qc = QuantumCircuit(self.num_qubits)
78
+ backend = QiskitBackend(qc, self._apply_gate)
79
+ ctx = ExecContext(
80
+ sorted_lines=sorted(self.program.keys()),
81
+ ip=0,
82
+ run_vars=Scope(self.variables),
83
+ max_iterations=self._max_iterations,
84
+ qc=qc,
85
+ backend=backend,
86
+ )
87
+ has_measure = False
88
+
89
+ while ctx.ip < len(ctx.sorted_lines):
90
+ ctx.iteration_count += 1
91
+ if ctx.iteration_count > ctx.max_iterations:
92
+ raise RuntimeError(f"LOOP LIMIT ({ctx.max_iterations}) — possible infinite loop")
93
+ line_num = ctx.sorted_lines[ctx.ip]
94
+ stmt = self.program[line_num].strip()
95
+ parsed = self._get_parsed(line_num)
96
+
97
+ if isinstance(parsed, MeasureStmt):
98
+ has_measure = True
99
+ ctx.ip += 1
100
+ continue
101
+ if isinstance(parsed, CompoundStmt):
102
+ for part in parsed.parts:
103
+ if part.strip().upper() == 'MEASURE':
104
+ has_measure = True
105
+
106
+ try:
107
+ result = self._exec_line(stmt, parsed=parsed, ctx=ctx)
108
+ except QBasicBuildError as e:
109
+ raise QBasicBuildError(
110
+ f"LINE {line_num}: {e}"
111
+ ) from None
112
+ except Exception as e:
113
+ raise RuntimeError(f"LINE {line_num}: {e}") from None
114
+
115
+ if result is ExecResult.END:
116
+ break
117
+ elif isinstance(result, int):
118
+ ctx.ip = result
119
+ else:
120
+ ctx.ip += 1
121
+
122
+ return qc, has_measure
123
+
124
+ def _exec_line(self, stmt, qc=None, loop_stack=None, sorted_lines=None,
125
+ ip=0, run_vars=None, parsed=None, *, ctx=None):
126
+ """Execute one program line.
127
+
128
+ Accepts either individual parameters (legacy) or ctx (ExecContext).
129
+ When ctx is provided, qc/loop_stack/sorted_lines/ip/run_vars are
130
+ read from ctx and the individual params are ignored.
131
+
132
+ Evaluation order (deterministic, first match wins):
133
+ 1. Typed fast-path (parsed Stmt): BARRIER, REM, MEASURE, END, @REG, compound
134
+ 2. Control flow: LET, PRINT, GOTO, GOSUB, FOR/NEXT, WHILE/WEND, IF/THEN,
135
+ DATA/READ, ON GOTO/GOSUB, SELECT CASE, DO/LOOP, EXIT, SUB/FUNCTION,
136
+ ON ERROR, ASSERT, STOP, SWAP, DEF FN, OPTION BASE
137
+ 3. Statement handlers: MEAS, RESET, MEASURE_X/Y/Z, SYNDROME, UNITARY,
138
+ DIM, REDIM, ERASE, GET, INPUT, POKE, SYS, file I/O, PRINT USING
139
+ 4. Colon-separated compound statements
140
+ 5. Gate application (subroutine expansion + gate dispatch)
141
+
142
+ Returns: int (jump target ip), ExecResult.ADVANCE, or ExecResult.END.
143
+ """
144
+ if ctx is not None:
145
+ qc = ctx.backend.qc if ctx.backend and hasattr(ctx.backend, 'qc') else ctx.qc
146
+ loop_stack = ctx.loop_stack
147
+ sorted_lines = ctx.sorted_lines
148
+ ip = ctx.ip
149
+ run_vars = ctx.run_vars
150
+ from qbasic_core.statements import (
151
+ BarrierStmt, RemStmt, MeasureStmt, EndStmt, ReturnStmt,
152
+ CompoundStmt, AtRegStmt, GotoStmt, GosubStmt,
153
+ ForStmt, NextStmt, WhileStmt, WendStmt, IfThenStmt,
154
+ LetStmt, LetArrayStmt, PrintStmt,
155
+ GateStmt, RawStmt,
156
+ )
157
+
158
+ # 1. Typed fast-path (no regex, no string manipulation)
159
+ if parsed is None:
160
+ from qbasic_core.parser import parse_stmt
161
+ parsed = parse_stmt(stmt)
162
+ if isinstance(parsed, BarrierStmt):
163
+ if hasattr(qc, 'barrier'):
164
+ qc.barrier()
165
+ return ExecResult.ADVANCE
166
+ if isinstance(parsed, (RemStmt, MeasureStmt)):
167
+ return ExecResult.ADVANCE
168
+ if isinstance(parsed, EndStmt):
169
+ return ExecResult.END
170
+ if isinstance(parsed, ReturnStmt):
171
+ if not self._gosub_stack:
172
+ raise RuntimeError("RETURN WITHOUT GOSUB")
173
+ return self._gosub_stack.pop()
174
+ if isinstance(parsed, GotoStmt):
175
+ for idx, ln in enumerate(sorted_lines):
176
+ if ln == parsed.target:
177
+ return idx
178
+ raise RuntimeError(f"GOTO {parsed.target}: LINE NOT FOUND")
179
+ if isinstance(parsed, GosubStmt):
180
+ self._gosub_stack.append(ip + 1)
181
+ for idx, ln in enumerate(sorted_lines):
182
+ if ln == parsed.target:
183
+ return idx
184
+ raise RuntimeError(f"GOSUB {parsed.target}: LINE NOT FOUND")
185
+ if isinstance(parsed, WendStmt):
186
+ _, r = self._cf_wend(run_vars, loop_stack, sorted_lines, ip)
187
+ return r
188
+ if isinstance(parsed, ForStmt):
189
+ start = self._eval_with_vars(parsed.start_expr, run_vars)
190
+ end = self._eval_with_vars(parsed.end_expr, run_vars)
191
+ step = self._eval_with_vars(parsed.step_expr, run_vars) if parsed.step_expr else 1
192
+ try:
193
+ if start == int(start): start = int(start)
194
+ except (OverflowError, ValueError): pass
195
+ try:
196
+ if end == int(end): end = int(end)
197
+ except (OverflowError, ValueError): pass
198
+ try:
199
+ if isinstance(step, float) and step == int(step): step = int(step)
200
+ except (OverflowError, ValueError): pass
201
+ run_vars[parsed.var] = start
202
+ self.variables[parsed.var] = start
203
+ loop_stack.append({'var': parsed.var, 'current': start, 'end': end,
204
+ 'step': step, 'return_ip': ip})
205
+ return ExecResult.ADVANCE
206
+ if isinstance(parsed, NextStmt):
207
+ if not loop_stack or loop_stack[-1].get('var') != parsed.var:
208
+ if loop_stack:
209
+ raise RuntimeError(f"NEXT {parsed.var} does not match current FOR {loop_stack[-1].get('var', '?')}")
210
+ raise RuntimeError(f"NEXT {parsed.var} without matching FOR")
211
+ loop = loop_stack[-1]
212
+ loop['current'] += loop['step']
213
+ if (loop['step'] > 0 and loop['current'] <= loop['end']) or \
214
+ (loop['step'] < 0 and loop['current'] >= loop['end']):
215
+ run_vars[parsed.var] = loop['current']
216
+ self.variables[parsed.var] = loop['current']
217
+ return loop['return_ip'] + 1
218
+ else:
219
+ loop_stack.pop()
220
+ return ExecResult.ADVANCE
221
+ if isinstance(parsed, WhileStmt):
222
+ if self._eval_condition(parsed.condition, run_vars):
223
+ loop_stack.append({'type': 'while', 'cond': parsed.condition, 'return_ip': ip})
224
+ return ExecResult.ADVANCE
225
+ else:
226
+ return self._find_matching_wend(sorted_lines, ip)
227
+ if isinstance(parsed, LetStmt):
228
+ val = self._eval_with_vars(parsed.expr, run_vars)
229
+ run_vars[parsed.name] = val
230
+ self.variables[parsed.name] = val
231
+ return ExecResult.ADVANCE
232
+ if isinstance(parsed, LetArrayStmt):
233
+ idx = int(self._eval_with_vars(parsed.index_expr, run_vars))
234
+ val = self._eval_with_vars(parsed.value_expr, run_vars)
235
+ if parsed.name not in self.arrays:
236
+ self.arrays[parsed.name] = [0.0] * (idx + 1)
237
+ while idx >= len(self.arrays[parsed.name]):
238
+ self.arrays[parsed.name].append(0.0)
239
+ self.arrays[parsed.name][idx] = val
240
+ return ExecResult.ADVANCE
241
+ if isinstance(parsed, PrintStmt):
242
+ text = parsed.expr
243
+ suppress_nl = text.rstrip().endswith(';')
244
+ tab_advance = text.rstrip().endswith(',')
245
+ if suppress_nl:
246
+ text = text.rstrip().removesuffix(';').rstrip()
247
+ elif tab_advance:
248
+ text = text.rstrip().removesuffix(',').rstrip()
249
+ # Quantum PRINT: @REG, QUBIT(n), ENTANGLEMENT(a,b)
250
+ qprint = self._try_quantum_print(text, run_vars)
251
+ if qprint is not None:
252
+ if suppress_nl:
253
+ self.io.write(qprint)
254
+ elif tab_advance:
255
+ col = len(qprint) % 14
256
+ self.io.write(qprint + ' ' * (14 - col if col > 0 else 14))
257
+ else:
258
+ self.io.writeln(qprint)
259
+ return ExecResult.ADVANCE
260
+ # SPC/TAB inline
261
+ def _spc(m_s):
262
+ return ' ' * max(0, int(self._eval_with_vars(m_s.group(1), run_vars)))
263
+ def _tab(m_t):
264
+ return ' ' * max(0, int(self._eval_with_vars(m_t.group(1), run_vars)))
265
+ text = re.sub(r'\bSPC\s*\(([^)]+)\)', _spc, text, flags=re.IGNORECASE)
266
+ text = re.sub(r'\bTAB\s*\(([^)]+)\)', _tab, text, flags=re.IGNORECASE)
267
+ if (text.startswith('"') and text.endswith('"')) or \
268
+ (text.startswith("'") and text.endswith("'")):
269
+ output = text[1:-1]
270
+ else:
271
+ try:
272
+ ns = run_vars.as_dict() if hasattr(run_vars, 'as_dict') else dict(run_vars) if not isinstance(run_vars, dict) else run_vars
273
+ result = self._safe_eval(text, extra_ns=ns)
274
+ output = str(result)
275
+ except Exception:
276
+ output = text
277
+ if suppress_nl:
278
+ self.io.write(output)
279
+ elif tab_advance:
280
+ col = len(output) % 14
281
+ self.io.write(output + ' ' * (14 - col if col > 0 else 14))
282
+ else:
283
+ self.io.writeln(output)
284
+ return ExecResult.ADVANCE
285
+ if isinstance(parsed, IfThenStmt):
286
+ cond_vars = run_vars
287
+ if self.locc_mode and self.locc:
288
+ cond_vars = {**({} if not hasattr(run_vars, 'as_dict') else run_vars.as_dict()),
289
+ **self.locc.classical}
290
+ if hasattr(run_vars, 'as_dict'):
291
+ cond_vars.update(run_vars.as_dict())
292
+ else:
293
+ cond_vars.update(run_vars)
294
+ result = ExecResult.ADVANCE
295
+ if self._eval_condition(parsed.condition, cond_vars):
296
+ if parsed.then_clause:
297
+ r = self._exec_line(parsed.then_clause, qc=qc, loop_stack=loop_stack,
298
+ sorted_lines=sorted_lines, ip=ip, run_vars=run_vars)
299
+ if r is not None and r is not ExecResult.ADVANCE:
300
+ result = r
301
+ elif parsed.else_clause:
302
+ r = self._exec_line(parsed.else_clause, qc=qc, loop_stack=loop_stack,
303
+ sorted_lines=sorted_lines, ip=ip, run_vars=run_vars)
304
+ if r is not None and r is not ExecResult.ADVANCE:
305
+ result = r
306
+ return result
307
+ if isinstance(parsed, AtRegStmt) and not self.locc_mode:
308
+ raise ValueError("@register syntax requires LOCC mode (try: LOCC <n1> <n2>)")
309
+ if isinstance(parsed, CompoundStmt):
310
+ for sub in parsed.parts:
311
+ self._exec_line(sub, qc=qc, loop_stack=loop_stack,
312
+ sorted_lines=sorted_lines, ip=ip, run_vars=run_vars)
313
+ return ExecResult.ADVANCE
314
+
315
+ # 2. Extended typed dispatch -- direct isinstance checks (no dict/lambda overhead)
316
+ from qbasic_core.statements import (
317
+ DataStmt, ReadStmt, OnGotoStmt, OnGosubStmt,
318
+ SelectCaseStmt, CaseStmt, EndSelectStmt,
319
+ DoStmt, LoopStmt, ExitStmt,
320
+ SwapStmt, DefFnStmt, OptionBaseStmt, RestoreStmt,
321
+ SubStmt, EndSubStmt, FunctionStmt, EndFunctionStmt,
322
+ CallStmt, LocalStmt, StaticStmt, SharedStmt,
323
+ OnErrorStmt, ResumeStmt, ErrorStmt, AssertStmt,
324
+ StopStmt, OnMeasureStmt, OnTimerStmt,
325
+ )
326
+ if isinstance(parsed, DataStmt):
327
+ r = self._cf_data(stmt, parsed=parsed)
328
+ if r is not None:
329
+ return r[1]
330
+ if isinstance(parsed, ReadStmt):
331
+ r = self._cf_read(stmt, run_vars, parsed=parsed)
332
+ if r is not None:
333
+ return r[1]
334
+ if isinstance(parsed, OnGotoStmt):
335
+ r = self._cf_on_goto(stmt, run_vars, sorted_lines, parsed=parsed)
336
+ if r is not None:
337
+ return r[1]
338
+ if isinstance(parsed, OnGosubStmt):
339
+ r = self._cf_on_gosub(stmt, run_vars, sorted_lines, ip, parsed=parsed)
340
+ if r is not None:
341
+ return r[1]
342
+ if isinstance(parsed, SelectCaseStmt):
343
+ r = self._cf_select_case(stmt, run_vars, sorted_lines, ip, parsed=parsed)
344
+ if r is not None:
345
+ return r[1]
346
+ if isinstance(parsed, CaseStmt):
347
+ r = self._cf_case(stmt, sorted_lines, ip, parsed=parsed)
348
+ if r is not None:
349
+ return r[1]
350
+ if isinstance(parsed, EndSelectStmt):
351
+ r = self._cf_end_select(stmt, parsed=parsed)
352
+ if r is not None:
353
+ return r[1]
354
+ if isinstance(parsed, DoStmt):
355
+ r = self._cf_do(stmt, run_vars, loop_stack, sorted_lines, ip, parsed=parsed)
356
+ if r is not None:
357
+ return r[1]
358
+ if isinstance(parsed, LoopStmt):
359
+ r = self._cf_loop(stmt, run_vars, loop_stack, sorted_lines, ip, parsed=parsed)
360
+ if r is not None:
361
+ return r[1]
362
+ if isinstance(parsed, ExitStmt):
363
+ r = self._cf_exit(stmt, loop_stack, sorted_lines, ip, parsed=parsed)
364
+ if r is not None:
365
+ return r[1]
366
+ if isinstance(parsed, SwapStmt):
367
+ r = self._cf_swap(stmt, run_vars, parsed=parsed)
368
+ if r is not None:
369
+ return r[1]
370
+ if isinstance(parsed, DefFnStmt):
371
+ r = self._cf_def_fn(stmt, run_vars, parsed=parsed)
372
+ if r is not None:
373
+ return r[1]
374
+ if isinstance(parsed, OptionBaseStmt):
375
+ r = self._cf_option_base(stmt, parsed=parsed)
376
+ if r is not None:
377
+ return r[1]
378
+ if isinstance(parsed, RestoreStmt):
379
+ return ExecResult.ADVANCE
380
+ if isinstance(parsed, SubStmt):
381
+ r = self._cf_sub(stmt, sorted_lines, ip, parsed=parsed)
382
+ if r is not None:
383
+ return r[1]
384
+ if isinstance(parsed, EndSubStmt):
385
+ r = self._cf_end_sub(stmt, parsed=parsed)
386
+ if r is not None:
387
+ return r[1]
388
+ if isinstance(parsed, FunctionStmt):
389
+ r = self._cf_function(stmt, sorted_lines, ip, parsed=parsed)
390
+ if r is not None:
391
+ return r[1]
392
+ if isinstance(parsed, EndFunctionStmt):
393
+ r = self._cf_end_function(stmt, parsed=parsed)
394
+ if r is not None:
395
+ return r[1]
396
+ if isinstance(parsed, CallStmt):
397
+ r = self._cf_call(stmt, run_vars, sorted_lines, ip, parsed=parsed)
398
+ if r is not None:
399
+ return r[1]
400
+ if isinstance(parsed, LocalStmt):
401
+ r = self._cf_local(stmt, run_vars, parsed=parsed)
402
+ if r is not None:
403
+ return r[1]
404
+ if isinstance(parsed, StaticStmt):
405
+ r = self._cf_static(stmt, run_vars, parsed=parsed)
406
+ if r is not None:
407
+ return r[1]
408
+ if isinstance(parsed, SharedStmt):
409
+ r = self._cf_shared(stmt, run_vars, parsed=parsed)
410
+ if r is not None:
411
+ return r[1]
412
+ if isinstance(parsed, OnErrorStmt):
413
+ r = self._cf_on_error(stmt, parsed=parsed)
414
+ if r is not None:
415
+ return r[1]
416
+ if isinstance(parsed, ResumeStmt):
417
+ r = self._cf_resume(stmt, sorted_lines, parsed=parsed)
418
+ if r is not None:
419
+ return r[1]
420
+ if isinstance(parsed, ErrorStmt):
421
+ r = self._cf_error(stmt, parsed=parsed)
422
+ if r is not None:
423
+ return r[1]
424
+ if isinstance(parsed, AssertStmt):
425
+ r = self._cf_assert(stmt, run_vars, parsed=parsed)
426
+ if r is not None:
427
+ return r[1]
428
+ if isinstance(parsed, StopStmt):
429
+ r = self._cf_stop(stmt, sorted_lines, ip, parsed=parsed)
430
+ if r is not None:
431
+ return r[1]
432
+ if isinstance(parsed, OnMeasureStmt):
433
+ r = self._cf_on_measure(stmt, parsed=parsed)
434
+ if r is not None:
435
+ return r[1]
436
+ if isinstance(parsed, OnTimerStmt):
437
+ r = self._cf_on_timer(stmt, parsed=parsed)
438
+ if r is not None:
439
+ return r[1]
440
+
441
+ # 3. Statement handlers
442
+ _backend = ctx.backend if ctx else None
443
+ if self._try_stmt_handlers(stmt, qc, run_vars, backend=_backend):
444
+ return ExecResult.ADVANCE
445
+
446
+ # 4. Colon-separated (legacy fallback for unparsed compound)
447
+ if ':' in stmt:
448
+ for sub in self._split_colon_stmts(stmt):
449
+ self._exec_line(sub, qc=qc, loop_stack=loop_stack,
450
+ sorted_lines=sorted_lines, ip=ip, run_vars=run_vars)
451
+ return ExecResult.ADVANCE
452
+
453
+ # 5. Gate application
454
+ _backend = ctx.backend if ctx else None
455
+
456
+ # Fast path: GateStmt already parsed by the parser
457
+ if isinstance(parsed, GateStmt):
458
+ info = self._gate_info(parsed.name)
459
+ if info is not None:
460
+ n_params, n_qubits = info
461
+ args = list(parsed.args)
462
+ params = [self._eval_with_vars(a, run_vars) for a in args[:n_params]]
463
+ qubits = [self._resolve_qubit(a) for a in args[n_params:n_params + n_qubits]]
464
+ try:
465
+ if _backend:
466
+ _backend.apply_gate(parsed.name, tuple(params), qubits)
467
+ else:
468
+ self._apply_gate(qc, parsed.name, params, qubits)
469
+ except Exception as _gate_err:
470
+ if 'duplicate' in str(_gate_err).lower():
471
+ raise QBasicBuildError(
472
+ f"duplicate qubit arguments in {parsed.name}"
473
+ ) from None
474
+ raise
475
+ return ExecResult.ADVANCE
476
+ # Fall through to _apply_gate_str for custom gates not in GATE_TABLE
477
+
478
+ # Slow path: subroutine expansion + gate dispatch
479
+ expanded = self._expand_statement(stmt)
480
+ for gate_str in expanded:
481
+ self._apply_gate_str(gate_str, qc, backend=_backend)
482
+
483
+ return ExecResult.ADVANCE
484
+
485
+ def _parse_syndrome(self, stmt: str, run_vars: dict) -> tuple[str, list[int], str] | None:
486
+ """Parse SYNDROME statement. Returns (pauli_str, qubits, var) or None."""
487
+ m = RE_SYNDROME.match(stmt)
488
+ if not m:
489
+ return None
490
+ rest = m.group(1).strip()
491
+ parts = rest.split('->')
492
+ if len(parts) != 2:
493
+ raise ValueError("SYNDROME syntax: SYNDROME <paulis> <qubits> -> <var>")
494
+ var = parts[1].strip()
495
+ tokens = parts[0].split()
496
+ if len(tokens) < 2:
497
+ raise ValueError("SYNDROME needs a Pauli string and qubit list")
498
+ pauli_str = tokens[0].upper()
499
+ qubit_args = tokens[1:]
500
+ if len(pauli_str) != len(qubit_args):
501
+ raise ValueError(
502
+ f"Pauli string length ({len(pauli_str)}) must match "
503
+ f"qubit count ({len(qubit_args)})")
504
+ for p in pauli_str:
505
+ if p not in 'IXYZ':
506
+ raise ValueError(f"Unknown Pauli: {p} (use I, X, Y, Z)")
507
+ qubits = [int(self._eval_with_vars(q, run_vars)) for q in qubit_args]
508
+ return pauli_str, qubits, var
509
+
510
+ @staticmethod
511
+ def _split_colon_stmts(stmt: str) -> list[str]:
512
+ """Split colon-separated statements, inheriting @register prefixes."""
513
+ from qbasic_core.parser import _split_colon_stmts
514
+ return _split_colon_stmts(stmt)
515
+
516
+ def _substitute_vars(self, stmt: str, run_vars: dict) -> str:
517
+ """Replace variable names with their values in a statement.
518
+
519
+ Tokenizes the statement and replaces eligible identifiers in-place,
520
+ avoiding substitution inside quoted strings, register notation, and
521
+ protected names (gates, keywords, constants, subroutines, custom gates).
522
+ """
523
+ merged = {**self.variables, **run_vars}
524
+ if not merged:
525
+ return stmt
526
+ # Build the set of names that should never be substituted
527
+ protected = (
528
+ self._RESERVED_NAMES |
529
+ {name.lower() for name in self.registers} |
530
+ {name.upper() for name in self.registers} |
531
+ set(self.subroutines.keys()) |
532
+ set(self._custom_gates.keys())
533
+ )
534
+ # Tokenize: split on word boundaries, preserving delimiters
535
+ tokens = re.split(r'(\b\w+\b)', stmt)
536
+ for i, tok in enumerate(tokens):
537
+ if not tok or not tok[0].isalpha():
538
+ continue
539
+ if tok in protected or tok.upper() in protected or tok.lower() in protected:
540
+ continue
541
+ if tok in merged:
542
+ tokens[i] = str(merged[tok])
543
+ return ''.join(tokens)
544
+
545
+ def _expand_statement(self, stmt, _call_stack: set[str] | None = None):
546
+ """Expand subroutines. Returns list of gate strings.
547
+
548
+ Uses explicit call-stack tracking to detect recursion instead of
549
+ an arbitrary depth counter.
550
+ """
551
+ if _call_stack is None:
552
+ _call_stack = set()
553
+
554
+ # Handle parenthesized subroutine calls: NAME(arg1, arg2) -> NAME arg1, arg2
555
+ m_call = re.match(r'(\w+)\(([^)]*)\)', stmt)
556
+ if m_call:
557
+ call_name = m_call.group(1).upper()
558
+ if call_name in self.subroutines:
559
+ call_args = m_call.group(2)
560
+ stmt = f"{call_name} {call_args}"
561
+
562
+ parts = stmt.split()
563
+ word = parts[0].upper() if parts else ''
564
+
565
+ if word not in self.subroutines:
566
+ return [stmt]
567
+
568
+ if word in _call_stack:
569
+ raise RuntimeError(f"RECURSIVE SUBROUTINE: {word} calls itself")
570
+ _call_stack = _call_stack | {word}
571
+
572
+ sub = self.subroutines[word]
573
+ # Handle both legacy (list) and new (dict with params) format
574
+ if isinstance(sub, list):
575
+ body = sub
576
+ param_names = []
577
+ else:
578
+ body = sub['body']
579
+ param_names = sub['params']
580
+
581
+ # Parse arguments: NAME arg1, arg2 @offset
582
+ rest = stmt[len(word):].strip()
583
+ offset = 0
584
+ m_off = re.search(r'@(\d+)', rest)
585
+ if m_off:
586
+ offset = int(m_off.group(1))
587
+ rest = rest[:m_off.start()].strip()
588
+
589
+ # Parse call arguments
590
+ call_args = [a.strip() for a in rest.split(',') if a.strip()] if rest else []
591
+
592
+ # Build param map for single-pass substitution
593
+ param_map = {}
594
+ for i, pname in enumerate(param_names):
595
+ if i < len(call_args):
596
+ param_map[pname] = call_args[i]
597
+ if param_map:
598
+ pattern = re.compile(r'\b(' + '|'.join(re.escape(p) for p in param_map) + r')\b')
599
+ def _sub(m):
600
+ return param_map[m.group(1)]
601
+
602
+ result = []
603
+ for gate_str in body:
604
+ gs = pattern.sub(_sub, gate_str) if param_map else gate_str
605
+ if offset:
606
+ gs = self._offset_qubits(gs, offset)
607
+ result.append(gs)
608
+ return result
609
+
610
+ def _offset_qubits(self, gate_str: str, offset: int) -> str:
611
+ """Add offset to qubit indices in a gate string, preserving parameters.
612
+
613
+ Handles both plain integers and register[index] notation.
614
+ """
615
+ tokens = self._tokenize_gate(gate_str)
616
+ if len(tokens) < 2:
617
+ return gate_str
618
+ gate_name = tokens[0].upper()
619
+ gate_key = GATE_ALIASES.get(gate_name, gate_name)
620
+ info = self._gate_info(gate_key)
621
+ n_params = info[0] if info else 0
622
+ result = [tokens[0]]
623
+ for i, tok in enumerate(tokens[1:]):
624
+ if i < n_params:
625
+ result.append(tok)
626
+ else:
627
+ # Skip register[index] notation -- it resolves its own offset
628
+ m = RE_REG_INDEX.match(tok)
629
+ if m:
630
+ result.append(tok)
631
+ else:
632
+ try:
633
+ result.append(str(int(tok) + offset))
634
+ except ValueError:
635
+ result.append(tok)
636
+ return ' '.join(result)
637
+
638
+ def _apply_gate_str(self, stmt, qc, _call_stack=None, *, backend=None):
639
+ """Parse and apply a single gate string to the circuit.
640
+
641
+ When backend is provided, standard gates are dispatched through
642
+ backend.apply_gate() instead of self._apply_gate(qc, ...).
643
+ """
644
+ stmt = stmt.strip()
645
+ if not stmt:
646
+ return
647
+
648
+ # Expand subroutines with call-stack tracking for recursion detection
649
+ word = stmt.split()[0].upper() if stmt.split() else ''
650
+ # Also check for parenthesized call syntax: NAME(args)
651
+ m_paren = re.match(r'(\w+)\(', stmt)
652
+ if m_paren:
653
+ paren_name = m_paren.group(1).upper()
654
+ if paren_name in self.subroutines:
655
+ word = paren_name
656
+ if word in self.subroutines:
657
+ for sub_stmt in self._expand_statement(stmt, _call_stack):
658
+ self._apply_gate_str(sub_stmt, qc, _call_stack, backend=backend)
659
+ return
660
+
661
+ upper = stmt.upper()
662
+ if upper.startswith('REM') or upper.startswith("'") or upper == 'BARRIER':
663
+ if upper == 'BARRIER':
664
+ if backend:
665
+ backend.barrier()
666
+ else:
667
+ qc.barrier()
668
+ return
669
+ if upper == 'MEASURE':
670
+ return # handled at run level
671
+
672
+ # CTRL gate ctrl_qubit, target_qubit(s) -- controlled version of any gate
673
+ m_ctrl = RE_CTRL.match(stmt)
674
+ if m_ctrl:
675
+ from qiskit.circuit.library import (HGate, XGate, YGate, ZGate,
676
+ SGate, TGate, SdgGate, TdgGate, SXGate, SwapGate)
677
+ gate_name = m_ctrl.group(1).upper()
678
+ args = [a.strip() for a in m_ctrl.group(2).replace(',', ' ').split()]
679
+ ctrl_qubit = self._resolve_qubit(args[0])
680
+ target_qubits = [self._resolve_qubit(a) for a in args[1:]]
681
+ gate_map = {
682
+ 'H': HGate(), 'X': XGate(), 'Y': YGate(), 'Z': ZGate(),
683
+ 'S': SGate(), 'T': TGate(), 'SDG': SdgGate(), 'TDG': TdgGate(),
684
+ 'SX': SXGate(), 'SWAP': SwapGate(),
685
+ }
686
+ all_qubits = [ctrl_qubit] + target_qubits
687
+ if gate_name in gate_map:
688
+ gate = gate_map[gate_name].control(1)
689
+ if backend and hasattr(backend, 'append_controlled'):
690
+ backend.append_controlled(gate, all_qubits)
691
+ else:
692
+ qc.append(gate, all_qubits)
693
+ elif gate_name in self._custom_gates:
694
+ from qiskit.circuit.library import UnitaryGate
695
+ gate = UnitaryGate(self._custom_gates[gate_name]).control(1)
696
+ if backend and hasattr(backend, 'append_controlled'):
697
+ backend.append_controlled(gate, all_qubits)
698
+ else:
699
+ qc.append(gate, all_qubits)
700
+ else:
701
+ raise ValueError(f"CTRL {gate_name}: gate not found")
702
+ return
703
+
704
+ # INV gate qubit(s) -- inverse/dagger of a single gate
705
+ m_inv = RE_INV.match(stmt)
706
+ if m_inv:
707
+ gate_name = m_inv.group(1).upper()
708
+ inv_args = m_inv.group(2)
709
+ tokens = self._tokenize_gate(f"{gate_name} {inv_args}")
710
+ gate_key = GATE_ALIASES.get(gate_name, gate_name)
711
+ info = self._gate_info(gate_key)
712
+ if info is not None:
713
+ n_params, n_qubits_needed = info
714
+ t_args = tokens[1:]
715
+ params = [self.eval_expr(a) for a in t_args[:n_params]]
716
+ qubits_inv = [self._resolve_qubit(a) for a in t_args[n_params:n_params+n_qubits_needed]]
717
+ sub_qc = QuantumCircuit(n_qubits_needed)
718
+ self._apply_gate(sub_qc, gate_key, params, list(range(n_qubits_needed)))
719
+ if backend and hasattr(backend, 'append_inverse'):
720
+ backend.append_inverse(sub_qc, qubits_inv)
721
+ else:
722
+ qc.append(sub_qc.inverse(), qubits_inv)
723
+ return
724
+
725
+ # Parse: GATE [params] qubits
726
+ # Tokenize
727
+ tokens = self._tokenize_gate(stmt)
728
+ if not tokens:
729
+ return
730
+
731
+ gate_name = tokens[0].upper()
732
+ gate_name = GATE_ALIASES.get(gate_name, gate_name)
733
+
734
+ info = self._gate_info(gate_name)
735
+ if info is None:
736
+ raise ValueError(f"UNKNOWN GATE: {gate_name}")
737
+
738
+ n_params, n_qubits = info
739
+ args = tokens[1:]
740
+
741
+ # Parse arguments: first n_params are parameters, rest are qubits
742
+ if len(args) < n_params + n_qubits:
743
+ raise ValueError(
744
+ f"{gate_name} needs {n_params} param(s) and {n_qubits} qubit(s), "
745
+ f"got {len(args)} arg(s)")
746
+
747
+ params = [self.eval_expr(a) for a in args[:n_params]]
748
+ qubits = [self._resolve_qubit(a) for a in args[n_params:n_params+n_qubits]]
749
+
750
+ # Apply gate through backend when available
751
+ try:
752
+ if backend:
753
+ backend.apply_gate(gate_name, tuple(params), qubits)
754
+ else:
755
+ self._apply_gate(qc, gate_name, params, qubits)
756
+ except Exception as _gate_err:
757
+ if 'duplicate' in str(_gate_err).lower():
758
+ raise QBasicBuildError(
759
+ f"duplicate qubit arguments in {gate_name}"
760
+ ) from None
761
+ raise
762
+
763
+ def _tokenize_gate(self, stmt: str) -> list[str]:
764
+ """Split gate statement into tokens, handling commas and register notation.
765
+
766
+ When arguments are comma-separated, splits on commas only so that
767
+ compound expressions like ``I + 1`` are preserved as single tokens.
768
+ Falls back to whitespace splitting when no commas are present
769
+ (legacy format like ``CX 0 1``).
770
+ """
771
+ stmt = RE_REG_INDEX.sub(r'\1[\2]', stmt)
772
+ parts = stmt.strip().split(None, 1)
773
+ if len(parts) < 2:
774
+ return [parts[0]] if parts else []
775
+ gate = parts[0]
776
+ args_str = parts[1]
777
+ if ',' in args_str:
778
+ # Comma-separated: split on commas, preserving expressions
779
+ args = [a.strip() for a in args_str.split(',') if a.strip()]
780
+ else:
781
+ # Space-separated: split on whitespace (legacy)
782
+ args = [a.strip() for a in args_str.split() if a.strip()]
783
+ return [gate] + args
784
+
785
+ def _resolve_qubit(self, arg: str, *, n_qubits: int | None = None) -> int:
786
+ """Resolve a qubit argument and validate range.
787
+
788
+ Accepts: integer literal, register[index], or expression.
789
+ Validates against n_qubits (defaults to self.num_qubits).
790
+ """
791
+ limit = n_qubits if n_qubits is not None else self.num_qubits
792
+ m = RE_REG_INDEX.match(arg)
793
+ if m:
794
+ name = m.group(1).lower()
795
+ idx = int(m.group(2))
796
+ if name not in self.registers:
797
+ raise QBasicRangeError(f"UNKNOWN REGISTER: {name}")
798
+ start, size = self.registers[name]
799
+ if idx >= size:
800
+ raise QBasicRangeError(f"{name}[{idx}] OUT OF RANGE (size={size})")
801
+ q = start + idx
802
+ else:
803
+ try:
804
+ q = int(self.eval_expr(arg))
805
+ except Exception:
806
+ raise ValueError(f"CANNOT RESOLVE QUBIT: {arg}")
807
+ if q < 0 or q >= limit:
808
+ raise QBasicRangeError(f"QUBIT {q} OUT OF RANGE (0-{limit-1})")
809
+ return q
810
+
811
+ def _apply_gate(self, qc, gate_name, params, qubits):
812
+ """Apply a gate to the quantum circuit."""
813
+ n = qc.num_qubits
814
+ for q in qubits:
815
+ if q < 0 or q >= n:
816
+ raise QBasicRangeError(
817
+ f"QUBIT {q} OUT OF RANGE (0-{n-1}) in {gate_name}"
818
+ )
819
+ method_name = self._GATE_DISPATCH.get(gate_name)
820
+ if method_name:
821
+ method = getattr(qc, method_name)
822
+ method(*params, *qubits)
823
+ elif gate_name in self._custom_gates:
824
+ from qiskit.circuit.library import UnitaryGate
825
+ qc.append(UnitaryGate(self._custom_gates[gate_name]), qubits)
826
+ else:
827
+ raise ValueError(f"GATE {gate_name} NOT IMPLEMENTED")
828
+
829
+ def run_immediate(self, line: str) -> None:
830
+ """Execute a single gate command immediately.
831
+
832
+ Uses the same _exec_line pipeline as cmd_run for consistency.
833
+ """
834
+ # In LOCC mode, handle @register prefix via the numpy engine
835
+ if self.locc_mode and self.locc:
836
+ m = RE_AT_REG_LINE.match(line)
837
+ if m:
838
+ reg = m.group(1).upper()
839
+ gate_stmt = m.group(2).strip()
840
+ if reg not in self.locc.names:
841
+ self.io.writeln(f"?UNKNOWN REGISTER: {reg} (have {', '.join(self.locc.names)})")
842
+ return
843
+ self._locc_apply_gate(reg, gate_stmt)
844
+ self._locc_state()
845
+ return
846
+ if line.strip().startswith('@'):
847
+ self.io.writeln("?@register syntax requires LOCC mode (try: LOCC <n1> <n2>)")
848
+ return
849
+ # Build and execute through the same gate pipeline as cmd_run
850
+ from qbasic_core.exec_context import ExecContext
851
+ qc = QuantumCircuit(self.num_qubits)
852
+ imm_ctx = ExecContext(sorted_lines=[0], ip=0,
853
+ run_vars=dict(self.variables), qc=qc)
854
+ self._exec_line(line, ctx=imm_ctx)
855
+ qc.save_statevector()
856
+ backend = AerSimulator(method='statevector')
857
+ result = backend.run(transpile(qc, backend)).result()
858
+ sv = np.array(result.get_statevector())
859
+ self.last_sv = sv
860
+ self.last_circuit = qc
861
+ self._print_sv_compact(sv)