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.
- qbasic.py +113 -0
- qbasic_core/__init__.py +80 -0
- qbasic_core/__main__.py +6 -0
- qbasic_core/analysis.py +179 -0
- qbasic_core/backend.py +76 -0
- qbasic_core/classic.py +419 -0
- qbasic_core/control_flow.py +576 -0
- qbasic_core/debug.py +327 -0
- qbasic_core/demos.py +356 -0
- qbasic_core/display.py +274 -0
- qbasic_core/engine.py +126 -0
- qbasic_core/engine_state.py +109 -0
- qbasic_core/errors.py +37 -0
- qbasic_core/exec_context.py +24 -0
- qbasic_core/executor.py +861 -0
- qbasic_core/expression.py +228 -0
- qbasic_core/file_io.py +457 -0
- qbasic_core/gates.py +284 -0
- qbasic_core/help_text.py +167 -0
- qbasic_core/io_protocol.py +33 -0
- qbasic_core/locc.py +10 -0
- qbasic_core/locc_commands.py +221 -0
- qbasic_core/locc_display.py +61 -0
- qbasic_core/locc_engine.py +195 -0
- qbasic_core/locc_execution.py +389 -0
- qbasic_core/memory.py +369 -0
- qbasic_core/mock_backend.py +66 -0
- qbasic_core/noise_mixin.py +96 -0
- qbasic_core/parser.py +564 -0
- qbasic_core/patterns.py +186 -0
- qbasic_core/profiler.py +156 -0
- qbasic_core/program_mgmt.py +369 -0
- qbasic_core/protocol.py +77 -0
- qbasic_core/py.typed +0 -0
- qbasic_core/scope.py +74 -0
- qbasic_core/screen.py +115 -0
- qbasic_core/state_display.py +60 -0
- qbasic_core/statements.py +387 -0
- qbasic_core/strings.py +107 -0
- qbasic_core/subs.py +261 -0
- qbasic_core/sweep.py +82 -0
- qbasic_core/terminal.py +1697 -0
- qubasic-0.1.0.dist-info/METADATA +736 -0
- qubasic-0.1.0.dist-info/RECORD +48 -0
- qubasic-0.1.0.dist-info/WHEEL +5 -0
- qubasic-0.1.0.dist-info/entry_points.txt +2 -0
- qubasic-0.1.0.dist-info/licenses/LICENSE +21 -0
- qubasic-0.1.0.dist-info/top_level.txt +2 -0
qbasic_core/executor.py
ADDED
|
@@ -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)
|