qubasic 0.13.0__tar.gz → 0.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qubasic-0.13.0 → qubasic-0.14.0}/CHANGELOG.md +16 -0
- {qubasic-0.13.0/qubasic.egg-info → qubasic-0.14.0}/PKG-INFO +2 -2
- {qubasic-0.13.0 → qubasic-0.14.0}/README.md +1 -1
- {qubasic-0.13.0 → qubasic-0.14.0}/pyproject.toml +1 -1
- {qubasic-0.13.0 → qubasic-0.14.0/qubasic.egg-info}/PKG-INFO +2 -2
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/__init__.py +1 -1
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/analysis.py +5 -2
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/cli.py +63 -3
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/control_flow.py +3 -2
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/engine_state.py +4 -1
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/executor.py +3 -1
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/expression.py +22 -12
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/patterns.py +2 -2
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/terminal.py +34 -32
- {qubasic-0.13.0 → qubasic-0.14.0}/tests/test_qubasic.py +28 -5
- {qubasic-0.13.0 → qubasic-0.14.0}/LICENSE +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/MANIFEST.in +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/examples/bell.qb +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/examples/grover3.qb +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/examples/locc_teleport.qb +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/examples/sweep_rx.qb +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/__main__.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/algorithms.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/algos2.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/backend.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/benchmarking.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/bosonic.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/classic.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/debug.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/demos.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/display.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/dynamics.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/engine.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/errors.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/file_io.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/gates.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/help_text.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/locc.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/locc_commands.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/locc_engine.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/locc_execution.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/memory.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/mock_backend.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/noise_mixin.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/parser.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/pauliprop.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/profiler.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/program_mgmt.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/protocol.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/qec.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/qol.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/qudits.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/resources.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/scope.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/screen.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/state_display.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/statements.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/strings.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/subs.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/qubasic_core/sweep.py +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/setup.cfg +0 -0
- {qubasic-0.13.0 → qubasic-0.14.0}/tests/test_features.py +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0 (2026-06-19)
|
|
4
|
+
|
|
5
|
+
Second audit-gap round: a complete agent contract, BASIC truth values, range
|
|
6
|
+
comparisons, louder mid-circuit-bit handling, string-array defaults, and lazy
|
|
7
|
+
density inspection. The quantum engine is unchanged.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `qubasic --spec` now reports the full language surface: a `statements` section (QFT, MEAS, SYNDROME, EVOLVE, QADD, CTRL/INV, the control-flow keywords, ...) alongside commands, each with a `signature` field, plus `true_value: -1`.
|
|
11
|
+
- `DIM s$(n)` declares a string array; unread elements default to `""` instead of `0.0`.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Comparisons return BASIC truth values: `-1` for true, `0` for false (so `LET t = (a > b)` is `-1`).
|
|
15
|
+
- Comparison chaining is Python-style, so `0 <= x <= 10` reads as `(0 <= x) AND (x <= 10)`.
|
|
16
|
+
- Reading a mid-circuit measurement bit (`MEAS`/`SYNDROME -> var`) in a classical expression now raises a clear error instead of silently using the placeholder 0; `IF <bit>` feedforward is unaffected.
|
|
17
|
+
- `DENSITY` solves the density matrix lazily, on demand, instead of on every measured `SET_DENSITY` run.
|
|
18
|
+
|
|
3
19
|
## 0.13.0 (2026-06-19)
|
|
4
20
|
|
|
5
21
|
Closes the remaining audit gaps in the classic-BASIC layer (conventions,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -245,7 +245,7 @@ Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
|
|
|
245
245
|
|
|
246
246
|
### Operators
|
|
247
247
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
248
|
-
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
248
|
+
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=` (yield -1 for true, 0 for false; chain Python-style, so `0 <= x <= 10` works)
|
|
249
249
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
250
250
|
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
251
251
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
@@ -212,7 +212,7 @@ Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
|
|
|
212
212
|
|
|
213
213
|
### Operators
|
|
214
214
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
215
|
-
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
215
|
+
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=` (yield -1 for true, 0 for false; chain Python-style, so `0 <= x <= 10` works)
|
|
216
216
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
217
217
|
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
218
218
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -245,7 +245,7 @@ Functions and keywords are case-insensitive (`SQRT` and `sqrt` both work).
|
|
|
245
245
|
|
|
246
246
|
### Operators
|
|
247
247
|
Arithmetic: `+`, `-`, `*`, `/`, `//`, `%`, `**`
|
|
248
|
-
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=`
|
|
248
|
+
Comparison: `==`, `!=`, `<>`, `<`, `>`, `<=`, `>=` (yield -1 for true, 0 for false; chain Python-style, so `0 <= x <= 10` works)
|
|
249
249
|
Logical: `AND`, `OR`, `NOT`, `XOR`
|
|
250
250
|
Bitwise: `AND`, `OR`, `XOR` on integers (`6 AND 3` = 2); `NOT` is logical
|
|
251
251
|
Hex/binary literals: `&HFF`, `&B10110`
|
|
@@ -109,9 +109,12 @@ class AnalysisMixin:
|
|
|
109
109
|
|
|
110
110
|
def cmd_density(self, rest: str = '') -> None:
|
|
111
111
|
"""Show density matrix (DENSITY [reg]); summarizes for large systems."""
|
|
112
|
-
# A mixed state set via SET_DENSITY has
|
|
113
|
-
#
|
|
112
|
+
# A mixed state set via SET_DENSITY has no statevector; solve its
|
|
113
|
+
# density matrix on demand (cached) from the captured circuit.
|
|
114
114
|
dm = getattr(self, '_last_density', None)
|
|
115
|
+
if dm is None and getattr(self, '_last_density_qc', None) is not None:
|
|
116
|
+
dm = self._density_from_qc(self._last_density_qc)
|
|
117
|
+
self._last_density = dm
|
|
115
118
|
if dm is not None and not rest.strip():
|
|
116
119
|
rho = np.ascontiguousarray(dm)
|
|
117
120
|
dim = rho.shape[0]
|
|
@@ -26,6 +26,54 @@ if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
|
|
|
26
26
|
from qubasic_core.terminal import QBasicTerminal
|
|
27
27
|
from qubasic_core.program_mgmt import ProgramMgmtMixin
|
|
28
28
|
|
|
29
|
+
# Statement-level operations that are parsed inline rather than dispatched as
|
|
30
|
+
# REPL commands or gates, so they are absent from the command tables and the
|
|
31
|
+
# gate table. Listed in --spec (name, signature, description) for agents.
|
|
32
|
+
_SPEC_STATEMENTS = [
|
|
33
|
+
('MEAS', 'MEAS <qubit> -> <bit>', 'mid-circuit measurement into a classical bit (drives IF feedforward)'),
|
|
34
|
+
('MEASURE', 'MEASURE [qubit list]', 'measure all qubits, or a subset, into the result histogram'),
|
|
35
|
+
('MEASURE_X', 'MEASURE_X <qubit>', 'measure in the X basis (result in mx_<q>)'),
|
|
36
|
+
('MEASURE_Y', 'MEASURE_Y <qubit>', 'measure in the Y basis (result in my_<q>)'),
|
|
37
|
+
('MEASURE_Z', 'MEASURE_Z <qubit>', 'measure in the Z basis (result in mz_<q>)'),
|
|
38
|
+
('SYNDROME', 'SYNDROME <paulis> <qubits> -> <var>', 'non-destructive stabilizer measurement via an ancilla'),
|
|
39
|
+
('RESET', 'RESET <qubit>', 'reset a qubit to |0>'),
|
|
40
|
+
('BARRIER', 'BARRIER', 'optimization barrier'),
|
|
41
|
+
('QFT', 'QFT <lo>-<hi>', 'quantum Fourier transform over a qubit range'),
|
|
42
|
+
('IQFT', 'IQFT <lo>-<hi>', 'inverse quantum Fourier transform'),
|
|
43
|
+
('DIFFUSE', 'DIFFUSE <lo>-<hi>', 'Grover diffusion operator'),
|
|
44
|
+
('MCX', 'MCX <ctrl,...>, <target>', 'multi-controlled X'),
|
|
45
|
+
('MCZ', 'MCZ <ctrl,...>', 'multi-controlled Z'),
|
|
46
|
+
('MCP', 'MCP <theta>, <ctrl,...>, <target>', 'multi-controlled phase'),
|
|
47
|
+
('QADD', 'QADD <a-range>, <b-range>', 'in-place register add A += B (mod 2^n)'),
|
|
48
|
+
('QADDC', 'QADDC <k>, <range>', 'in-place constant add A += k (mod 2^n)'),
|
|
49
|
+
('QPE', 'QPE <range> <target> <UGATE>', 'quantum phase estimation of a unitary'),
|
|
50
|
+
('AMPLIFY', 'AMPLIFY <marked>', 'one amplitude-amplification (Grover) step'),
|
|
51
|
+
('GRAPHSTATE', 'GRAPHSTATE <a-b, b-c, ...>', 'prepare a graph/cluster state'),
|
|
52
|
+
('FEATUREMAP', 'FEATUREMAP <x0> <x1> ...', 'ZZ feature-map data encoding'),
|
|
53
|
+
('EVOLVE', 'EVOLVE <H>, <time>, <steps>', 'Trotterized Hamiltonian time evolution'),
|
|
54
|
+
('APPLYCHANNEL', 'APPLYCHANNEL <name> <qubit>', 'apply a defined Kraus channel'),
|
|
55
|
+
('SAVE_EXPECT', 'SAVE_EXPECT <obs> <qubits> -> <var>', 'record an expectation value into a variable after RUN'),
|
|
56
|
+
('SAVE_PROBS', 'SAVE_PROBS <qubits> -> <array>', 'record a probability snapshot into an array after RUN'),
|
|
57
|
+
('SAVE_AMPS', 'SAVE_AMPS <lo>,<hi> -> <array>', 'record amplitudes into an array after RUN'),
|
|
58
|
+
('CTRL', 'CTRL <gate> <ctrl>, <target>', 'controlled version of any gate'),
|
|
59
|
+
('INV', 'INV <gate> <args>', 'inverse/dagger of a gate'),
|
|
60
|
+
('UNITARY', 'UNITARY <NAME> = [[...]]', 'define a custom gate from a unitary matrix'),
|
|
61
|
+
('GOTO', 'GOTO <line>', 'jump to a line number'),
|
|
62
|
+
('GOSUB', 'GOSUB <line>', 'call a line block; RETURN resumes'),
|
|
63
|
+
('FOR', 'FOR <v> = <a> TO <b> [STEP <s>]', 'counted loop, closed by NEXT'),
|
|
64
|
+
('WHILE', 'WHILE <cond>', 'pre-test loop, closed by WEND'),
|
|
65
|
+
('DO', 'DO [WHILE|UNTIL <cond>]', 'loop, closed by LOOP [WHILE|UNTIL]'),
|
|
66
|
+
('IF', 'IF <cond> THEN <stmt> [ELSE <stmt>]', 'conditional (single-line, or block ending in END IF)'),
|
|
67
|
+
('SELECT', 'SELECT CASE <expr>', 'multi-way branch (CASE / CASE ELSE / END SELECT)'),
|
|
68
|
+
('DATA', 'DATA <v1>, <v2>, ...', 'inline data consumed by READ'),
|
|
69
|
+
('READ', 'READ <var>, ...', 'read the next DATA values'),
|
|
70
|
+
('DEF', 'DEF <NAME>[(p)] = <gates>', 'define a gate-sequence subroutine (also DEF FN, DEF BEGIN)'),
|
|
71
|
+
('SUB', 'SUB <name>(<args>)', 'structured subroutine (END SUB; LOCAL/STATIC/SHARED)'),
|
|
72
|
+
('FUNCTION', 'FUNCTION <name>(<args>)', 'function returning a value (END FUNCTION)'),
|
|
73
|
+
('DIM', 'DIM <name>(<size>[,...])', 'declare an array (name$ for strings; inclusive sizing)'),
|
|
74
|
+
('REDIM', 'REDIM [PRESERVE] <name>(<size>)', 'resize an array; PRESERVE keeps existing data'),
|
|
75
|
+
]
|
|
76
|
+
|
|
29
77
|
|
|
30
78
|
def run_script(path: str, terminal: 'QBasicTerminal') -> None:
|
|
31
79
|
"""Run a .qb script file. Supports multi-line DEF blocks.
|
|
@@ -100,20 +148,32 @@ def main():
|
|
|
100
148
|
from qubasic_core.engine import GATE_TABLE
|
|
101
149
|
from qubasic_core.expression import ExpressionMixin
|
|
102
150
|
|
|
103
|
-
def
|
|
151
|
+
def _doc_parts(mname):
|
|
104
152
|
doc = (getattr(getattr(QBasicTerminal, mname, None), '__doc__', '') or '').strip()
|
|
105
|
-
|
|
153
|
+
line = doc.split('\n')[0].strip()
|
|
154
|
+
# Docstrings format the first line as "SIGNATURE — description".
|
|
155
|
+
for sep in ('—', ' - '):
|
|
156
|
+
if sep in line:
|
|
157
|
+
sig, _, hlp = line.partition(sep)
|
|
158
|
+
return sig.strip(), hlp.strip()
|
|
159
|
+
return '', line
|
|
106
160
|
|
|
107
161
|
cmds = []
|
|
108
162
|
for tbl, takes in ((QBasicTerminal._CMD_WITH_ARG, True),
|
|
109
163
|
(QBasicTerminal._CMD_NO_ARG, False)):
|
|
110
164
|
for cname, mname in tbl.items():
|
|
111
|
-
|
|
165
|
+
sig, hlp = _doc_parts(mname)
|
|
166
|
+
cmds.append({'name': cname, 'takes_arg': takes,
|
|
167
|
+
'signature': sig or cname, 'help': hlp})
|
|
168
|
+
statements = [{'name': n, 'signature': s, 'help': h}
|
|
169
|
+
for (n, s, h) in _SPEC_STATEMENTS]
|
|
112
170
|
spec = {
|
|
113
171
|
'name': 'qubasic',
|
|
114
172
|
'version': __version__,
|
|
115
173
|
'bit_order': 'little-endian (qubit 0 = rightmost bit)',
|
|
174
|
+
'true_value': -1,
|
|
116
175
|
'commands': sorted(cmds, key=lambda c: c['name']),
|
|
176
|
+
'statements': sorted(statements, key=lambda s: s['name']),
|
|
117
177
|
'gates': sorted(GATE_TABLE.keys()),
|
|
118
178
|
'functions': sorted(
|
|
119
179
|
set(ExpressionMixin._SAFE_FUNCS)
|
|
@@ -102,9 +102,10 @@ class ControlFlowMixin:
|
|
|
102
102
|
if idx < 0:
|
|
103
103
|
raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx + base})")
|
|
104
104
|
dimmed = getattr(self, '_dimmed_arrays', set())
|
|
105
|
+
fill = '' if name.endswith('$') else 0.0 # string arrays default to ""
|
|
105
106
|
if name not in self.arrays:
|
|
106
107
|
# Implicit array: created (and allowed to grow) on first assignment.
|
|
107
|
-
self.arrays[name] = [
|
|
108
|
+
self.arrays[name] = [fill] * (idx + 1)
|
|
108
109
|
elif idx >= len(self.arrays[name]):
|
|
109
110
|
if name in dimmed:
|
|
110
111
|
# Explicitly DIMmed: writes are bounds-checked like reads,
|
|
@@ -113,7 +114,7 @@ class ControlFlowMixin:
|
|
|
113
114
|
f"ARRAY INDEX OUT OF RANGE: {name}({idx + base}), "
|
|
114
115
|
f"size {len(self.arrays[name])}")
|
|
115
116
|
while idx >= len(self.arrays[name]):
|
|
116
|
-
self.arrays[name].append(
|
|
117
|
+
self.arrays[name].append(fill)
|
|
117
118
|
self.arrays[name][idx] = val
|
|
118
119
|
return True, ExecResult.ADVANCE
|
|
119
120
|
|
|
@@ -57,8 +57,10 @@ class Engine:
|
|
|
57
57
|
# Arrays declared with DIM/REDIM enforce their bounds on write;
|
|
58
58
|
# arrays created implicitly by first assignment keep auto-growing.
|
|
59
59
|
self._dimmed_arrays: set[str] = set()
|
|
60
|
-
# Density matrix
|
|
60
|
+
# Density matrix (and the measure-free circuit to solve it lazily)
|
|
61
|
+
# for a run with SET_DENSITY, computed on demand by DENSITY.
|
|
61
62
|
self._last_density: Any = None
|
|
63
|
+
self._last_density_qc: Any = None
|
|
62
64
|
|
|
63
65
|
# Subroutines and registers
|
|
64
66
|
self.subroutines: dict[str, Any] = {}
|
|
@@ -119,6 +121,7 @@ class Engine:
|
|
|
119
121
|
self._array_dims.clear()
|
|
120
122
|
self._dimmed_arrays.clear()
|
|
121
123
|
self._last_density = None
|
|
124
|
+
self._last_density_qc = None
|
|
122
125
|
self.last_counts = None
|
|
123
126
|
self.last_sv = None
|
|
124
127
|
self.last_circuit = None
|
|
@@ -113,8 +113,10 @@ class ExecutorMixin:
|
|
|
113
113
|
self._classical_bits = {}
|
|
114
114
|
# Partial-measurement subset (None = measure all at the end).
|
|
115
115
|
self._measure_subset = None
|
|
116
|
-
# Density matrix captured by a
|
|
116
|
+
# Density matrix (and circuit) captured by a SET_DENSITY run; the
|
|
117
|
+
# matrix is solved lazily by DENSITY from the stored circuit.
|
|
117
118
|
self._last_density = None
|
|
119
|
+
self._last_density_qc = None
|
|
118
120
|
# Apply any qubit state preparation requested via POKE to $0100.
|
|
119
121
|
if getattr(self, '_poke_state_prep', None):
|
|
120
122
|
self._emit_poke_state_prep(qc)
|
|
@@ -190,6 +190,15 @@ class ExpressionMixin:
|
|
|
190
190
|
return node.value
|
|
191
191
|
raise ValueError(f"UNSUPPORTED CONSTANT: {node.value!r}")
|
|
192
192
|
if isinstance(node, ast.Name):
|
|
193
|
+
# A mid-circuit measurement bit has no classical value (it is
|
|
194
|
+
# resolved per shot); reading it outside an IF feedforward is an
|
|
195
|
+
# error rather than a silent placeholder 0.
|
|
196
|
+
cb = getattr(self, '_classical_bits', None)
|
|
197
|
+
if cb and node.id in cb:
|
|
198
|
+
raise ValueError(
|
|
199
|
+
f"'{node.id}' is a mid-circuit measurement bit; its value is "
|
|
200
|
+
f"per-shot, so it can't be read in a classical expression "
|
|
201
|
+
f"(use IF {node.id} for feedforward, or LOCC mode for a live value)")
|
|
193
202
|
if node.id in ns:
|
|
194
203
|
return ns[node.id]
|
|
195
204
|
raise ValueError(f"UNDEFINED: {node.id}")
|
|
@@ -215,18 +224,19 @@ class ExpressionMixin:
|
|
|
215
224
|
result = op(result, self._ast_eval(val, ns))
|
|
216
225
|
return result
|
|
217
226
|
if isinstance(node, ast.Compare):
|
|
218
|
-
#
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
# Python-style chaining (a < b < c means (a<b) and (b<c)) so range
|
|
228
|
+
# checks like 0 <= x <= 10 work as written; results use BASIC truth
|
|
229
|
+
# values (-1 for true, 0 for false).
|
|
230
|
+
left = self._ast_eval(node.left, ns)
|
|
231
|
+
for op_node, comparator in zip(node.ops, node.comparators):
|
|
232
|
+
op = self._AST_OPS.get(type(op_node))
|
|
233
|
+
if op is None:
|
|
234
|
+
raise ValueError(f"UNSUPPORTED OP: {type(op_node).__name__}")
|
|
235
|
+
right = self._ast_eval(comparator, ns)
|
|
236
|
+
if not op(left, right):
|
|
237
|
+
return 0
|
|
238
|
+
left = right
|
|
239
|
+
return -1
|
|
230
240
|
if isinstance(node, ast.Call):
|
|
231
241
|
if not isinstance(node.func, ast.Name):
|
|
232
242
|
raise ValueError("ONLY SIMPLE FUNCTION CALLS ALLOWED")
|
|
@@ -17,7 +17,7 @@ RE_SHARE = re.compile(r'SHARE\s+([A-Z])\s+(\d+)\s*,?\s*([A-Z])\s+(\d+)', re.IGNO
|
|
|
17
17
|
RE_MEAS = re.compile(r'MEAS\s+(\S+)\s*->\s*(\w+)', re.IGNORECASE)
|
|
18
18
|
RE_RESET = re.compile(r'RESET\s+(\S+)', re.IGNORECASE)
|
|
19
19
|
RE_UNITARY = re.compile(r'UNITARY\s+(\w+)\s*=\s*(\[.+\])', re.IGNORECASE)
|
|
20
|
-
RE_DIM = re.compile(r'DIM\s+(\w
|
|
20
|
+
RE_DIM = re.compile(r'DIM\s+(\w+\$?)\((\d+)\)', re.IGNORECASE)
|
|
21
21
|
RE_REDIM = re.compile(r'REDIM\s+(PRESERVE\s+)?(\w+)\((\d+)\)', re.IGNORECASE)
|
|
22
22
|
RE_ERASE = re.compile(r'ERASE\s+(\w+)', re.IGNORECASE)
|
|
23
23
|
RE_GET = re.compile(r'GET\s+(\w+\$?)', re.IGNORECASE)
|
|
@@ -100,7 +100,7 @@ RE_SCREEN = re.compile(r'SCREEN\s+(\d+)', re.IGNORECASE)
|
|
|
100
100
|
RE_LPRINT = re.compile(r'LPRINT\s+(.*)', re.IGNORECASE)
|
|
101
101
|
RE_ON_MEASURE = re.compile(r'ON\s+MEASURE\s+GOSUB\s+(\d+)', re.IGNORECASE)
|
|
102
102
|
RE_ON_TIMER = re.compile(r'ON\s+TIMER\s*\((\d+)\)\s+GOSUB\s+(\d+)', re.IGNORECASE)
|
|
103
|
-
RE_DIM_MULTI = re.compile(r'DIM\s+(\w
|
|
103
|
+
RE_DIM_MULTI = re.compile(r'DIM\s+(\w+\$?)\((\d+(?:\s*,\s*\d+)*)\)', re.IGNORECASE)
|
|
104
104
|
RE_LET_STR = re.compile(r'LET\s+(\w+\$)\s*=\s*(.*)', re.IGNORECASE)
|
|
105
105
|
# Implicit LET: an assignment written without the LET keyword (x = 5, s$ = "hi",
|
|
106
106
|
# a(1) = 5, p.x = 3). Anchored lvalue followed by a single '=' that is not part
|
|
@@ -1244,6 +1244,28 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1244
1244
|
# it skip extraction; STATE/BLOCH then report no state.
|
|
1245
1245
|
_SV_EXTRACT_MAX_QUBITS = 24
|
|
1246
1246
|
|
|
1247
|
+
def _density_from_qc(self, qc_sv):
|
|
1248
|
+
"""Solve the density matrix of a measure-free circuit, for DENSITY.
|
|
1249
|
+
|
|
1250
|
+
Computed on demand (not on every run) from the circuit captured when a
|
|
1251
|
+
SET_DENSITY state was prepared.
|
|
1252
|
+
"""
|
|
1253
|
+
try:
|
|
1254
|
+
q2 = qc_sv.copy()
|
|
1255
|
+
q2.save_density_matrix()
|
|
1256
|
+
dm_backend = self._make_backend('density_matrix', include_noise=True)
|
|
1257
|
+
_kw = {}
|
|
1258
|
+
if self._seed is not None:
|
|
1259
|
+
_kw['seed_simulator'] = self._seed
|
|
1260
|
+
res = dm_backend.run(
|
|
1261
|
+
transpile(q2, dm_backend, optimization_level=self._transpile_opt_level),
|
|
1262
|
+
**_kw).result()
|
|
1263
|
+
data = res.data(0)
|
|
1264
|
+
dm = data.get('density_matrix') if hasattr(data, 'get') else None
|
|
1265
|
+
return np.array(dm) if dm is not None else None
|
|
1266
|
+
except Exception:
|
|
1267
|
+
return None
|
|
1268
|
+
|
|
1247
1269
|
def _extract_statevector(self, qc_sv) -> None:
|
|
1248
1270
|
"""Run the measurement-free circuit copy to get last_sv.
|
|
1249
1271
|
|
|
@@ -1255,25 +1277,13 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1255
1277
|
2^n statevector that cannot be displayed anyway.
|
|
1256
1278
|
"""
|
|
1257
1279
|
if getattr(self, '_pending_set_density', None) is not None:
|
|
1258
|
-
# Mixed state: no pure statevector.
|
|
1259
|
-
# DENSITY
|
|
1280
|
+
# Mixed state: no pure statevector. Defer the density-matrix solve
|
|
1281
|
+
# to DENSITY (lazy) so a measured run does not pay for it unused;
|
|
1282
|
+
# keep the measure-free circuit to solve from.
|
|
1260
1283
|
self.last_sv = None
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
dm_backend = self._make_backend('density_matrix', include_noise=True)
|
|
1265
|
-
_kw = {}
|
|
1266
|
-
if self._seed is not None:
|
|
1267
|
-
_kw['seed_simulator'] = self._seed
|
|
1268
|
-
dm_result = dm_backend.run(
|
|
1269
|
-
transpile(qc_sv, dm_backend,
|
|
1270
|
-
optimization_level=self._transpile_opt_level),
|
|
1271
|
-
**_kw).result()
|
|
1272
|
-
data = dm_result.data(0)
|
|
1273
|
-
dm = data.get('density_matrix') if hasattr(data, 'get') else None
|
|
1274
|
-
self._last_density = np.array(dm) if dm is not None else None
|
|
1275
|
-
except Exception:
|
|
1276
|
-
self._last_density = None
|
|
1284
|
+
self._last_density = None
|
|
1285
|
+
self._last_density_qc = (qc_sv if self.num_qubits <= self._SV_EXTRACT_MAX_QUBITS
|
|
1286
|
+
else None)
|
|
1277
1287
|
return
|
|
1278
1288
|
if self.num_qubits > self._SV_EXTRACT_MAX_QUBITS:
|
|
1279
1289
|
self.last_sv = None
|
|
@@ -1318,20 +1328,12 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1318
1328
|
"""Execute the no-MEASURE path: statevector (or density matrix), no shots."""
|
|
1319
1329
|
too_large = self.num_qubits > self._SV_EXTRACT_MAX_QUBITS
|
|
1320
1330
|
if getattr(self, '_pending_set_density', None) is not None and not too_large:
|
|
1321
|
-
# A mixed state has no statevector
|
|
1322
|
-
#
|
|
1323
|
-
# untranslatable-circuit error from Aer.
|
|
1331
|
+
# A mixed state has no statevector. Defer the density solve to
|
|
1332
|
+
# DENSITY (lazy); keep the measure-free circuit. Saving a statevector
|
|
1333
|
+
# here used to raise an untranslatable-circuit error from Aer.
|
|
1324
1334
|
self.last_sv = None
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
dm_backend = self._make_backend('density_matrix', include_noise=True)
|
|
1328
|
-
dm_result = dm_backend.run(
|
|
1329
|
-
transpile(qc_sv, dm_backend, optimization_level=self._transpile_opt_level)).result()
|
|
1330
|
-
data = dm_result.data(0)
|
|
1331
|
-
dm = data.get('density_matrix') if hasattr(data, 'get') else None
|
|
1332
|
-
self._last_density = np.array(dm) if dm is not None else None
|
|
1333
|
-
except Exception:
|
|
1334
|
-
self._last_density = None
|
|
1335
|
+
self._last_density = None
|
|
1336
|
+
self._last_density_qc = qc_sv
|
|
1335
1337
|
elif too_large:
|
|
1336
1338
|
self.last_sv = None
|
|
1337
1339
|
else:
|
|
@@ -1969,7 +1971,7 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1969
1971
|
total = 1
|
|
1970
1972
|
for s in sizes:
|
|
1971
1973
|
total *= s
|
|
1972
|
-
self.arrays[name] = [0.0] * total
|
|
1974
|
+
self.arrays[name] = [('' if name.endswith('$') else 0.0)] * total
|
|
1973
1975
|
if len(sizes) > 1:
|
|
1974
1976
|
self._array_dims[name] = sizes
|
|
1975
1977
|
self._dimmed_arrays.add(name)
|
|
@@ -2138,7 +2138,9 @@ class TestDeepFixRegressions(unittest.TestCase):
|
|
|
2138
2138
|
t.process('10 ID 0', track_undo=False)
|
|
2139
2139
|
_, out = capture(t.cmd_run)
|
|
2140
2140
|
self.assertNotIn('Unable to translate', out)
|
|
2141
|
-
|
|
2141
|
+
# Density is solved lazily: the measure-free circuit is captured now,
|
|
2142
|
+
# and DENSITY computes the matrix on demand.
|
|
2143
|
+
self.assertIsNotNone(getattr(t, '_last_density_qc', None))
|
|
2142
2144
|
_, dout = capture(t.cmd_density, '')
|
|
2143
2145
|
self.assertIn('Density matrix', dout)
|
|
2144
2146
|
|
|
@@ -2173,10 +2175,13 @@ class TestConventionFixesV2(unittest.TestCase):
|
|
|
2173
2175
|
self.assertEqual(self.t.eval_expr('round(-2.5)'), -3.0)
|
|
2174
2176
|
self.assertAlmostEqual(self.t.eval_expr('round(2.345, 2)'), 2.35)
|
|
2175
2177
|
|
|
2176
|
-
def
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2178
|
+
def test_comparison_truth_and_chaining(self):
|
|
2179
|
+
# Comparisons yield BASIC truth values: -1 for true, 0 for false.
|
|
2180
|
+
self.assertEqual(self.t._safe_eval('3 < 5'), -1)
|
|
2181
|
+
self.assertEqual(self.t._safe_eval('3 > 5'), 0)
|
|
2182
|
+
# Python-style chaining, so range checks work as written.
|
|
2183
|
+
self.assertEqual(self.t._safe_eval('0 <= 5 <= 10'), -1)
|
|
2184
|
+
self.assertEqual(self.t._safe_eval('0 <= 50 <= 10'), 0)
|
|
2180
2185
|
|
|
2181
2186
|
def test_dim_inclusive_top_index(self):
|
|
2182
2187
|
t = self._runp(['10 DIM a(5)', '20 LET a(5)=7', '30 LET v=a(5)'])
|
|
@@ -2203,6 +2208,24 @@ class TestConventionFixesV2(unittest.TestCase):
|
|
|
2203
2208
|
_, dout = capture(t.cmd_density, '')
|
|
2204
2209
|
self.assertIn('Density matrix', dout)
|
|
2205
2210
|
|
|
2211
|
+
def test_meas_bit_classical_use_errors(self):
|
|
2212
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
2213
|
+
for l in ['10 X 0', '20 MEAS 0 -> m', '30 LET y = m + 1', '40 MEASURE']:
|
|
2214
|
+
t.process(l, track_undo=False)
|
|
2215
|
+
_, out = capture(t.cmd_run)
|
|
2216
|
+
self.assertIn('mid-circuit', out)
|
|
2217
|
+
|
|
2218
|
+
def test_string_array_default_empty(self):
|
|
2219
|
+
t = QBasicTerminal(); t.num_qubits = 1
|
|
2220
|
+
t.process('DIM s$(3)', track_undo=False)
|
|
2221
|
+
self.assertEqual(t._safe_eval('s$(1)'), '')
|
|
2222
|
+
|
|
2223
|
+
def test_spec_statements_present(self):
|
|
2224
|
+
from qubasic_core.cli import _SPEC_STATEMENTS
|
|
2225
|
+
names = {n for (n, _s, _h) in _SPEC_STATEMENTS}
|
|
2226
|
+
for op in ('QFT', 'MEAS', 'SYNDROME', 'EVOLVE', 'QADD'):
|
|
2227
|
+
self.assertIn(op, names)
|
|
2228
|
+
|
|
2206
2229
|
|
|
2207
2230
|
if __name__ == '__main__':
|
|
2208
2231
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|