qubasic 0.6.1__tar.gz → 0.6.4__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.6.1 → qubasic-0.6.4}/CHANGELOG.md +22 -0
- {qubasic-0.6.1/qubasic.egg-info → qubasic-0.6.4}/PKG-INFO +2 -2
- {qubasic-0.6.1 → qubasic-0.6.4}/README.md +1 -1
- {qubasic-0.6.1 → qubasic-0.6.4}/pyproject.toml +1 -1
- {qubasic-0.6.1 → qubasic-0.6.4/qubasic.egg-info}/PKG-INFO +2 -2
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/__init__.py +1 -1
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/cli.py +10 -11
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/control_flow.py +126 -38
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/engine_state.py +14 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/executor.py +91 -20
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/file_io.py +0 -2
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/locc_engine.py +19 -4
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/locc_execution.py +1 -2
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/memory.py +8 -2
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/parser.py +8 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/terminal.py +27 -7
- {qubasic-0.6.1 → qubasic-0.6.4}/tests/test_features.py +36 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/tests/test_qubasic.py +86 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/LICENSE +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/MANIFEST.in +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/examples/bell.qb +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/examples/grover3.qb +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/examples/locc_teleport.qb +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/examples/sweep_rx.qb +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic.egg-info/SOURCES.txt +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic.egg-info/dependency_links.txt +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic.egg-info/entry_points.txt +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic.egg-info/requires.txt +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic.egg-info/top_level.txt +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/__main__.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/analysis.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/backend.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/classic.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/debug.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/demos.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/display.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/engine.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/errors.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/exec_context.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/expression.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/gates.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/help_text.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/io_protocol.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/locc.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/locc_commands.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/locc_display.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/mock_backend.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/noise_mixin.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/patterns.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/profiler.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/program_mgmt.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/protocol.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/qol.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/scope.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/screen.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/state_display.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/statements.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/strings.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/subs.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/qubasic_core/sweep.py +0 -0
- {qubasic-0.6.1 → qubasic-0.6.4}/setup.cfg +0 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.4 (2026-06-19)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Immediate-mode `PRINT` at the REPL no longer prints a spurious statevector after its output. `PRINT` is not a dispatch-table command, so it fell through to the immediate gate path, which always dumped `|psi>`. That path now skips the dump when the typed line added no circuit operations, so gate and subroutine entries still show state while `PRINT`, `MEASURE`, and other classical statements do not.
|
|
7
|
+
- `--quiet` now prints results with the banner suppressed, as documented. The results branch was unreachable (guarded by `elif not quiet` inside an `if quiet or json_mode` block), so `--quiet` previously produced no output at all.
|
|
8
|
+
- A `MEASURE` reachable only inside a `DEF`/`SUB` body, an `IF ... THEN/ELSE` clause, or a colon compound is now detected, so the program takes the shots path (and `qubasic script.qb` auto-runs) instead of the no-measure statevector path.
|
|
9
|
+
- `LET m(i, j) = x` writes a multi-dimensional array element using the same flat-stride convention the expression-side accessor reads, so multi-dimensional reads and writes agree.
|
|
10
|
+
- Immediate-mode `LET a(i) = <expr>` at the REPL assigns the array element, matching the in-program `LET` (previously only the in-program form handled array targets).
|
|
11
|
+
|
|
12
|
+
## 0.6.3 (2026-06-18)
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Colon-compound `@register` lines in LOCC mode now apply every gate. `@A H 0 : @A CX 0,1` (and the inherited form `@A H 0 : CX 0,1`) were captured as a single statement and mis-tokenized, so all but the first gate were silently dropped. The parser now splits them into per-statement parts with register inheritance, and the immediate-mode REPL path does the same.
|
|
16
|
+
- `SAVE_EXPECT`, `SAVE_PROBS`, and `SAVE_AMPS` no longer overwrite their target variable with 0 at circuit-build time. The placeholder keeps any prior value, so a program that re-runs with the SAVE line still present can read the previous result during the build pass (the post-run extraction then fills in the fresh value).
|
|
17
|
+
- `PRINT` honors `;` and `,` between multiple items: `PRINT "S ="; x` concatenates and `PRINT a, b` advances to the next 14-column zone. Separators inside quoted strings and inside call parentheses (`PRINT LEFT$(s$, 3)`) are no longer treated as item breaks.
|
|
18
|
+
- `PRINT` variable substitution no longer rewrites identifiers inside quoted string literals, so a label such as `PRINT "value of S"` is emitted verbatim even when `S` is a defined variable.
|
|
19
|
+
|
|
20
|
+
## 0.6.2 (2026-06-18)
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Changing the qubit count after a run (via `QUBITS n` or `POKE $D000`) now invalidates the cached statevector, so `MAP`, a `PEEK` of the qubit-state block, and `BLOCH` no longer reshape a stale statevector to the new qubit count and crash.
|
|
24
|
+
|
|
3
25
|
## 0.6.1 (2026-06-18)
|
|
4
26
|
|
|
5
27
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -661,7 +661,7 @@ QBASIC detects available RAM, estimates per-instance memory, and reports maximum
|
|
|
661
661
|
### Simulation method selection
|
|
662
662
|
- **automatic**: stabilizer for Clifford-only circuits, MPS for >28 qubits, statevector otherwise
|
|
663
663
|
- **stabilizer**: polynomial-time for Clifford circuits (H, S, CX, SWAP, etc.)
|
|
664
|
-
- **matrix_product_state**:
|
|
664
|
+
- **matrix_product_state**: memory-efficient for low-entanglement circuits; handles the full 32-qubit range that would exhaust statevector
|
|
665
665
|
- **extended_stabilizer**: approximate simulation for near-Clifford circuits
|
|
666
666
|
- **statevector**: exact, limited by RAM (~28 qubits on 16GB)
|
|
667
667
|
- **density_matrix**: includes mixed states, ~14 qubits on 16GB
|
|
@@ -628,7 +628,7 @@ QBASIC detects available RAM, estimates per-instance memory, and reports maximum
|
|
|
628
628
|
### Simulation method selection
|
|
629
629
|
- **automatic**: stabilizer for Clifford-only circuits, MPS for >28 qubits, statevector otherwise
|
|
630
630
|
- **stabilizer**: polynomial-time for Clifford circuits (H, S, CX, SWAP, etc.)
|
|
631
|
-
- **matrix_product_state**:
|
|
631
|
+
- **matrix_product_state**: memory-efficient for low-entanglement circuits; handles the full 32-qubit range that would exhaust statevector
|
|
632
632
|
- **extended_stabilizer**: approximate simulation for near-Clifford circuits
|
|
633
633
|
- **statevector**: exact, limited by RAM (~28 qubits on 16GB)
|
|
634
634
|
- **density_matrix**: includes mixed states, ~14 qubits on 16GB
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qubasic
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: Quantum BASIC Interactive Terminal
|
|
5
5
|
Author-email: "Charles C. Norton" <machineelv@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -661,7 +661,7 @@ QBASIC detects available RAM, estimates per-instance memory, and reports maximum
|
|
|
661
661
|
### Simulation method selection
|
|
662
662
|
- **automatic**: stabilizer for Clifford-only circuits, MPS for >28 qubits, statevector otherwise
|
|
663
663
|
- **stabilizer**: polynomial-time for Clifford circuits (H, S, CX, SWAP, etc.)
|
|
664
|
-
- **matrix_product_state**:
|
|
664
|
+
- **matrix_product_state**: memory-efficient for low-entanglement circuits; handles the full 32-qubit range that would exhaust statevector
|
|
665
665
|
- **extended_stabilizer**: approximate simulation for near-Clifford circuits
|
|
666
666
|
- **statevector**: exact, limited by RAM (~28 qubits on 16GB)
|
|
667
667
|
- **density_matrix**: includes mixed states, ~14 qubits on 16GB
|
|
@@ -38,12 +38,8 @@ def run_script(path: str, terminal: 'QBasicTerminal') -> None:
|
|
|
38
38
|
ProgramMgmtMixin._load_lines_with_defs(
|
|
39
39
|
lines, lambda line: terminal.process(line, track_undo=False))
|
|
40
40
|
|
|
41
|
-
# Auto-run if the program has a MEASURE
|
|
42
|
-
|
|
43
|
-
terminal.program.get(ln, '').strip().upper() == 'MEASURE'
|
|
44
|
-
for ln in terminal.program
|
|
45
|
-
)
|
|
46
|
-
if terminal.program and has_measure:
|
|
41
|
+
# Auto-run if the program has a reachable MEASURE (incl. in subs / IF clauses)
|
|
42
|
+
if terminal.program and terminal._program_has_measure(sorted(terminal.program)):
|
|
47
43
|
terminal.cmd_run()
|
|
48
44
|
|
|
49
45
|
|
|
@@ -125,8 +121,12 @@ def main():
|
|
|
125
121
|
print(_json.dumps({'error': err}, indent=2))
|
|
126
122
|
sys.exit(1)
|
|
127
123
|
print(_json.dumps(term.result(), indent=2))
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
else:
|
|
125
|
+
# --quiet (no --json): the banner was never emitted into the
|
|
126
|
+
# buffer (only the non-captured path calls print_banner), so the
|
|
127
|
+
# captured text is exactly the results. Print them, matching the
|
|
128
|
+
# documented "suppress banner, output results only" behavior.
|
|
129
|
+
print(buf.getvalue(), end='')
|
|
130
130
|
if err is not None:
|
|
131
131
|
print(f"?ERROR: {err}")
|
|
132
132
|
sys.exit(1)
|
|
@@ -134,9 +134,8 @@ def main():
|
|
|
134
134
|
term.print_banner()
|
|
135
135
|
run_script(path, term)
|
|
136
136
|
# Exit 0 on success; 1 if a measured program produced no counts.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
for ln in term.program) else 1)
|
|
137
|
+
expects_measure = bool(term.program) and term._program_has_measure(sorted(term.program))
|
|
138
|
+
sys.exit(0 if term.last_counts is not None or not expects_measure else 1)
|
|
140
139
|
else:
|
|
141
140
|
term.repl()
|
|
142
141
|
|
|
@@ -40,12 +40,60 @@ class ControlFlowMixin:
|
|
|
40
40
|
# signature for compatibility with _exec_control_flow's argument
|
|
41
41
|
# passing but is not used by the methods themselves.
|
|
42
42
|
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _split_arg_list(s: str) -> list[str]:
|
|
45
|
+
"""Split a comma list at top level (outside quotes and brackets)."""
|
|
46
|
+
parts: list[str] = []
|
|
47
|
+
buf = ''
|
|
48
|
+
depth = 0
|
|
49
|
+
quote: str | None = None
|
|
50
|
+
for ch in s:
|
|
51
|
+
if quote:
|
|
52
|
+
buf += ch
|
|
53
|
+
if ch == quote:
|
|
54
|
+
quote = None
|
|
55
|
+
elif ch in ('"', "'"):
|
|
56
|
+
quote = ch
|
|
57
|
+
buf += ch
|
|
58
|
+
elif ch in '([':
|
|
59
|
+
depth += 1
|
|
60
|
+
buf += ch
|
|
61
|
+
elif ch in ')]':
|
|
62
|
+
depth = max(0, depth - 1)
|
|
63
|
+
buf += ch
|
|
64
|
+
elif ch == ',' and depth == 0:
|
|
65
|
+
parts.append(buf)
|
|
66
|
+
buf = ''
|
|
67
|
+
else:
|
|
68
|
+
buf += ch
|
|
69
|
+
if buf.strip():
|
|
70
|
+
parts.append(buf)
|
|
71
|
+
return [p.strip() for p in parts if p.strip()]
|
|
72
|
+
|
|
43
73
|
def _cf_let_array(self, stmt: str, run_vars: dict[str, Any],
|
|
44
74
|
parsed: LetArrayStmt) -> tuple[bool, ExecOutcome]:
|
|
45
75
|
name, idx_expr, val_expr = parsed.name, parsed.index_expr, parsed.value_expr
|
|
46
76
|
base = getattr(self, '_option_base', 0)
|
|
47
|
-
idx = int(self._eval_with_vars(idx_expr, run_vars)) - base
|
|
48
77
|
val = self._eval_with_vars(val_expr, run_vars)
|
|
78
|
+
parts = self._split_arg_list(idx_expr)
|
|
79
|
+
if len(parts) > 1:
|
|
80
|
+
# Multi-dimensional write: flatten with the same stride convention
|
|
81
|
+
# the expression-side accessor uses, so reads and writes agree.
|
|
82
|
+
dims = getattr(self, '_array_dims', {}).get(name)
|
|
83
|
+
indices = [int(self._eval_with_vars(p, run_vars)) - base for p in parts]
|
|
84
|
+
if any(i < 0 for i in indices):
|
|
85
|
+
raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx_expr})")
|
|
86
|
+
flat, stride = 0, 1
|
|
87
|
+
for k in range(len(indices) - 1, -1, -1):
|
|
88
|
+
flat += indices[k] * stride
|
|
89
|
+
stride *= dims[k] if dims and k < len(dims) else 1
|
|
90
|
+
if name not in self.arrays:
|
|
91
|
+
raise RuntimeError(f"ARRAY NOT DIMENSIONED: {name} (use DIM first)")
|
|
92
|
+
if flat < 0 or flat >= len(self.arrays[name]):
|
|
93
|
+
raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx_expr})")
|
|
94
|
+
self.arrays[name][flat] = val
|
|
95
|
+
return True, ExecResult.ADVANCE
|
|
96
|
+
idx = int(self._eval_with_vars(idx_expr, run_vars)) - base
|
|
49
97
|
if idx < 0:
|
|
50
98
|
raise RuntimeError(f"ARRAY INDEX OUT OF RANGE: {name}({idx + base})")
|
|
51
99
|
if name not in self.arrays:
|
|
@@ -63,46 +111,86 @@ class ControlFlowMixin:
|
|
|
63
111
|
self.variables[name] = val
|
|
64
112
|
return True, ExecResult.ADVANCE
|
|
65
113
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _split_print_items(expr: str) -> list[tuple[str, str]]:
|
|
116
|
+
"""Split a PRINT argument list into (item, trailing-separator) pairs.
|
|
117
|
+
|
|
118
|
+
';' and ',' are recognized only at top level (outside quotes and
|
|
119
|
+
parentheses/brackets), so PRINT LEFT$(s$, 3) stays one item and a comma
|
|
120
|
+
inside a quoted literal is preserved. The separator recorded with each
|
|
121
|
+
item is the one that follows it; '' marks the final item / no trailing
|
|
122
|
+
separator.
|
|
123
|
+
"""
|
|
124
|
+
items: list[tuple[str, str]] = []
|
|
125
|
+
buf = ''
|
|
126
|
+
depth = 0
|
|
127
|
+
quote: str | None = None
|
|
128
|
+
for ch in expr:
|
|
129
|
+
if quote:
|
|
130
|
+
buf += ch
|
|
131
|
+
if ch == quote:
|
|
132
|
+
quote = None
|
|
133
|
+
elif ch in ('"', "'"):
|
|
134
|
+
quote = ch
|
|
135
|
+
buf += ch
|
|
136
|
+
elif ch in '([':
|
|
137
|
+
depth += 1
|
|
138
|
+
buf += ch
|
|
139
|
+
elif ch in ')]':
|
|
140
|
+
depth = max(0, depth - 1)
|
|
141
|
+
buf += ch
|
|
142
|
+
elif ch in ';,' and depth == 0:
|
|
143
|
+
items.append((buf.strip(), ch))
|
|
144
|
+
buf = ''
|
|
145
|
+
else:
|
|
146
|
+
buf += ch
|
|
147
|
+
if buf.strip():
|
|
148
|
+
items.append((buf.strip(), ''))
|
|
149
|
+
return items
|
|
150
|
+
|
|
151
|
+
def _eval_print_item(self, item: str, run_vars: dict[str, Any]) -> str:
|
|
152
|
+
"""Evaluate a single PRINT item to its display string."""
|
|
153
|
+
item = item.strip()
|
|
154
|
+
if not item:
|
|
155
|
+
return ''
|
|
156
|
+
# Quoted literal: emit verbatim (no substitution, no SPC/TAB).
|
|
157
|
+
if (item[0] == '"' and item[-1] == '"') or (item[0] == "'" and item[-1] == "'"):
|
|
158
|
+
return item[1:-1]
|
|
159
|
+
text = self._substitute_vars(item, run_vars)
|
|
160
|
+
|
|
161
|
+
def _spaces(m):
|
|
91
162
|
try:
|
|
92
|
-
|
|
93
|
-
result = self._safe_eval(text, extra_ns=ns)
|
|
94
|
-
output = str(result)
|
|
163
|
+
return ' ' * max(0, int(self._eval_with_vars(m.group(1), run_vars)))
|
|
95
164
|
except Exception:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
165
|
+
return ''
|
|
166
|
+
text = re.sub(r'\bSPC\s*\(([^)]+)\)', _spaces, text, flags=re.IGNORECASE)
|
|
167
|
+
text = re.sub(r'\bTAB\s*\(([^)]+)\)', _spaces, text, flags=re.IGNORECASE)
|
|
168
|
+
if not text.strip():
|
|
169
|
+
return text # standalone SPC/TAB -> whitespace
|
|
170
|
+
try:
|
|
171
|
+
ns = run_vars.as_dict() if hasattr(run_vars, 'as_dict') else (
|
|
172
|
+
run_vars if isinstance(run_vars, dict) else dict(run_vars))
|
|
173
|
+
return str(self._safe_eval(text, extra_ns=ns))
|
|
174
|
+
except Exception:
|
|
175
|
+
return text
|
|
176
|
+
|
|
177
|
+
def _cf_print(self, stmt: str, run_vars: dict[str, Any],
|
|
178
|
+
parsed: PrintStmt) -> tuple[bool, ExecOutcome]:
|
|
179
|
+
items = self._split_print_items(parsed.expr)
|
|
180
|
+
if not items:
|
|
181
|
+
self.io.writeln('') # bare PRINT -> blank line
|
|
182
|
+
return True, ExecResult.ADVANCE
|
|
183
|
+
out = ''
|
|
184
|
+
for item, sep in items:
|
|
185
|
+
out += self._eval_print_item(item, run_vars)
|
|
186
|
+
if sep == ',': # advance to next 14-column zone
|
|
187
|
+
col = len(out) % 14
|
|
188
|
+
out += ' ' * (14 - col if col else 14)
|
|
189
|
+
# A trailing ';' or ',' suppresses the newline (cursor stays on line).
|
|
190
|
+
if items[-1][1] in (';', ','):
|
|
191
|
+
self.io.write(out)
|
|
104
192
|
else:
|
|
105
|
-
self.io.writeln(
|
|
193
|
+
self.io.writeln(out)
|
|
106
194
|
return True, ExecResult.ADVANCE
|
|
107
195
|
|
|
108
196
|
def _cf_goto(self, stmt: str, sorted_lines: list[int],
|
|
@@ -119,3 +119,17 @@ class Engine:
|
|
|
119
119
|
self._circuit_cache = None
|
|
120
120
|
self._pending_set_state = None
|
|
121
121
|
self._poke_state_prep = {}
|
|
122
|
+
|
|
123
|
+
def _invalidate_run_state(self) -> None:
|
|
124
|
+
"""Drop cached results that describe a now-stale configuration.
|
|
125
|
+
|
|
126
|
+
Called when the qubit count changes: last_sv, last_counts,
|
|
127
|
+
last_circuit, and the transpile cache all assume the previous
|
|
128
|
+
num_qubits, so reading them afterwards would mislabel (or, for
|
|
129
|
+
the reshape in _peek_qubit/_bloch_vector, crash) the state.
|
|
130
|
+
"""
|
|
131
|
+
self.last_counts = None
|
|
132
|
+
self.last_sv = None
|
|
133
|
+
self.last_circuit = None
|
|
134
|
+
self._circuit_cache_key = None
|
|
135
|
+
self._circuit_cache = None
|
|
@@ -66,6 +66,41 @@ class ExecutorMixin:
|
|
|
66
66
|
|
|
67
67
|
# ── Circuit Building ──────────────────────────────────────────────
|
|
68
68
|
|
|
69
|
+
def _program_has_measure(self, sorted_lines) -> bool:
|
|
70
|
+
"""True if any reachable statement measures.
|
|
71
|
+
|
|
72
|
+
The plain per-line scan misses a MEASURE that only appears inside a DEF
|
|
73
|
+
subroutine body or an IF/THEN-ELSE clause, which left the run on the
|
|
74
|
+
no-measure path with no counts. This recursion-guarded walk follows
|
|
75
|
+
colon compounds, IF clauses, and subroutine bodies so the shots path
|
|
76
|
+
fires whenever a measurement is actually reachable.
|
|
77
|
+
"""
|
|
78
|
+
from qubasic_core.statements import MeasureStmt, CompoundStmt, IfThenStmt
|
|
79
|
+
from qubasic_core.parser import parse_stmt
|
|
80
|
+
|
|
81
|
+
def scan(text: str, seen: frozenset) -> bool:
|
|
82
|
+
p = parse_stmt(text)
|
|
83
|
+
if isinstance(p, MeasureStmt):
|
|
84
|
+
return True
|
|
85
|
+
if isinstance(p, CompoundStmt):
|
|
86
|
+
return any(scan(part, seen) for part in p.parts)
|
|
87
|
+
if isinstance(p, IfThenStmt):
|
|
88
|
+
if p.then_clause and scan(p.then_clause, seen):
|
|
89
|
+
return True
|
|
90
|
+
if p.else_clause and scan(p.else_clause, seen):
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
words = text.strip().split()
|
|
94
|
+
if words:
|
|
95
|
+
word = words[0].upper().split('(')[0]
|
|
96
|
+
if word in self.subroutines and word not in seen:
|
|
97
|
+
sub = self.subroutines[word]
|
|
98
|
+
body = sub['body'] if isinstance(sub, dict) else sub
|
|
99
|
+
return any(scan(b, seen | {word}) for b in body)
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
return any(scan(self.program[ln], frozenset()) for ln in sorted_lines)
|
|
103
|
+
|
|
69
104
|
def build_circuit(self) -> tuple['QuantumCircuit', bool]:
|
|
70
105
|
"""Compile program lines into a QuantumCircuit. Returns (circuit, has_measure)."""
|
|
71
106
|
from qubasic_core.exec_context import ExecContext
|
|
@@ -92,7 +127,9 @@ class ExecutorMixin:
|
|
|
92
127
|
qc=qc,
|
|
93
128
|
backend=backend,
|
|
94
129
|
)
|
|
95
|
-
|
|
130
|
+
# Reachability scan catches MEASURE inside subroutines and IF clauses,
|
|
131
|
+
# not just literal top-level MEASURE lines.
|
|
132
|
+
has_measure = self._program_has_measure(ctx.sorted_lines)
|
|
96
133
|
self._on_measure_fired = False
|
|
97
134
|
|
|
98
135
|
while ctx.ip < len(ctx.sorted_lines):
|
|
@@ -328,20 +365,42 @@ class ExecutorMixin:
|
|
|
328
365
|
set(self.subroutines.keys()) |
|
|
329
366
|
set(self._custom_gates.keys())
|
|
330
367
|
)
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
continue
|
|
338
|
-
if tok in merged:
|
|
339
|
-
# Don't expand record bases (dicts) — leave p in p.x intact so
|
|
340
|
-
# the expression evaluator can resolve the field.
|
|
341
|
-
if isinstance(merged[tok], dict):
|
|
368
|
+
|
|
369
|
+
def _sub_segment(seg: str) -> str:
|
|
370
|
+
# Tokenize: split on word boundaries, preserving delimiters
|
|
371
|
+
tokens = re.split(r'(\b\w+\b)', seg)
|
|
372
|
+
for i, tok in enumerate(tokens):
|
|
373
|
+
if not tok or not tok[0].isalpha():
|
|
342
374
|
continue
|
|
343
|
-
|
|
344
|
-
|
|
375
|
+
if tok in protected or tok.upper() in protected or tok.lower() in protected:
|
|
376
|
+
continue
|
|
377
|
+
if tok in merged:
|
|
378
|
+
# Don't expand record bases (dicts) — leave p in p.x intact
|
|
379
|
+
# so the expression evaluator can resolve the field.
|
|
380
|
+
if isinstance(merged[tok], dict):
|
|
381
|
+
continue
|
|
382
|
+
tokens[i] = str(merged[tok])
|
|
383
|
+
return ''.join(tokens)
|
|
384
|
+
|
|
385
|
+
# Substitute only outside quoted string literals: a variable named in a
|
|
386
|
+
# PRINT label (PRINT "S ="; S) must survive verbatim inside the quotes.
|
|
387
|
+
out: list[str] = []
|
|
388
|
+
i, n = 0, len(stmt)
|
|
389
|
+
while i < n:
|
|
390
|
+
ch = stmt[i]
|
|
391
|
+
if ch in ('"', "'"):
|
|
392
|
+
j = i + 1
|
|
393
|
+
while j < n and stmt[j] != ch:
|
|
394
|
+
j += 1
|
|
395
|
+
out.append(stmt[i:j + 1]) # quoted span, verbatim
|
|
396
|
+
i = j + 1
|
|
397
|
+
else:
|
|
398
|
+
j = i
|
|
399
|
+
while j < n and stmt[j] not in ('"', "'"):
|
|
400
|
+
j += 1
|
|
401
|
+
out.append(_sub_segment(stmt[i:j])) # unquoted span
|
|
402
|
+
i = j
|
|
403
|
+
return ''.join(out)
|
|
345
404
|
|
|
346
405
|
def _expand_statement(self, stmt, _call_stack: set[str] | None = None):
|
|
347
406
|
"""Expand subroutines. Returns list of gate strings.
|
|
@@ -636,18 +695,24 @@ class ExecutorMixin:
|
|
|
636
695
|
|
|
637
696
|
Uses the same _exec_line pipeline as cmd_run for consistency.
|
|
638
697
|
"""
|
|
639
|
-
# In LOCC mode, handle @register prefix via the numpy engine
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
698
|
+
# In LOCC mode, handle @register prefix via the numpy engine. Split
|
|
699
|
+
# colon-compound @REG lines so each gate is applied (a single @REG line
|
|
700
|
+
# is just the one-part case); a bare @REG line keeps prior behavior.
|
|
701
|
+
if self.locc_mode and self.locc and line.strip().startswith('@'):
|
|
702
|
+
parts = self._split_colon_stmts(line) if ':' in line else [line.strip()]
|
|
703
|
+
for part in parts:
|
|
704
|
+
m = RE_AT_REG_LINE.match(part)
|
|
705
|
+
if not m:
|
|
706
|
+
self.io.writeln(f"?BAD @register statement: {part}")
|
|
707
|
+
continue
|
|
643
708
|
reg = m.group(1).upper()
|
|
644
709
|
gate_stmt = m.group(2).strip()
|
|
645
710
|
if reg not in self.locc.names:
|
|
646
711
|
self.io.writeln(f"?UNKNOWN REGISTER: {reg} (have {', '.join(self.locc.names)})")
|
|
647
712
|
return
|
|
648
713
|
self._locc_apply_gate(reg, gate_stmt)
|
|
649
|
-
|
|
650
|
-
|
|
714
|
+
self._locc_state()
|
|
715
|
+
return
|
|
651
716
|
if line.strip().startswith('@'):
|
|
652
717
|
self.io.writeln("?@register syntax requires LOCC mode (try: LOCC <n1> <n2>)")
|
|
653
718
|
return
|
|
@@ -657,6 +722,12 @@ class ExecutorMixin:
|
|
|
657
722
|
imm_ctx = ExecContext(sorted_lines=[0], ip=0,
|
|
658
723
|
run_vars=dict(self.variables), qc=qc)
|
|
659
724
|
self._exec_line(line, ctx=imm_ctx)
|
|
725
|
+
# A classical statement typed at the prompt (PRINT, MEASURE, an IF whose
|
|
726
|
+
# clause printed, ...) adds no circuit operations and has already emitted
|
|
727
|
+
# its own output via _exec_line. Simulating an empty circuit just to print
|
|
728
|
+
# an unchanged |psi> would be spurious, so skip the statevector dump.
|
|
729
|
+
if qc.size() == 0:
|
|
730
|
+
return
|
|
660
731
|
qc.save_statevector()
|
|
661
732
|
backend = self._make_backend('statevector')
|
|
662
733
|
result = backend.run(transpile(qc, backend)).result()
|
|
@@ -195,8 +195,6 @@ class FileIOMixin:
|
|
|
195
195
|
DEFs in the imported file are prefixed with the module name.
|
|
196
196
|
E.g., IMPORT "math.qb" makes DEF ROT available as MATH.ROT.
|
|
197
197
|
"""
|
|
198
|
-
from qubasic_core.engine import RE_IMPORT
|
|
199
|
-
m = RE_IMPORT.match(f"IMPORT {rest}") if not rest.startswith('IMPORT') else RE_IMPORT.match(rest)
|
|
200
198
|
path = rest.strip().strip('"').strip("'")
|
|
201
199
|
if not path:
|
|
202
200
|
self.io.writeln('?USAGE: IMPORT "filename"')
|
|
@@ -180,7 +180,13 @@ class LOCCEngine:
|
|
|
180
180
|
self._renormalize(reg)
|
|
181
181
|
|
|
182
182
|
def apply(self, reg: str, gate_name: str, params: tuple[float, ...], qubits: list[int]) -> None:
|
|
183
|
-
"""Apply a gate to a
|
|
183
|
+
"""Apply a gate to a register, then apply noise if configured.
|
|
184
|
+
|
|
185
|
+
Qubit indices are register-LOCAL (0..size-1); the register offset into
|
|
186
|
+
the joint statevector is added internally. Callers that index the raw
|
|
187
|
+
statevector (e.g. for fidelity/inspection) must convert with
|
|
188
|
+
``offsets[reg] + local`` themselves.
|
|
189
|
+
"""
|
|
184
190
|
self._check_qubits(reg, qubits)
|
|
185
191
|
matrix = _np_gate_matrix(gate_name, tuple(params))
|
|
186
192
|
if self.joint:
|
|
@@ -194,7 +200,10 @@ class LOCCEngine:
|
|
|
194
200
|
self._contiguate(reg)
|
|
195
201
|
|
|
196
202
|
def share(self, reg1: str, q1: int, reg2: str, q2: int) -> None:
|
|
197
|
-
"""Create Bell pair |Phi+> between reg1[q1] and reg2[q2]. JOINT only.
|
|
203
|
+
"""Create Bell pair |Phi+> between reg1[q1] and reg2[q2]. JOINT only.
|
|
204
|
+
|
|
205
|
+
``q1``/``q2`` are register-LOCAL indices; offsets are added internally.
|
|
206
|
+
"""
|
|
198
207
|
if not self.joint:
|
|
199
208
|
raise RuntimeError("SHARE requires LOCC JOINT mode")
|
|
200
209
|
self._check_qubits(reg1, [q1])
|
|
@@ -210,7 +219,10 @@ class LOCCEngine:
|
|
|
210
219
|
self._apply_noise(reg2, [q2])
|
|
211
220
|
|
|
212
221
|
def send(self, reg: str, qubit: int) -> int:
|
|
213
|
-
"""Measure a qubit (Born rule) and return the classical outcome.
|
|
222
|
+
"""Measure a qubit (Born rule) and return the classical outcome.
|
|
223
|
+
|
|
224
|
+
``qubit`` is register-LOCAL (0..size-1); the offset is added internally.
|
|
225
|
+
"""
|
|
214
226
|
self._check_qubits(reg, [qubit])
|
|
215
227
|
if self.joint:
|
|
216
228
|
actual = qubit + self.offsets[self._idx(reg)]
|
|
@@ -266,7 +278,10 @@ class LOCCEngine:
|
|
|
266
278
|
return per_reg, {}
|
|
267
279
|
|
|
268
280
|
def apply_matrix(self, reg: str, matrix: np.ndarray, qubits: list[int]) -> None:
|
|
269
|
-
"""Apply a raw unitary matrix to qubits in a register.
|
|
281
|
+
"""Apply a raw unitary matrix to qubits in a register.
|
|
282
|
+
|
|
283
|
+
``qubits`` are register-LOCAL indices; offsets are added internally.
|
|
284
|
+
"""
|
|
270
285
|
self._check_qubits(reg, qubits)
|
|
271
286
|
if self.joint:
|
|
272
287
|
idx = self._idx(reg)
|
|
@@ -33,8 +33,7 @@ class LOCCExecutionMixin:
|
|
|
33
33
|
if not sorted_lines:
|
|
34
34
|
self.io.writeln("NOTHING TO RUN")
|
|
35
35
|
return
|
|
36
|
-
has_measure =
|
|
37
|
-
for l in sorted_lines)
|
|
36
|
+
has_measure = self._program_has_measure(sorted_lines)
|
|
38
37
|
has_send = any(re.search(r'\bSEND\b', self.program[l], re.IGNORECASE)
|
|
39
38
|
for l in sorted_lines)
|
|
40
39
|
if has_send:
|
|
@@ -103,7 +103,10 @@ class MemoryMixin:
|
|
|
103
103
|
qubit, field = off // QUBIT_BLOCK, off % QUBIT_BLOCK
|
|
104
104
|
if self.last_sv is None or qubit >= self.num_qubits:
|
|
105
105
|
return 0.0
|
|
106
|
-
|
|
106
|
+
sv_flat = np.ascontiguousarray(self.last_sv).ravel()
|
|
107
|
+
if sv_flat.size != 2 ** self.num_qubits:
|
|
108
|
+
return 0.0 # stale statevector from a different qubit count
|
|
109
|
+
sv = sv_flat.reshape([2] * self.num_qubits)
|
|
107
110
|
ax = self.num_qubits - 1 - qubit
|
|
108
111
|
t = np.moveaxis(sv, ax, 0)
|
|
109
112
|
t0, t1 = t[0].flatten(), t[1].flatten()
|
|
@@ -155,7 +158,10 @@ class MemoryMixin:
|
|
|
155
158
|
self._zero_page[a] = v
|
|
156
159
|
return
|
|
157
160
|
if a == 0xD000:
|
|
158
|
-
|
|
161
|
+
new_n = max(1, min(32, int(v)))
|
|
162
|
+
if new_n != self.num_qubits:
|
|
163
|
+
self._invalidate_run_state()
|
|
164
|
+
self.num_qubits = new_n
|
|
159
165
|
elif a == 0xD001:
|
|
160
166
|
self.shots = max(1, int(v))
|
|
161
167
|
elif a == 0xD002:
|
|
@@ -554,6 +554,14 @@ def parse_stmt(raw: str) -> Stmt:
|
|
|
554
554
|
|
|
555
555
|
# ── @REG lines ────────────────────────────────────────────────
|
|
556
556
|
if text.startswith('@'):
|
|
557
|
+
# A colon-compound @REG line (e.g. "@A H 0 : @A CX 0,1" or
|
|
558
|
+
# "@A H 0 : CX 0,1") must split into per-statement parts with
|
|
559
|
+
# @register inheritance; otherwise the whole tail is captured as one
|
|
560
|
+
# AtRegStmt.inner and the LOCC executor mis-tokenizes it.
|
|
561
|
+
if ':' in text:
|
|
562
|
+
parts = _split_colon_stmts(text)
|
|
563
|
+
if len(parts) > 1:
|
|
564
|
+
return CompoundStmt(raw=raw, parts=tuple(parts))
|
|
557
565
|
m = RE_AT_REG_LINE.match(text)
|
|
558
566
|
if m:
|
|
559
567
|
return AtRegStmt(raw=raw, reg=m.group(1).upper(),
|
|
@@ -571,6 +571,8 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
571
571
|
from qubasic_core.errors import QBasicRangeError
|
|
572
572
|
raise QBasicRangeError(f"RANGE: 1-{MAX_QUBITS}")
|
|
573
573
|
|
|
574
|
+
if n != self.num_qubits:
|
|
575
|
+
self._invalidate_run_state()
|
|
574
576
|
self.num_qubits = n
|
|
575
577
|
self.registers.clear()
|
|
576
578
|
est = _estimate_gb(n)
|
|
@@ -926,7 +928,18 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
926
928
|
|
|
927
929
|
def cmd_let(self, rest: str) -> None:
|
|
928
930
|
"""LET <var> = <expr> — assign a computed value to a variable.
|
|
929
|
-
Supports record fields
|
|
931
|
+
Supports record fields (LET p.x = 3.14) and array elements
|
|
932
|
+
(LET a(0) = PI, LET m(i, j) = x), matching the in-program LET."""
|
|
933
|
+
from qubasic_core.patterns import RE_LET_ARRAY
|
|
934
|
+
am = RE_LET_ARRAY.match(f"LET {rest}")
|
|
935
|
+
if am:
|
|
936
|
+
from qubasic_core.statements import LetArrayStmt
|
|
937
|
+
name, idx_expr, val_expr = am.group(1), am.group(2), am.group(3)
|
|
938
|
+
parsed = LetArrayStmt(raw=f"LET {rest}", name=name,
|
|
939
|
+
index_expr=idx_expr, value_expr=val_expr)
|
|
940
|
+
self._cf_let_array(f"LET {rest}", self.variables, parsed)
|
|
941
|
+
self.io.writeln(f"{name}({idx_expr.strip()}) = {self.eval_expr(val_expr)}")
|
|
942
|
+
return
|
|
930
943
|
m = re.match(r'(\w+(?:\.\w+)?)\s*=\s*(.*)', rest)
|
|
931
944
|
if not m:
|
|
932
945
|
self.io.writeln("?USAGE: LET <var> = <expr>")
|
|
@@ -1980,8 +1993,13 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1980
1993
|
full_pauli[self.num_qubits - 1 - qubits[i]] = p
|
|
1981
1994
|
op = SparsePauliOp(''.join(full_pauli))
|
|
1982
1995
|
qc.save_expectation_value(op, list(range(self.num_qubits)), label=f'exp_{var}')
|
|
1983
|
-
|
|
1984
|
-
|
|
1996
|
+
# Placeholder until the run fills the real value via
|
|
1997
|
+
# _extract_save_results. Preserve any prior value instead of
|
|
1998
|
+
# zeroing it, so a re-run that re-includes this SAVE line leaves
|
|
1999
|
+
# earlier results readable to LET/PRINT during the build pass.
|
|
2000
|
+
_prev = self.variables.get(var, 0)
|
|
2001
|
+
run_vars[var] = _prev
|
|
2002
|
+
self.variables[var] = _prev
|
|
1985
2003
|
except Exception as e:
|
|
1986
2004
|
self.io.writeln(f"?SAVE_EXPECT ERROR: {e}")
|
|
1987
2005
|
return True
|
|
@@ -1996,8 +2014,9 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
1996
2014
|
var = m.group(2)
|
|
1997
2015
|
try:
|
|
1998
2016
|
qc.save_probabilities(qubits, label=f'prob_{var}')
|
|
1999
|
-
|
|
2000
|
-
|
|
2017
|
+
_prev = self.variables.get(var, 0) # preserve prior value (see SAVE_EXPECT)
|
|
2018
|
+
run_vars[var] = _prev
|
|
2019
|
+
self.variables[var] = _prev
|
|
2001
2020
|
except Exception as e:
|
|
2002
2021
|
self.io.writeln(f"?SAVE_PROBS ERROR: {e}")
|
|
2003
2022
|
return True
|
|
@@ -2012,8 +2031,9 @@ class QBasicTerminal(Engine, ExecutorMixin, ExpressionMixin, DisplayMixin, DemoM
|
|
|
2012
2031
|
var = m.group(2)
|
|
2013
2032
|
try:
|
|
2014
2033
|
qc.save_amplitudes(indices, label=f'amp_{var}')
|
|
2015
|
-
|
|
2016
|
-
|
|
2034
|
+
_prev = self.variables.get(var, 0) # preserve prior value (see SAVE_EXPECT)
|
|
2035
|
+
run_vars[var] = _prev
|
|
2036
|
+
self.variables[var] = _prev
|
|
2017
2037
|
except Exception as e:
|
|
2018
2038
|
self.io.writeln(f"?SAVE_AMPS ERROR: {e}")
|
|
2019
2039
|
return True
|
|
@@ -359,6 +359,42 @@ class TestMemoryMap(unittest.TestCase):
|
|
|
359
359
|
self.assertEqual(ntype, 2)
|
|
360
360
|
self.assertAlmostEqual(nparam, 0.1)
|
|
361
361
|
|
|
362
|
+
def test_qubit_count_change_invalidates_stale_state(self):
|
|
363
|
+
"""Changing the qubit count after a run must not leave a stale last_sv
|
|
364
|
+
that MAP/PEEK/BLOCH then reshape to the wrong size and crash."""
|
|
365
|
+
import numpy as np
|
|
366
|
+
|
|
367
|
+
# Run a 1-qubit circuit so last_sv has size 2.
|
|
368
|
+
t = QBasicTerminal()
|
|
369
|
+
t.num_qubits = 1
|
|
370
|
+
t.process('10 H 0')
|
|
371
|
+
t.process('20 MEASURE')
|
|
372
|
+
capture(t.cmd_run)
|
|
373
|
+
|
|
374
|
+
# QUBITS grows the count: the size-2 statevector no longer applies.
|
|
375
|
+
capture(t.cmd_qubits, '2')
|
|
376
|
+
self.assertIsNone(t.last_sv)
|
|
377
|
+
self.assertIsNone(t._circuit_cache_key)
|
|
378
|
+
self.assertEqual(t._peek(0x0100), 0.0) # would have crashed on reshape
|
|
379
|
+
capture(t.cmd_map) # MAP walks the qubit block
|
|
380
|
+
capture(t.cmd_bloch, '0') # BLOCH reshapes too
|
|
381
|
+
|
|
382
|
+
# The $D000 POKE path invalidates the same way.
|
|
383
|
+
t2 = QBasicTerminal()
|
|
384
|
+
t2.num_qubits = 1
|
|
385
|
+
t2.process('10 H 0')
|
|
386
|
+
t2.process('20 MEASURE')
|
|
387
|
+
capture(t2.cmd_run)
|
|
388
|
+
t2._poke(0xD000, 3)
|
|
389
|
+
self.assertIsNone(t2.last_sv)
|
|
390
|
+
self.assertEqual(t2._peek(0x0100), 0.0)
|
|
391
|
+
|
|
392
|
+
# Defensive guard: even a hand-set mismatched last_sv reads as 0, not a crash.
|
|
393
|
+
t3 = QBasicTerminal()
|
|
394
|
+
t3.num_qubits = 2
|
|
395
|
+
t3.last_sv = np.array([1.0, 0.0], dtype=complex) # size 2, but 2 qubits
|
|
396
|
+
self.assertEqual(t3._peek(0x0100), 0.0)
|
|
397
|
+
|
|
362
398
|
def test_sys(self):
|
|
363
399
|
"""SYS builtin BELL, SYS unmapped, SYS in program, SYS INSTALL."""
|
|
364
400
|
# SYS 0xE000 BELL demo
|
|
@@ -1432,6 +1432,92 @@ class TestAdditionalCoverage(unittest.TestCase):
|
|
|
1432
1432
|
self.assertEqual(t4.last_counts.get('1', 0), 100)
|
|
1433
1433
|
|
|
1434
1434
|
|
|
1435
|
+
# ---------------------------------------------------------------------------
|
|
1436
|
+
# TestBugFixes063 — regressions fixed in 0.6.3
|
|
1437
|
+
# ---------------------------------------------------------------------------
|
|
1438
|
+
class TestBugFixes063(unittest.TestCase):
|
|
1439
|
+
"""Regression tests for the 0.6.3 fixes."""
|
|
1440
|
+
|
|
1441
|
+
def _ghz3(self):
|
|
1442
|
+
v = np.zeros(8, dtype=complex)
|
|
1443
|
+
v[0] = v[7] = 1 / np.sqrt(2)
|
|
1444
|
+
return v
|
|
1445
|
+
|
|
1446
|
+
def test_locc_colon_compound_at_register(self):
|
|
1447
|
+
"""LOCC: colon-compound @REG lines apply every gate (explicit and inherited)."""
|
|
1448
|
+
for line in ('@A H 0 : @A CX 0,1 : @A CX 0,2', # explicit @A per clause
|
|
1449
|
+
'@A H 0 : CX 0,1 : CX 0,2'): # @A inherited
|
|
1450
|
+
t = QBasicTerminal()
|
|
1451
|
+
t.locc = LOCCEngine([3], joint=True)
|
|
1452
|
+
t.locc_mode = True
|
|
1453
|
+
t.shots = 1
|
|
1454
|
+
t.process(f'10 {line}', track_undo=False)
|
|
1455
|
+
t.process('20 MEASURE', track_undo=False)
|
|
1456
|
+
capture(t.cmd_run)
|
|
1457
|
+
sv = np.ascontiguousarray(t.locc.sv).ravel()
|
|
1458
|
+
fid = abs(np.vdot(self._ghz3(), sv)) ** 2
|
|
1459
|
+
self.assertGreater(fid, 0.999, f"GHZ not built by: {line}")
|
|
1460
|
+
|
|
1461
|
+
def test_save_expect_preserved_across_rerun(self):
|
|
1462
|
+
"""SAVE_EXPECT keeps prior values on re-run instead of zeroing them."""
|
|
1463
|
+
t = QBasicTerminal()
|
|
1464
|
+
t.num_qubits = 2
|
|
1465
|
+
t.shots = 64
|
|
1466
|
+
t.process('10 H 0', track_undo=False)
|
|
1467
|
+
t.process('20 CX 0,1', track_undo=False)
|
|
1468
|
+
t.process('30 SAVE_EXPECT ZZ 0,1 -> zz', track_undo=False)
|
|
1469
|
+
t.process('40 SAVE_EXPECT XX 0,1 -> xx', track_undo=False)
|
|
1470
|
+
capture(t.cmd_run)
|
|
1471
|
+
self.assertAlmostEqual(t.variables['zz'], 1.0, places=6)
|
|
1472
|
+
self.assertAlmostEqual(t.variables['xx'], 1.0, places=6)
|
|
1473
|
+
# A second run that still contains the SAVE lines must not zero zz/xx
|
|
1474
|
+
# before the LET on line 50 reads them.
|
|
1475
|
+
t.process('50 LET chsh = SQRT2 * (zz + xx)', track_undo=False)
|
|
1476
|
+
capture(t.cmd_run)
|
|
1477
|
+
self.assertAlmostEqual(t.variables['chsh'], 2 * math.sqrt(2), places=6)
|
|
1478
|
+
|
|
1479
|
+
def test_print_multi_item_separators(self):
|
|
1480
|
+
"""PRINT concatenates ';' items and tab-aligns ',' items."""
|
|
1481
|
+
t = QBasicTerminal()
|
|
1482
|
+
t.num_qubits = 1
|
|
1483
|
+
t.variables['S'] = 2.8284271247
|
|
1484
|
+
t.process('10 PRINT "S ="; S', track_undo=False)
|
|
1485
|
+
_, out = capture(t.cmd_run)
|
|
1486
|
+
self.assertIn('S =2.8284', out)
|
|
1487
|
+
|
|
1488
|
+
t2 = QBasicTerminal()
|
|
1489
|
+
t2.num_qubits = 1
|
|
1490
|
+
t2.process('10 PRINT "a", "b"', track_undo=False)
|
|
1491
|
+
_, out2 = capture(t2.cmd_run)
|
|
1492
|
+
line = next(l for l in out2.splitlines() if 'a' in l and 'b' in l)
|
|
1493
|
+
self.assertGreaterEqual(line.index('b') - line.index('a'), 14)
|
|
1494
|
+
|
|
1495
|
+
def test_print_does_not_substitute_inside_quotes(self):
|
|
1496
|
+
"""A variable name inside a quoted PRINT literal is emitted verbatim."""
|
|
1497
|
+
t = QBasicTerminal()
|
|
1498
|
+
t.num_qubits = 1
|
|
1499
|
+
t.variables['S'] = 99.0
|
|
1500
|
+
t.process('10 PRINT "value of S here"', track_undo=False)
|
|
1501
|
+
_, out = capture(t.cmd_run)
|
|
1502
|
+
self.assertIn('value of S here', out)
|
|
1503
|
+
self.assertNotIn('99', out)
|
|
1504
|
+
|
|
1505
|
+
def test_print_preserves_commas_in_quotes_and_calls(self):
|
|
1506
|
+
"""Top-level split skips commas inside quotes and inside call parens."""
|
|
1507
|
+
t = QBasicTerminal()
|
|
1508
|
+
t.num_qubits = 1
|
|
1509
|
+
t.process('10 PRINT "x,y,z"', track_undo=False)
|
|
1510
|
+
_, out = capture(t.cmd_run)
|
|
1511
|
+
self.assertIn('x,y,z', out)
|
|
1512
|
+
|
|
1513
|
+
t2 = QBasicTerminal()
|
|
1514
|
+
t2.num_qubits = 1
|
|
1515
|
+
t2.process('10 LET name$ = "hello"', track_undo=False)
|
|
1516
|
+
t2.process('20 PRINT LEFT$(name$, 3)', track_undo=False)
|
|
1517
|
+
_, out2 = capture(t2.cmd_run)
|
|
1518
|
+
self.assertIn('hel', out2)
|
|
1519
|
+
|
|
1520
|
+
|
|
1435
1521
|
if __name__ == '__main__':
|
|
1436
1522
|
if hasattr(sys.stdout, 'reconfigure'):
|
|
1437
1523
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
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
|